        \usepackage[utf8]{fontenc}
        \usepackage{amsmath,amsfonts,amssymb,amsthm}
        \usepackage[math-style=literal]{unicode-math}
        \usepackage{newunicodechar}
        \usepackage{graphicx}
    
ASCII map (and evennia supports UTF-8 characters and even emojis)

I don’t know (or don’t want to do) any programming - I just want to run a game!

Should your game rules be enforced by coded systems by human game masters?

What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?

Should players be able to post out-of-characters on channels and via other means like bulletin-boards?

Can only privileged Builders create things or should regular players also have limited build-capability?

Do you base your game off an existing RPG system or make up your own?

What are the game mechanics? How do you decide if an action succeeds or fails?

Does the flow of time matter in your game - does night and day change? What about seasons?

Do you want changing, global weather or should weather just be set manually in roleplay?

Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?

Will your characters be known by their name or only by their physical appearance?

Is a simple room description enough or should the description be able to change?

Can objects be hidden in the room? Can a person hide in the room?

How numerous are your objects? Do you want large loot-lists or are objects just role playing props?

Is each coin a separate object or do you just store a bank account value?

Do multiple similar objects form stack and how are those stacks handled in that case?

Does an object have weight or volume (so you cannot carry an infinite amount of them)?

Can you fight with a chair or a flower or must you use a special ‘weapon’ kind of thing?

_Should there be NPCs giving quests? If so, how do you track Quest status?

Can players have more than one Character active at a time or are they allowed to multi-play?

If a Character can hide in a room, what skill will decide if they are detected?

What does the skill tree look like? Can a Character gain experience to improve? By killing enemies? Solving quests? By roleplaying?

What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?

Optional Evennia settings (if not given, these defaults are used)

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.

Changed in version 1.0: Channels stopped supporting Msg and TmpMsg, using only log files.

Changed in version 1.0: Changed the old ‘view’ lock to control the help-index inclusion and added
the new ‘read’ lock-type to control access to the entry itself.

Changed in version 1.0: Replaced the old bag-of-words algorithm with lunr package.

Changed in version 1.0: Channels dropped Msg-support. Now only used in page command by default.

Changed in version 1.0: In previous Evennia versions, stopping the Script’s timer also meant deleting the Script object.
Starting with this version, the timer can be start/stopped separately and .delete() must be called
on the Script explicitly to delete it.

Changed in version 1.0: The addscript command used to be only script which was easy to confuse with scripts.

Changed in version 1.0: Game folders created with older versions of Evennia will lack most of this
convenient mygame/web/ layout. If you use a game dir from an older version,
you should copy over the missing evennia/game_template/web/ folders from
there, as well as the main urls.py file.

Changed in version 1.0: How the feature changed with this version.

Changed in version 0.7: Channels changed from using Msg to TmpMsg and optional log files.

default_character_typeclass (evennia.accounts.accounts.DefaultAccount attribute)

at_post_add_character() (evennia.accounts.accounts.DefaultAccount method)

at_post_remove_character() (evennia.accounts.accounts.DefaultAccount method)

uses_screenreader() (evennia.accounts.accounts.DefaultAccount method)

get_display_name() (evennia.accounts.accounts.DefaultAccount method)

puppet_object() (evennia.accounts.accounts.DefaultAccount method)

unpuppet_object() (evennia.accounts.accounts.DefaultAccount method)

unpuppet_all() (evennia.accounts.accounts.DefaultAccount method)

get_all_puppets() (evennia.accounts.accounts.DefaultAccount method)

is_banned() (evennia.accounts.accounts.DefaultAccount class method)

get_username_validators() (evennia.accounts.accounts.DefaultAccount class method)

authenticate() (evennia.accounts.accounts.DefaultAccount class method)

normalize_username() (evennia.accounts.accounts.DefaultAccount class method)

validate_username() (evennia.accounts.accounts.DefaultAccount class method)

validate_password() (evennia.accounts.accounts.DefaultAccount class method)

set_password() (evennia.accounts.accounts.DefaultAccount method)

get_character_slots() (evennia.accounts.accounts.DefaultAccount method)

check_available_slots() (evennia.accounts.accounts.DefaultAccount method)

create_character() (evennia.accounts.accounts.DefaultAccount method)

at_post_create_character() (evennia.accounts.accounts.DefaultAccount method)

create() (evennia.accounts.accounts.DefaultAccount class method)

at_pre_channel_msg() (evennia.accounts.accounts.DefaultAccount method)

at_post_channel_msg() (evennia.accounts.accounts.DefaultAccount method)

connection_time() (evennia.accounts.accounts.DefaultAccount property)

basetype_setup() (evennia.accounts.accounts.DefaultAccount method)

at_account_creation() (evennia.accounts.accounts.DefaultAccount method)

at_first_save() (evennia.accounts.accounts.DefaultAccount method)

at_cmdset_get() (evennia.accounts.accounts.DefaultAccount method)

at_first_login() (evennia.accounts.accounts.DefaultAccount method)

at_password_change() (evennia.accounts.accounts.DefaultAccount method)

at_pre_login() (evennia.accounts.accounts.DefaultAccount method)

at_post_login() (evennia.accounts.accounts.DefaultAccount method)

at_failed_login() (evennia.accounts.accounts.DefaultAccount method)

at_disconnect() (evennia.accounts.accounts.DefaultAccount method)

at_post_disconnect() (evennia.accounts.accounts.DefaultAccount method)

at_msg_receive() (evennia.accounts.accounts.DefaultAccount method)

at_server_reload() (evennia.accounts.accounts.DefaultAccount method)

at_server_shutdown() (evennia.accounts.accounts.DefaultAccount method)

ooc_appearance_template (evennia.accounts.accounts.DefaultAccount attribute)

authenticate() (evennia.accounts.accounts.DefaultGuest class method)

at_server_shutdown() (evennia.accounts.accounts.DefaultGuest method)

at_post_disconnect() (evennia.accounts.accounts.DefaultGuest method)

num_total_accounts() (evennia.accounts.manager.AccountDBManager method)

get_connected_accounts() (evennia.accounts.manager.AccountDBManager method)

get_account_from_email() (evennia.accounts.manager.AccountDBManager method)

get_account_from_uid() (evennia.accounts.manager.AccountDBManager method)

get_account_from_name() (evennia.accounts.manager.AccountDBManager method)

search_account() (evennia.accounts.manager.AccountDBManager method)

create_account() (evennia.accounts.manager.AccountDBManager method)

account_search() (evennia.accounts.manager.AccountDBManager method)

account_subscription_set (evennia.accounts.models.AccountDB attribute)

get_next_by_date_joined() (evennia.accounts.models.AccountDB method)

get_next_by_db_date_created() (evennia.accounts.models.AccountDB method)

get_previous_by_date_joined() (evennia.accounts.models.AccountDB method)

get_previous_by_db_date_created() (evennia.accounts.models.AccountDB method)

hide_from_accounts_set (evennia.accounts.models.AccountDB attribute)

receiver_account_set (evennia.accounts.models.AccountDB attribute)

sender_account_set (evennia.accounts.models.AccountDB attribute)

try_num_differentiators() (in module evennia.commands.cmdparser)

get_all_cmd_keys_and_aliases() (evennia.commands.cmdset.CmdSet method)

__init__() (evennia.commands.cmdsethandler.CmdSetHandler method)

add_default() (evennia.commands.cmdsethandler.CmdSetHandler method)

remove_default() (evennia.commands.cmdsethandler.CmdSetHandler method)

delete_default() (evennia.commands.cmdsethandler.CmdSetHandler method)

has_cmdset() (evennia.commands.cmdsethandler.CmdSetHandler method)

help_category (evennia.commands.default.account.CmdOOCLook attribute)

account_caller (evennia.commands.default.account.CmdOOCLook attribute)

lock_storage (evennia.commands.default.account.CmdOOCLook attribute)

search_index_entry (evennia.commands.default.account.CmdOOCLook attribute)

help_category (evennia.commands.default.account.CmdIC attribute)

account_caller (evennia.commands.default.account.CmdIC attribute)

search_index_entry (evennia.commands.default.account.CmdIC attribute)

help_category (evennia.commands.default.account.CmdOOC attribute)

account_caller (evennia.commands.default.account.CmdOOC attribute)

lock_storage (evennia.commands.default.account.CmdOOC attribute)

search_index_entry (evennia.commands.default.account.CmdOOC attribute)

account_caller (evennia.commands.default.account.CmdPassword attribute)

aliases (evennia.commands.default.account.CmdPassword attribute)

help_category (evennia.commands.default.account.CmdPassword attribute)

lock_storage (evennia.commands.default.account.CmdPassword attribute)

search_index_entry (evennia.commands.default.account.CmdPassword attribute)

switch_options (evennia.commands.default.account.CmdQuit attribute)

account_caller (evennia.commands.default.account.CmdQuit attribute)

help_category (evennia.commands.default.account.CmdQuit attribute)

lock_storage (evennia.commands.default.account.CmdQuit attribute)

search_index_entry (evennia.commands.default.account.CmdQuit attribute)

locks (evennia.commands.default.account.CmdCharCreate attribute)

help_category (evennia.commands.default.account.CmdCharCreate attribute)

account_caller (evennia.commands.default.account.CmdCharCreate attribute)

aliases (evennia.commands.default.account.CmdCharCreate attribute)

lock_storage (evennia.commands.default.account.CmdCharCreate attribute)

search_index_entry (evennia.commands.default.account.CmdCharCreate attribute)

switch_options (evennia.commands.default.account.CmdOption attribute)

account_caller (evennia.commands.default.account.CmdOption attribute)

help_category (evennia.commands.default.account.CmdOption attribute)

lock_storage (evennia.commands.default.account.CmdOption attribute)

search_index_entry (evennia.commands.default.account.CmdOption attribute)

help_category (evennia.commands.default.account.CmdSessions attribute)

account_caller (evennia.commands.default.account.CmdSessions attribute)

aliases (evennia.commands.default.account.CmdSessions attribute)

lock_storage (evennia.commands.default.account.CmdSessions attribute)

search_index_entry (evennia.commands.default.account.CmdSessions attribute)

account_caller (evennia.commands.default.account.CmdWho attribute)

help_category (evennia.commands.default.account.CmdWho attribute)

lock_storage (evennia.commands.default.account.CmdWho attribute)

search_index_entry (evennia.commands.default.account.CmdWho attribute)

help_category (evennia.commands.default.account.CmdColorTest attribute)

account_caller (evennia.commands.default.account.CmdColorTest attribute)

slice_bright_fg (evennia.commands.default.account.CmdColorTest attribute)

slice_dark_fg (evennia.commands.default.account.CmdColorTest attribute)

slice_dark_bg (evennia.commands.default.account.CmdColorTest attribute)

slice_bright_bg (evennia.commands.default.account.CmdColorTest attribute)

table_format() (evennia.commands.default.account.CmdColorTest method)

aliases (evennia.commands.default.account.CmdColorTest attribute)

lock_storage (evennia.commands.default.account.CmdColorTest attribute)

search_index_entry (evennia.commands.default.account.CmdColorTest attribute)

help_category (evennia.commands.default.account.CmdQuell attribute)

account_caller (evennia.commands.default.account.CmdQuell attribute)

lock_storage (evennia.commands.default.account.CmdQuell attribute)

search_index_entry (evennia.commands.default.account.CmdQuell attribute)

locks (evennia.commands.default.account.CmdCharDelete attribute)

help_category (evennia.commands.default.account.CmdCharDelete attribute)

aliases (evennia.commands.default.account.CmdCharDelete attribute)

lock_storage (evennia.commands.default.account.CmdCharDelete attribute)

search_index_entry (evennia.commands.default.account.CmdCharDelete attribute)

switch_options (evennia.commands.default.account.CmdStyle attribute)

list_styles() (evennia.commands.default.account.CmdStyle method)

help_category (evennia.commands.default.account.CmdStyle attribute)

lock_storage (evennia.commands.default.account.CmdStyle attribute)

search_index_entry (evennia.commands.default.account.CmdStyle attribute)

switch_options (evennia.commands.default.admin.CmdBoot attribute)

help_category (evennia.commands.default.admin.CmdBoot attribute)

search_index_entry (evennia.commands.default.admin.CmdBoot attribute)

search_index_entry (evennia.commands.default.admin.CmdBan attribute)

help_category (evennia.commands.default.admin.CmdUnban attribute)

lock_storage (evennia.commands.default.admin.CmdUnban attribute)

search_index_entry (evennia.commands.default.admin.CmdUnban attribute)

switch_options (evennia.commands.default.admin.CmdEmit attribute)

help_category (evennia.commands.default.admin.CmdEmit attribute)

search_index_entry (evennia.commands.default.admin.CmdEmit attribute)

help_category (evennia.commands.default.admin.CmdNewPassword attribute)

aliases (evennia.commands.default.admin.CmdNewPassword attribute)

lock_storage (evennia.commands.default.admin.CmdNewPassword attribute)

search_index_entry (evennia.commands.default.admin.CmdNewPassword attribute)

switch_options (evennia.commands.default.admin.CmdPerm attribute)

help_category (evennia.commands.default.admin.CmdPerm attribute)

search_index_entry (evennia.commands.default.admin.CmdPerm attribute)

help_category (evennia.commands.default.admin.CmdWall attribute)

search_index_entry (evennia.commands.default.admin.CmdWall attribute)

help_category (evennia.commands.default.admin.CmdForce attribute)

lock_storage (evennia.commands.default.admin.CmdForce attribute)

search_index_entry (evennia.commands.default.admin.CmdForce attribute)

CmdBatchCommands (class in evennia.commands.default.batchprocess)

key (evennia.commands.default.batchprocess.CmdBatchCommands attribute)

aliases (evennia.commands.default.batchprocess.CmdBatchCommands attribute)

locks (evennia.commands.default.batchprocess.CmdBatchCommands attribute)

help_category (evennia.commands.default.batchprocess.CmdBatchCommands attribute)

func() (evennia.commands.default.batchprocess.CmdBatchCommands method)

lock_storage (evennia.commands.default.batchprocess.CmdBatchCommands attribute)

key (evennia.commands.default.batchprocess.CmdBatchCode attribute)

aliases (evennia.commands.default.batchprocess.CmdBatchCode attribute)

switch_options (evennia.commands.default.batchprocess.CmdBatchCode attribute)

locks (evennia.commands.default.batchprocess.CmdBatchCode attribute)

help_category (evennia.commands.default.batchprocess.CmdBatchCode attribute)

func() (evennia.commands.default.batchprocess.CmdBatchCode method)

lock_storage (evennia.commands.default.batchprocess.CmdBatchCode attribute)

parse() (evennia.commands.default.building.ObjManipCommand method)

aliases (evennia.commands.default.building.ObjManipCommand attribute)

help_category (evennia.commands.default.building.ObjManipCommand attribute)

key (evennia.commands.default.building.ObjManipCommand attribute)

lock_storage (evennia.commands.default.building.ObjManipCommand attribute)

search_index_entry (evennia.commands.default.building.ObjManipCommand attribute)

key (evennia.commands.default.building.CmdSetObjAlias attribute)

aliases (evennia.commands.default.building.CmdSetObjAlias attribute)

switch_options (evennia.commands.default.building.CmdSetObjAlias attribute)

locks (evennia.commands.default.building.CmdSetObjAlias attribute)

help_category (evennia.commands.default.building.CmdSetObjAlias attribute)

method_type (evennia.commands.default.building.CmdSetObjAlias attribute)

func() (evennia.commands.default.building.CmdSetObjAlias method)

lock_storage (evennia.commands.default.building.CmdSetObjAlias attribute)

search_index_entry (evennia.commands.default.building.CmdSetObjAlias attribute)

help_category (evennia.commands.default.building.CmdCopy attribute)

lock_storage (evennia.commands.default.building.CmdCopy attribute)

search_index_entry (evennia.commands.default.building.CmdCopy attribute)

switch_options (evennia.commands.default.building.CmdCpAttr attribute)

help_category (evennia.commands.default.building.CmdCpAttr attribute)

check_from_attr() (evennia.commands.default.building.CmdCpAttr method)

check_to_attr() (evennia.commands.default.building.CmdCpAttr method)

check_has_attr() (evennia.commands.default.building.CmdCpAttr method)

lock_storage (evennia.commands.default.building.CmdCpAttr attribute)

search_index_entry (evennia.commands.default.building.CmdCpAttr attribute)

switch_options (evennia.commands.default.building.CmdMvAttr attribute)

help_category (evennia.commands.default.building.CmdMvAttr attribute)

lock_storage (evennia.commands.default.building.CmdMvAttr attribute)

search_index_entry (evennia.commands.default.building.CmdMvAttr attribute)

switch_options (evennia.commands.default.building.CmdCreate attribute)

help_category (evennia.commands.default.building.CmdCreate attribute)

lock_storage (evennia.commands.default.building.CmdCreate attribute)

search_index_entry (evennia.commands.default.building.CmdCreate attribute)

switch_options (evennia.commands.default.building.CmdDesc attribute)

help_category (evennia.commands.default.building.CmdDesc attribute)

edit_handler() (evennia.commands.default.building.CmdDesc method)

lock_storage (evennia.commands.default.building.CmdDesc attribute)

search_index_entry (evennia.commands.default.building.CmdDesc attribute)

aliases (evennia.commands.default.building.CmdDestroy attribute)

switch_options (evennia.commands.default.building.CmdDestroy attribute)

help_category (evennia.commands.default.building.CmdDestroy attribute)

confirm (evennia.commands.default.building.CmdDestroy attribute)

default_confirm (evennia.commands.default.building.CmdDestroy attribute)

lock_storage (evennia.commands.default.building.CmdDestroy attribute)

search_index_entry (evennia.commands.default.building.CmdDestroy attribute)

switch_options (evennia.commands.default.building.CmdDig attribute)

help_category (evennia.commands.default.building.CmdDig attribute)

method_type (evennia.commands.default.building.CmdDig attribute)

new_room_lockstring (evennia.commands.default.building.CmdDig attribute)

lock_storage (evennia.commands.default.building.CmdDig attribute)

search_index_entry (evennia.commands.default.building.CmdDig attribute)

switch_options (evennia.commands.default.building.CmdTunnel attribute)

help_category (evennia.commands.default.building.CmdTunnel attribute)

method_type (evennia.commands.default.building.CmdTunnel attribute)

directions (evennia.commands.default.building.CmdTunnel attribute)

lock_storage (evennia.commands.default.building.CmdTunnel attribute)

search_index_entry (evennia.commands.default.building.CmdTunnel attribute)

help_category (evennia.commands.default.building.CmdLink attribute)

lock_storage (evennia.commands.default.building.CmdLink attribute)

search_index_entry (evennia.commands.default.building.CmdLink attribute)

help_key (evennia.commands.default.building.CmdUnLink attribute)

help_category (evennia.commands.default.building.CmdUnLink attribute)

lock_storage (evennia.commands.default.building.CmdUnLink attribute)

search_index_entry (evennia.commands.default.building.CmdUnLink attribute)

help_category (evennia.commands.default.building.CmdSetHome attribute)

aliases (evennia.commands.default.building.CmdSetHome attribute)

lock_storage (evennia.commands.default.building.CmdSetHome attribute)

search_index_entry (evennia.commands.default.building.CmdSetHome attribute)

key (evennia.commands.default.building.CmdListCmdSets attribute)

locks (evennia.commands.default.building.CmdListCmdSets attribute)

help_category (evennia.commands.default.building.CmdListCmdSets attribute)

func() (evennia.commands.default.building.CmdListCmdSets method)

aliases (evennia.commands.default.building.CmdListCmdSets attribute)

lock_storage (evennia.commands.default.building.CmdListCmdSets attribute)

search_index_entry (evennia.commands.default.building.CmdListCmdSets attribute)

help_category (evennia.commands.default.building.CmdName attribute)

lock_storage (evennia.commands.default.building.CmdName attribute)

search_index_entry (evennia.commands.default.building.CmdName attribute)

help_category (evennia.commands.default.building.CmdOpen attribute)

method_type (evennia.commands.default.building.CmdOpen attribute)

new_obj_lockstring (evennia.commands.default.building.CmdOpen attribute)

create_exit() (evennia.commands.default.building.CmdOpen method)

lock_storage (evennia.commands.default.building.CmdOpen attribute)

search_index_entry (evennia.commands.default.building.CmdOpen attribute)

key (evennia.commands.default.building.CmdSetAttribute attribute)

locks (evennia.commands.default.building.CmdSetAttribute attribute)

help_category (evennia.commands.default.building.CmdSetAttribute attribute)

nested_re (evennia.commands.default.building.CmdSetAttribute attribute)

not_found (evennia.commands.default.building.CmdSetAttribute attribute)

check_obj() (evennia.commands.default.building.CmdSetAttribute method)

check_attr() (evennia.commands.default.building.CmdSetAttribute method)

split_nested_attr() (evennia.commands.default.building.CmdSetAttribute method)

do_nested_lookup() (evennia.commands.default.building.CmdSetAttribute method)

view_attr() (evennia.commands.default.building.CmdSetAttribute method)

rm_attr() (evennia.commands.default.building.CmdSetAttribute method)

set_attr() (evennia.commands.default.building.CmdSetAttribute method)

edit_handler() (evennia.commands.default.building.CmdSetAttribute method)

search_for_obj() (evennia.commands.default.building.CmdSetAttribute method)

func() (evennia.commands.default.building.CmdSetAttribute method)

aliases (evennia.commands.default.building.CmdSetAttribute attribute)

lock_storage (evennia.commands.default.building.CmdSetAttribute attribute)

search_index_entry (evennia.commands.default.building.CmdSetAttribute attribute)

aliases (evennia.commands.default.building.CmdTypeclass attribute)

switch_options (evennia.commands.default.building.CmdTypeclass attribute)

locks (evennia.commands.default.building.CmdTypeclass attribute)

help_category (evennia.commands.default.building.CmdTypeclass attribute)

lock_storage (evennia.commands.default.building.CmdTypeclass attribute)

search_index_entry (evennia.commands.default.building.CmdTypeclass attribute)

help_category (evennia.commands.default.building.CmdWipe attribute)

lock_storage (evennia.commands.default.building.CmdWipe attribute)

search_index_entry (evennia.commands.default.building.CmdWipe attribute)

help_category (evennia.commands.default.building.CmdLock attribute)

lock_storage (evennia.commands.default.building.CmdLock attribute)

search_index_entry (evennia.commands.default.building.CmdLock attribute)

aliases (evennia.commands.default.building.CmdExamine attribute)

help_category (evennia.commands.default.building.CmdExamine attribute)

arg_regex (evennia.commands.default.building.CmdExamine attribute)

switch_options (evennia.commands.default.building.CmdExamine attribute)

object_type (evennia.commands.default.building.CmdExamine attribute)

detail_color (evennia.commands.default.building.CmdExamine attribute)

header_color (evennia.commands.default.building.CmdExamine attribute)

quell_color (evennia.commands.default.building.CmdExamine attribute)

separator (evennia.commands.default.building.CmdExamine attribute)

format_key() (evennia.commands.default.building.CmdExamine method)

format_aliases() (evennia.commands.default.building.CmdExamine method)

format_typeclass() (evennia.commands.default.building.CmdExamine method)

format_sessions() (evennia.commands.default.building.CmdExamine method)

format_email() (evennia.commands.default.building.CmdExamine method)

format_last_login() (evennia.commands.default.building.CmdExamine method)

format_account_key() (evennia.commands.default.building.CmdExamine method)

format_account_typeclass() (evennia.commands.default.building.CmdExamine method)

format_location() (evennia.commands.default.building.CmdExamine method)

format_home() (evennia.commands.default.building.CmdExamine method)

format_destination() (evennia.commands.default.building.CmdExamine method)

format_permissions() (evennia.commands.default.building.CmdExamine method)

format_locks() (evennia.commands.default.building.CmdExamine method)

format_scripts() (evennia.commands.default.building.CmdExamine method)

format_single_tag() (evennia.commands.default.building.CmdExamine method)

format_tags() (evennia.commands.default.building.CmdExamine method)

format_single_cmdset() (evennia.commands.default.building.CmdExamine method)

format_stored_cmdsets() (evennia.commands.default.building.CmdExamine method)

format_merged_cmdsets() (evennia.commands.default.building.CmdExamine method)

format_current_cmds() (evennia.commands.default.building.CmdExamine method)

format_single_attribute() (evennia.commands.default.building.CmdExamine method)

format_attributes() (evennia.commands.default.building.CmdExamine method)

format_nattributes() (evennia.commands.default.building.CmdExamine method)

format_exits() (evennia.commands.default.building.CmdExamine method)

format_chars() (evennia.commands.default.building.CmdExamine method)

format_things() (evennia.commands.default.building.CmdExamine method)

format_script_desc() (evennia.commands.default.building.CmdExamine method)

format_script_timer_data() (evennia.commands.default.building.CmdExamine method)

get_formatted_obj_data() (evennia.commands.default.building.CmdExamine method)

format_output() (evennia.commands.default.building.CmdExamine method)

lock_storage (evennia.commands.default.building.CmdExamine attribute)

search_index_entry (evennia.commands.default.building.CmdExamine attribute)

switch_options (evennia.commands.default.building.CmdFind attribute)

help_category (evennia.commands.default.building.CmdFind attribute)

lock_storage (evennia.commands.default.building.CmdFind attribute)

search_index_entry (evennia.commands.default.building.CmdFind attribute)

aliases (evennia.commands.default.building.CmdTeleport attribute)

switch_options (evennia.commands.default.building.CmdTeleport attribute)

rhs_split (evennia.commands.default.building.CmdTeleport attribute)

help_category (evennia.commands.default.building.CmdTeleport attribute)

lock_storage (evennia.commands.default.building.CmdTeleport attribute)

search_index_entry (evennia.commands.default.building.CmdTeleport attribute)

aliases (evennia.commands.default.building.CmdScripts attribute)

switch_options (evennia.commands.default.building.CmdScripts attribute)

help_category (evennia.commands.default.building.CmdScripts attribute)

switch_mapping (evennia.commands.default.building.CmdScripts attribute)

hide_script_paths (evennia.commands.default.building.CmdScripts attribute)

lock_storage (evennia.commands.default.building.CmdScripts attribute)

search_index_entry (evennia.commands.default.building.CmdScripts attribute)

help_category (evennia.commands.default.building.CmdObjects attribute)

aliases (evennia.commands.default.building.CmdObjects attribute)

lock_storage (evennia.commands.default.building.CmdObjects attribute)

search_index_entry (evennia.commands.default.building.CmdObjects attribute)

help_category (evennia.commands.default.building.CmdTag attribute)

lock_storage (evennia.commands.default.building.CmdTag attribute)

search_index_entry (evennia.commands.default.building.CmdTag attribute)

switch_options (evennia.commands.default.building.CmdSpawn attribute)

help_category (evennia.commands.default.building.CmdSpawn attribute)

lock_storage (evennia.commands.default.building.CmdSpawn attribute)

search_index_entry (evennia.commands.default.building.CmdSpawn attribute)

AccountCmdSet (class in evennia.commands.default.cmdset_account)

key (evennia.commands.default.cmdset_account.AccountCmdSet attribute)

priority (evennia.commands.default.cmdset_account.AccountCmdSet attribute)

path (evennia.commands.default.cmdset_account.AccountCmdSet attribute)

CharacterCmdSet (class in evennia.commands.default.cmdset_character)

key (evennia.commands.default.cmdset_character.CharacterCmdSet attribute)

priority (evennia.commands.default.cmdset_character.CharacterCmdSet attribute)

path (evennia.commands.default.cmdset_character.CharacterCmdSet attribute)

SessionCmdSet (class in evennia.commands.default.cmdset_session)

key (evennia.commands.default.cmdset_session.SessionCmdSet attribute)

priority (evennia.commands.default.cmdset_session.SessionCmdSet attribute)

path (evennia.commands.default.cmdset_session.SessionCmdSet attribute)

UnloggedinCmdSet (class in evennia.commands.default.cmdset_unloggedin)

key (evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet attribute)

priority (evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet attribute)

path (evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet attribute)

help_category (evennia.commands.default.comms.CmdChannel attribute)

switch_options (evennia.commands.default.comms.CmdChannel attribute)

account_caller (evennia.commands.default.comms.CmdChannel attribute)

search_channel() (evennia.commands.default.comms.CmdChannel method)

msg_channel() (evennia.commands.default.comms.CmdChannel method)

get_channel_history() (evennia.commands.default.comms.CmdChannel method)

sub_to_channel() (evennia.commands.default.comms.CmdChannel method)

unsub_from_channel() (evennia.commands.default.comms.CmdChannel method)

remove_alias() (evennia.commands.default.comms.CmdChannel method)

get_channel_aliases() (evennia.commands.default.comms.CmdChannel method)

mute_channel() (evennia.commands.default.comms.CmdChannel method)

unmute_channel() (evennia.commands.default.comms.CmdChannel method)

create_channel() (evennia.commands.default.comms.CmdChannel method)

destroy_channel() (evennia.commands.default.comms.CmdChannel method)

channel_list_bans() (evennia.commands.default.comms.CmdChannel method)

channel_list_who() (evennia.commands.default.comms.CmdChannel method)

list_channels() (evennia.commands.default.comms.CmdChannel method)

display_subbed_channels() (evennia.commands.default.comms.CmdChannel method)

display_all_channels() (evennia.commands.default.comms.CmdChannel method)

lock_storage (evennia.commands.default.comms.CmdChannel attribute)

search_index_entry (evennia.commands.default.comms.CmdChannel attribute)

account_caller (evennia.commands.default.comms.CmdObjectChannel attribute)

aliases (evennia.commands.default.comms.CmdObjectChannel attribute)

help_category (evennia.commands.default.comms.CmdObjectChannel attribute)

lock_storage (evennia.commands.default.comms.CmdObjectChannel attribute)

search_index_entry (evennia.commands.default.comms.CmdObjectChannel attribute)

switch_options (evennia.commands.default.comms.CmdPage attribute)

help_category (evennia.commands.default.comms.CmdPage attribute)

account_caller (evennia.commands.default.comms.CmdPage attribute)

search_index_entry (evennia.commands.default.comms.CmdPage attribute)

switch_options (evennia.commands.default.comms.CmdIRC2Chan attribute)

help_category (evennia.commands.default.comms.CmdIRC2Chan attribute)

lock_storage (evennia.commands.default.comms.CmdIRC2Chan attribute)

search_index_entry (evennia.commands.default.comms.CmdIRC2Chan attribute)

help_category (evennia.commands.default.comms.CmdIRCStatus attribute)

lock_storage (evennia.commands.default.comms.CmdIRCStatus attribute)

search_index_entry (evennia.commands.default.comms.CmdIRCStatus attribute)

switch_options (evennia.commands.default.comms.CmdRSS2Chan attribute)

help_category (evennia.commands.default.comms.CmdRSS2Chan attribute)

lock_storage (evennia.commands.default.comms.CmdRSS2Chan attribute)

search_index_entry (evennia.commands.default.comms.CmdRSS2Chan attribute)

key (evennia.commands.default.comms.CmdGrapevine2Chan attribute)

switch_options (evennia.commands.default.comms.CmdGrapevine2Chan attribute)

locks (evennia.commands.default.comms.CmdGrapevine2Chan attribute)

help_category (evennia.commands.default.comms.CmdGrapevine2Chan attribute)

func() (evennia.commands.default.comms.CmdGrapevine2Chan method)

aliases (evennia.commands.default.comms.CmdGrapevine2Chan attribute)

lock_storage (evennia.commands.default.comms.CmdGrapevine2Chan attribute)

search_index_entry (evennia.commands.default.comms.CmdGrapevine2Chan attribute)

aliases (evennia.commands.default.comms.CmdDiscord2Chan attribute)

switch_options (evennia.commands.default.comms.CmdDiscord2Chan attribute)

locks (evennia.commands.default.comms.CmdDiscord2Chan attribute)

help_category (evennia.commands.default.comms.CmdDiscord2Chan attribute)

lock_storage (evennia.commands.default.comms.CmdDiscord2Chan attribute)

search_index_entry (evennia.commands.default.comms.CmdDiscord2Chan attribute)

help_category (evennia.commands.default.general.CmdHome attribute)

lock_storage (evennia.commands.default.general.CmdHome attribute)

search_index_entry (evennia.commands.default.general.CmdHome attribute)

help_category (evennia.commands.default.general.CmdLook attribute)

lock_storage (evennia.commands.default.general.CmdLook attribute)

search_index_entry (evennia.commands.default.general.CmdLook attribute)

switch_options (evennia.commands.default.general.CmdNick attribute)

help_category (evennia.commands.default.general.CmdNick attribute)

lock_storage (evennia.commands.default.general.CmdNick attribute)

search_index_entry (evennia.commands.default.general.CmdNick attribute)

aliases (evennia.commands.default.general.CmdInventory attribute)

arg_regex (evennia.commands.default.general.CmdInventory attribute)

help_category (evennia.commands.default.general.CmdInventory attribute)

lock_storage (evennia.commands.default.general.CmdInventory attribute)

search_index_entry (evennia.commands.default.general.CmdInventory attribute)

arg_regex (evennia.commands.default.general.CmdSetDesc attribute)

help_category (evennia.commands.default.general.CmdSetDesc attribute)

lock_storage (evennia.commands.default.general.CmdSetDesc attribute)

search_index_entry (evennia.commands.default.general.CmdSetDesc attribute)

help_category (evennia.commands.default.general.CmdGet attribute)

lock_storage (evennia.commands.default.general.CmdGet attribute)

search_index_entry (evennia.commands.default.general.CmdGet attribute)

help_category (evennia.commands.default.general.CmdDrop attribute)

lock_storage (evennia.commands.default.general.CmdDrop attribute)

search_index_entry (evennia.commands.default.general.CmdDrop attribute)

help_category (evennia.commands.default.general.CmdGive attribute)

lock_storage (evennia.commands.default.general.CmdGive attribute)

search_index_entry (evennia.commands.default.general.CmdGive attribute)

help_category (evennia.commands.default.general.CmdSay attribute)

lock_storage (evennia.commands.default.general.CmdSay attribute)

search_index_entry (evennia.commands.default.general.CmdSay attribute)

help_category (evennia.commands.default.general.CmdWhisper attribute)

lock_storage (evennia.commands.default.general.CmdWhisper attribute)

search_index_entry (evennia.commands.default.general.CmdWhisper attribute)

help_category (evennia.commands.default.general.CmdPose attribute)

lock_storage (evennia.commands.default.general.CmdPose attribute)

search_index_entry (evennia.commands.default.general.CmdPose attribute)

arg_regex (evennia.commands.default.general.CmdAccess attribute)

help_category (evennia.commands.default.general.CmdAccess attribute)

lock_storage (evennia.commands.default.general.CmdAccess attribute)

search_index_entry (evennia.commands.default.general.CmdAccess attribute)

index_type_separator_clr (evennia.commands.default.help.CmdHelp attribute)

index_category_clr (evennia.commands.default.help.CmdHelp attribute)

index_topic_clr (evennia.commands.default.help.CmdHelp attribute)

suggestion_cutoff (evennia.commands.default.help.CmdHelp attribute)

suggestion_maxnum (evennia.commands.default.help.CmdHelp attribute)

subtopic_separator_char (evennia.commands.default.help.CmdHelp attribute)

clickable_topics (evennia.commands.default.help.CmdHelp attribute)

format_help_entry() (evennia.commands.default.help.CmdHelp method)

format_help_index() (evennia.commands.default.help.CmdHelp method)

strip_cmd_prefix() (evennia.commands.default.help.CmdHelp method)

search_index_entry (evennia.commands.default.help.CmdHelp attribute)

switch_options (evennia.commands.default.help.CmdSetHelp attribute)

help_category (evennia.commands.default.help.CmdSetHelp attribute)

lock_storage (evennia.commands.default.help.CmdSetHelp attribute)

search_index_entry (evennia.commands.default.help.CmdSetHelp attribute)

has_perm() (evennia.commands.default.muxcommand.MuxCommand method)

at_pre_cmd() (evennia.commands.default.muxcommand.MuxCommand method)

at_post_cmd() (evennia.commands.default.muxcommand.MuxCommand method)

get_command_info() (evennia.commands.default.muxcommand.MuxCommand method)

aliases (evennia.commands.default.muxcommand.MuxCommand attribute)

help_category (evennia.commands.default.muxcommand.MuxCommand attribute)

lock_storage (evennia.commands.default.muxcommand.MuxCommand attribute)

search_index_entry (evennia.commands.default.muxcommand.MuxCommand attribute)

MuxAccountCommand (class in evennia.commands.default.muxcommand)

account_caller (evennia.commands.default.muxcommand.MuxAccountCommand attribute)

aliases (evennia.commands.default.muxcommand.MuxAccountCommand attribute)

help_category (evennia.commands.default.muxcommand.MuxAccountCommand attribute)

key (evennia.commands.default.muxcommand.MuxAccountCommand attribute)

lock_storage (evennia.commands.default.muxcommand.MuxAccountCommand attribute)

key (evennia.commands.default.syscommands.SystemNoInput attribute)

locks (evennia.commands.default.syscommands.SystemNoInput attribute)

func() (evennia.commands.default.syscommands.SystemNoInput method)

aliases (evennia.commands.default.syscommands.SystemNoInput attribute)

help_category (evennia.commands.default.syscommands.SystemNoInput attribute)

lock_storage (evennia.commands.default.syscommands.SystemNoInput attribute)

key (evennia.commands.default.syscommands.SystemNoMatch attribute)

locks (evennia.commands.default.syscommands.SystemNoMatch attribute)

func() (evennia.commands.default.syscommands.SystemNoMatch method)

aliases (evennia.commands.default.syscommands.SystemNoMatch attribute)

help_category (evennia.commands.default.syscommands.SystemNoMatch attribute)

lock_storage (evennia.commands.default.syscommands.SystemNoMatch attribute)

SystemMultimatch (class in evennia.commands.default.syscommands)

key (evennia.commands.default.syscommands.SystemMultimatch attribute)

locks (evennia.commands.default.syscommands.SystemMultimatch attribute)

func() (evennia.commands.default.syscommands.SystemMultimatch method)

aliases (evennia.commands.default.syscommands.SystemMultimatch attribute)

help_category (evennia.commands.default.syscommands.SystemMultimatch attribute)

lock_storage (evennia.commands.default.syscommands.SystemMultimatch attribute)

switch_options (evennia.commands.default.system.CmdAccounts attribute)

help_category (evennia.commands.default.system.CmdAccounts attribute)

lock_storage (evennia.commands.default.system.CmdAccounts attribute)

search_index_entry (evennia.commands.default.system.CmdAccounts attribute)

help_category (evennia.commands.default.system.CmdReload attribute)

lock_storage (evennia.commands.default.system.CmdReload attribute)

search_index_entry (evennia.commands.default.system.CmdReload attribute)

help_category (evennia.commands.default.system.CmdReset attribute)

lock_storage (evennia.commands.default.system.CmdReset attribute)

search_index_entry (evennia.commands.default.system.CmdReset attribute)

help_category (evennia.commands.default.system.CmdShutdown attribute)

lock_storage (evennia.commands.default.system.CmdShutdown attribute)

search_index_entry (evennia.commands.default.system.CmdShutdown attribute)

switch_options (evennia.commands.default.system.CmdPy attribute)

search_index_entry (evennia.commands.default.system.CmdPy attribute)

switch_options (evennia.commands.default.system.CmdService attribute)

help_category (evennia.commands.default.system.CmdService attribute)

lock_storage (evennia.commands.default.system.CmdService attribute)

search_index_entry (evennia.commands.default.system.CmdService attribute)

help_category (evennia.commands.default.system.CmdAbout attribute)

lock_storage (evennia.commands.default.system.CmdAbout attribute)

search_index_entry (evennia.commands.default.system.CmdAbout attribute)

help_category (evennia.commands.default.system.CmdTime attribute)

lock_storage (evennia.commands.default.system.CmdTime attribute)

search_index_entry (evennia.commands.default.system.CmdTime attribute)

aliases (evennia.commands.default.system.CmdServerLoad attribute)

switch_options (evennia.commands.default.system.CmdServerLoad attribute)

help_category (evennia.commands.default.system.CmdServerLoad attribute)

lock_storage (evennia.commands.default.system.CmdServerLoad attribute)

search_index_entry (evennia.commands.default.system.CmdServerLoad attribute)

switch_options (evennia.commands.default.system.CmdTasks attribute)

help_category (evennia.commands.default.system.CmdTasks attribute)

coll_date_func() (evennia.commands.default.system.CmdTasks static method)

do_task_action() (evennia.commands.default.system.CmdTasks method)

lock_storage (evennia.commands.default.system.CmdTasks attribute)

search_index_entry (evennia.commands.default.system.CmdTasks attribute)

help_category (evennia.commands.default.system.CmdTickers attribute)

lock_storage (evennia.commands.default.system.CmdTickers attribute)

search_index_entry (evennia.commands.default.system.CmdTickers attribute)

test_look_no_location() (evennia.commands.default.tests.TestGeneral method)

test_look_nonexisting() (evennia.commands.default.tests.TestGeneral method)

test_go_home() (evennia.commands.default.tests.TestGeneral method)

test_no_home() (evennia.commands.default.tests.TestGeneral method)

test_inventory() (evennia.commands.default.tests.TestGeneral method)

test_nick_list() (evennia.commands.default.tests.TestGeneral method)

test_get_and_drop() (evennia.commands.default.tests.TestGeneral method)

test_mux_command() (evennia.commands.default.tests.TestGeneral method)

test_whisper() (evennia.commands.default.tests.TestGeneral method)

test_access() (evennia.commands.default.tests.TestGeneral method)

test_set_help() (evennia.commands.default.tests.TestHelp method)

test_subtopic_fetch (evennia.commands.default.tests.TestHelp attribute)

test_subtopic_fetch_00_test() (evennia.commands.default.tests.TestHelp method)

test_scripts() (evennia.commands.default.tests.TestSystem method)

test_objects() (evennia.commands.default.tests.TestSystem method)

test_server_load() (evennia.commands.default.tests.TestSystem method)

func_test_cmd_tasks() (in module evennia.commands.default.tests)

test_no_tasks() (evennia.commands.default.tests.TestCmdTasks method)

test_active_task() (evennia.commands.default.tests.TestCmdTasks method)

test_persistent_task() (evennia.commands.default.tests.TestCmdTasks method)

test_pause_unpause() (evennia.commands.default.tests.TestCmdTasks method)

test_do_task() (evennia.commands.default.tests.TestCmdTasks method)

test_remove() (evennia.commands.default.tests.TestCmdTasks method)

test_call() (evennia.commands.default.tests.TestCmdTasks method)

test_cancel() (evennia.commands.default.tests.TestCmdTasks method)

test_wrong_func_name() (evennia.commands.default.tests.TestCmdTasks method)

test_no_input() (evennia.commands.default.tests.TestCmdTasks method)

test_responce_of_yes() (evennia.commands.default.tests.TestCmdTasks method)

test_misformed_command() (evennia.commands.default.tests.TestCmdTasks method)

test_ooc_look (evennia.commands.default.tests.TestAccount attribute)

test_ic__other_object() (evennia.commands.default.tests.TestAccount method)

test_ic__nonaccess() (evennia.commands.default.tests.TestAccount method)

test_password() (evennia.commands.default.tests.TestAccount method)

test_option() (evennia.commands.default.tests.TestAccount method)

test_sessions() (evennia.commands.default.tests.TestAccount method)

test_color_test() (evennia.commands.default.tests.TestAccount method)

test_char_create() (evennia.commands.default.tests.TestAccount method)

test_char_delete() (evennia.commands.default.tests.TestAccount method)

test_quell() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_00() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_01() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_02() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_03() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_04() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_05() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_06() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_07() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_08() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_09() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_10() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_11() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_12() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_13() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_14() (evennia.commands.default.tests.TestAccount method)

test_ooc_look_15() (evennia.commands.default.tests.TestAccount method)

test_create() (evennia.commands.default.tests.TestBuilding method)

test_examine() (evennia.commands.default.tests.TestBuilding method)

test_set_obj_alias() (evennia.commands.default.tests.TestBuilding method)

test_copy() (evennia.commands.default.tests.TestBuilding method)

test_attribute_commands() (evennia.commands.default.tests.TestBuilding method)

test_split_nested_attr() (evennia.commands.default.tests.TestBuilding method)

test_do_nested_lookup() (evennia.commands.default.tests.TestBuilding method)

test_name() (evennia.commands.default.tests.TestBuilding method)

test_desc() (evennia.commands.default.tests.TestBuilding method)

test_empty_desc() (evennia.commands.default.tests.TestBuilding method)

test_desc_default_to_room() (evennia.commands.default.tests.TestBuilding method)

test_destroy() (evennia.commands.default.tests.TestBuilding method)

test_destroy_sequence() (evennia.commands.default.tests.TestBuilding method)

test_tunnel() (evennia.commands.default.tests.TestBuilding method)

test_exit_commands() (evennia.commands.default.tests.TestBuilding method)

test_set_home() (evennia.commands.default.tests.TestBuilding method)

test_list_cmdsets() (evennia.commands.default.tests.TestBuilding method)

test_typeclass() (evennia.commands.default.tests.TestBuilding method)

test_lock() (evennia.commands.default.tests.TestBuilding method)

test_find() (evennia.commands.default.tests.TestBuilding method)

test_script() (evennia.commands.default.tests.TestBuilding method)

test_script_multi_delete() (evennia.commands.default.tests.TestBuilding method)

test_teleport() (evennia.commands.default.tests.TestBuilding method)

test_spawn() (evennia.commands.default.tests.TestBuilding method)

setUp() (evennia.commands.default.tests.TestCommsChannel method)

tearDown() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__noarg() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__msg() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__list() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__all() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__history() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__sub() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__unsub() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__mute() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__unmute() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__create() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__destroy() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__desc() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__lock() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__unlock() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__boot() (evennia.commands.default.tests.TestCommsChannel method)

test_channel__who() (evennia.commands.default.tests.TestCommsChannel method)

test_discord__switches (evennia.commands.default.tests.TestDiscord attribute)

test_discord__linking() (evennia.commands.default.tests.TestDiscord method)

test_discord__list() (evennia.commands.default.tests.TestDiscord method)

test_discord__switches_0_() (evennia.commands.default.tests.TestDiscord method)

red_button (evennia.commands.default.tests.TestBatchProcess attribute)

test_batch_commands() (evennia.commands.default.tests.TestBatchProcess method)

help_category (evennia.commands.default.tests.CmdInterrupt attribute)

lock_storage (evennia.commands.default.tests.CmdInterrupt attribute)

search_index_entry (evennia.commands.default.tests.CmdInterrupt attribute)

TestUnconnectedCommand (class in evennia.commands.default.tests)

test_multimatch() (evennia.commands.default.tests.TestSystemCommands method)

CmdUnconnectedConnect (class in evennia.commands.default.unloggedin)

key (evennia.commands.default.unloggedin.CmdUnconnectedConnect attribute)

aliases (evennia.commands.default.unloggedin.CmdUnconnectedConnect attribute)

locks (evennia.commands.default.unloggedin.CmdUnconnectedConnect attribute)

arg_regex (evennia.commands.default.unloggedin.CmdUnconnectedConnect attribute)

func() (evennia.commands.default.unloggedin.CmdUnconnectedConnect method)

CmdUnconnectedCreate (class in evennia.commands.default.unloggedin)

key (evennia.commands.default.unloggedin.CmdUnconnectedCreate attribute)

aliases (evennia.commands.default.unloggedin.CmdUnconnectedCreate attribute)

locks (evennia.commands.default.unloggedin.CmdUnconnectedCreate attribute)

arg_regex (evennia.commands.default.unloggedin.CmdUnconnectedCreate attribute)

at_pre_cmd() (evennia.commands.default.unloggedin.CmdUnconnectedCreate method)

func() (evennia.commands.default.unloggedin.CmdUnconnectedCreate method)

CmdUnconnectedQuit (class in evennia.commands.default.unloggedin)

key (evennia.commands.default.unloggedin.CmdUnconnectedQuit attribute)

aliases (evennia.commands.default.unloggedin.CmdUnconnectedQuit attribute)

locks (evennia.commands.default.unloggedin.CmdUnconnectedQuit attribute)

func() (evennia.commands.default.unloggedin.CmdUnconnectedQuit method)

help_category (evennia.commands.default.unloggedin.CmdUnconnectedQuit attribute)

lock_storage (evennia.commands.default.unloggedin.CmdUnconnectedQuit attribute)

CmdUnconnectedLook (class in evennia.commands.default.unloggedin)

key (evennia.commands.default.unloggedin.CmdUnconnectedLook attribute)

aliases (evennia.commands.default.unloggedin.CmdUnconnectedLook attribute)

locks (evennia.commands.default.unloggedin.CmdUnconnectedLook attribute)

func() (evennia.commands.default.unloggedin.CmdUnconnectedLook method)

help_category (evennia.commands.default.unloggedin.CmdUnconnectedLook attribute)

lock_storage (evennia.commands.default.unloggedin.CmdUnconnectedLook attribute)

CmdUnconnectedHelp (class in evennia.commands.default.unloggedin)

key (evennia.commands.default.unloggedin.CmdUnconnectedHelp attribute)

aliases (evennia.commands.default.unloggedin.CmdUnconnectedHelp attribute)

locks (evennia.commands.default.unloggedin.CmdUnconnectedHelp attribute)

func() (evennia.commands.default.unloggedin.CmdUnconnectedHelp method)

help_category (evennia.commands.default.unloggedin.CmdUnconnectedHelp attribute)

lock_storage (evennia.commands.default.unloggedin.CmdUnconnectedHelp attribute)

CmdUnconnectedEncoding (class in evennia.commands.default.unloggedin)

key (evennia.commands.default.unloggedin.CmdUnconnectedEncoding attribute)

aliases (evennia.commands.default.unloggedin.CmdUnconnectedEncoding attribute)

locks (evennia.commands.default.unloggedin.CmdUnconnectedEncoding attribute)

func() (evennia.commands.default.unloggedin.CmdUnconnectedEncoding method)

CmdUnconnectedInfo (class in evennia.commands.default.unloggedin)

key (evennia.commands.default.unloggedin.CmdUnconnectedInfo attribute)

locks (evennia.commands.default.unloggedin.CmdUnconnectedInfo attribute)

func() (evennia.commands.default.unloggedin.CmdUnconnectedInfo method)

aliases (evennia.commands.default.unloggedin.CmdUnconnectedInfo attribute)

help_category (evennia.commands.default.unloggedin.CmdUnconnectedInfo attribute)

lock_storage (evennia.commands.default.unloggedin.CmdUnconnectedInfo attribute)

CmdUnconnectedScreenreader (class in evennia.commands.default.unloggedin)

key (evennia.commands.default.unloggedin.CmdUnconnectedScreenreader attribute)

func() (evennia.commands.default.unloggedin.CmdUnconnectedScreenreader method)

send_to_online_only (evennia.comms.comms.DefaultChannel attribute)

channel_prefix_string (evennia.comms.comms.DefaultChannel attribute)

channel_msg_nick_pattern (evennia.comms.comms.DefaultChannel attribute)

channel_msg_nick_replacement (evennia.comms.comms.DefaultChannel attribute)

at_channel_creation() (evennia.comms.comms.DefaultChannel method)

add_user_channel_alias() (evennia.comms.comms.DefaultChannel method)

remove_user_channel_alias() (evennia.comms.comms.DefaultChannel class method)

post_leave_channel() (evennia.comms.comms.DefaultChannel method)

web_get_create_url() (evennia.comms.comms.DefaultChannel class method)

web_get_detail_url() (evennia.comms.comms.DefaultChannel method)

web_get_update_url() (evennia.comms.comms.DefaultChannel method)

web_get_delete_url() (evennia.comms.comms.DefaultChannel method)

distribute_message() (evennia.comms.comms.DefaultChannel method)

get_messages_by_sender() (evennia.comms.managers.MsgManager method)

get_messages_by_receiver() (evennia.comms.managers.MsgManager method)

get_all_channels() (evennia.comms.managers.ChannelDBManager method)

get_subscriptions() (evennia.comms.managers.ChannelDBManager method)

search_channel() (evennia.comms.managers.ChannelDBManager method)

create_channel() (evennia.comms.managers.ChannelDBManager method)

channel_search() (evennia.comms.managers.ChannelDBManager method)

get_previous_by_db_date_created() (evennia.comms.models.Msg method)

db_account_subscriptions (evennia.comms.models.ChannelDB attribute)

db_object_subscriptions (evennia.comms.models.ChannelDB attribute)

get_next_by_db_date_created() (evennia.comms.models.ChannelDB method)

get_previous_by_db_date_created() (evennia.comms.models.ChannelDB method)

S3Boto3TestCase (class in evennia.contrib.base_systems.awsstorage.tests)

setUp() (evennia.contrib.base_systems.awsstorage.tests.S3Boto3TestCase method)

S3Boto3StorageTests (class in evennia.contrib.base_systems.awsstorage.tests)

menu_setattr() (in module evennia.contrib.base_systems.building_menu.building_menu)

menu_quit() (in module evennia.contrib.base_systems.building_menu.building_menu)

menu_edit() (in module evennia.contrib.base_systems.building_menu.building_menu)

CmdNoInput (class in evennia.contrib.base_systems.building_menu.building_menu)

CmdNoMatch (class in evennia.contrib.base_systems.building_menu.building_menu)

BuildingMenuCmdSet (class in evennia.contrib.base_systems.building_menu.building_menu)

Choice (class in evennia.contrib.base_systems.building_menu.building_menu)

enter() (evennia.contrib.base_systems.building_menu.building_menu.Choice method)

leave() (evennia.contrib.base_systems.building_menu.building_menu.Choice method)

BuildingMenu (class in evennia.contrib.base_systems.building_menu.building_menu)

restore() (evennia.contrib.base_systems.building_menu.building_menu.BuildingMenu static method)

GenericBuildingMenu (class in evennia.contrib.base_systems.building_menu.building_menu)

GenericBuildingCmd (class in evennia.contrib.base_systems.building_menu.building_menu)

Submenu (class in evennia.contrib.base_systems.building_menu.tests)

init() (evennia.contrib.base_systems.building_menu.tests.Submenu method)

TestBuildingMenu (class in evennia.contrib.base_systems.building_menu.tests)

TestColorMarkup (class in evennia.contrib.base_systems.color_markups.tests)

get_component_class() (in module evennia.contrib.base_systems.components)

Component (class in evennia.contrib.base_systems.components.component)

name (evennia.contrib.base_systems.components.component.Component attribute)

__init__() (evennia.contrib.base_systems.components.component.Component method)

default_create() (evennia.contrib.base_systems.components.component.Component class method)

create() (evennia.contrib.base_systems.components.component.Component class method)

cleanup() (evennia.contrib.base_systems.components.component.Component method)

load() (evennia.contrib.base_systems.components.component.Component class method)

at_added() (evennia.contrib.base_systems.components.component.Component method)

DBField (class in evennia.contrib.base_systems.components.dbfield)

NDBField (class in evennia.contrib.base_systems.components.dbfield)

TagField (class in evennia.contrib.base_systems.components.dbfield)

__init__() (evennia.contrib.base_systems.components.dbfield.TagField method)

ComponentProperty (class in evennia.contrib.base_systems.components.holder)

ComponentHandler (class in evennia.contrib.base_systems.components.holder)

add() (evennia.contrib.base_systems.components.holder.ComponentHandler method)

get() (evennia.contrib.base_systems.components.holder.ComponentHandler method)

has() (evennia.contrib.base_systems.components.holder.ComponentHandler method)

ComponentHolderMixin (class in evennia.contrib.base_systems.components.holder)

as_listener() (in module evennia.contrib.base_systems.components.signals)

as_responder() (in module evennia.contrib.base_systems.components.signals)

SignalsHandler (class in evennia.contrib.base_systems.components.signals)

query() (evennia.contrib.base_systems.components.signals.SignalsHandler method)

ComponentTestA (class in evennia.contrib.base_systems.components.tests)

name (evennia.contrib.base_systems.components.tests.ComponentTestA attribute)

my_int (evennia.contrib.base_systems.components.tests.ComponentTestA attribute)

my_list (evennia.contrib.base_systems.components.tests.ComponentTestA attribute)

ComponentTestB (class in evennia.contrib.base_systems.components.tests)

name (evennia.contrib.base_systems.components.tests.ComponentTestB attribute)

my_int (evennia.contrib.base_systems.components.tests.ComponentTestB attribute)

my_list (evennia.contrib.base_systems.components.tests.ComponentTestB attribute)

RuntimeComponentTestC (class in evennia.contrib.base_systems.components.tests)

CharacterWithComponents (class in evennia.contrib.base_systems.components.tests)

InheritedTCWithComponents (class in evennia.contrib.base_systems.components.tests)

TestComponents (class in evennia.contrib.base_systems.components.tests)

CharWithSignal (class in evennia.contrib.base_systems.components.tests)

path (evennia.contrib.base_systems.components.tests.CharWithSignal attribute)

ComponentWithSignal (class in evennia.contrib.base_systems.components.tests)

TestComponentSignals (class in evennia.contrib.base_systems.components.tests)

time_to_tuple() (in module evennia.contrib.base_systems.custom_gametime.custom_gametime)

gametime_to_realtime() (in module evennia.contrib.base_systems.custom_gametime.custom_gametime)

realtime_to_gametime() (in module evennia.contrib.base_systems.custom_gametime.custom_gametime)

custom_gametime() (in module evennia.contrib.base_systems.custom_gametime.custom_gametime)

real_seconds_until() (in module evennia.contrib.base_systems.custom_gametime.custom_gametime)

schedule() (in module evennia.contrib.base_systems.custom_gametime.custom_gametime)

GametimeScript (class in evennia.contrib.base_systems.custom_gametime.custom_gametime)

TestCustomGameTime (class in evennia.contrib.base_systems.custom_gametime.tests)

CmdUnconnectedConnect (class in evennia.contrib.base_systems.email_login.email_login)

CmdUnconnectedCreate (class in evennia.contrib.base_systems.email_login.email_login)

CmdUnconnectedQuit (class in evennia.contrib.base_systems.email_login.email_login)

CmdUnconnectedLook (class in evennia.contrib.base_systems.email_login.email_login)

CmdUnconnectedHelp (class in evennia.contrib.base_systems.email_login.email_login)

TestEmailLogin (class in evennia.contrib.base_systems.email_login.tests)

TestText2Bbcode (class in evennia.contrib.base_systems.godotwebsocket.test_text2bbcode)

COLOR_INDICE_TO_HEX (in module evennia.contrib.base_systems.godotwebsocket.text2bbcode)

RootTag (class in evennia.contrib.base_systems.godotwebsocket.text2bbcode)

ChildTag (class in evennia.contrib.base_systems.godotwebsocket.text2bbcode)

TextTag (class in evennia.contrib.base_systems.godotwebsocket.text2bbcode)

text (evennia.contrib.base_systems.godotwebsocket.text2bbcode.TextTag attribute)

BBCodeTag (class in evennia.contrib.base_systems.godotwebsocket.text2bbcode)

UnderlineTag (class in evennia.contrib.base_systems.godotwebsocket.text2bbcode)

BlinkTag (class in evennia.contrib.base_systems.godotwebsocket.text2bbcode)

ColorTag (class in evennia.contrib.base_systems.godotwebsocket.text2bbcode)

BGColorTag (class in evennia.contrib.base_systems.godotwebsocket.text2bbcode)

UrlTag (class in evennia.contrib.base_systems.godotwebsocket.text2bbcode)

code (evennia.contrib.base_systems.godotwebsocket.text2bbcode.UrlTag attribute)

child (evennia.contrib.base_systems.godotwebsocket.text2bbcode.UrlTag attribute)

TextToBBCODEparser (class in evennia.contrib.base_systems.godotwebsocket.text2bbcode)

parse_to_bbcode() (in module evennia.contrib.base_systems.godotwebsocket.text2bbcode)

GodotWebSocketClient (class in evennia.contrib.base_systems.godotwebsocket.webclient)

start_plugin_services() (in module evennia.contrib.base_systems.godotwebsocket.webclient)

CallbackHandler (class in evennia.contrib.base_systems.ingame_python.callbackhandler)

format_callback() (evennia.contrib.base_systems.ingame_python.callbackhandler.CallbackHandler static method)

Callback (class in evennia.contrib.base_systems.ingame_python.callbackhandler)

CmdCallback (class in evennia.contrib.base_systems.ingame_python.commands)

key (evennia.contrib.base_systems.ingame_python.commands.CmdCallback attribute)

func() (evennia.contrib.base_systems.ingame_python.commands.CmdCallback method)

deny() (in module evennia.contrib.base_systems.ingame_python.eventfuncs)

get() (in module evennia.contrib.base_systems.ingame_python.eventfuncs)

call_event() (in module evennia.contrib.base_systems.ingame_python.eventfuncs)

EventHandler (class in evennia.contrib.base_systems.ingame_python.scripts)

call() (evennia.contrib.base_systems.ingame_python.scripts.EventHandler method)

path (evennia.contrib.base_systems.ingame_python.scripts.EventHandler attribute)

TimeEventScript (class in evennia.contrib.base_systems.ingame_python.scripts)

complete_task() (in module evennia.contrib.base_systems.ingame_python.scripts)

TestEventHandler (class in evennia.contrib.base_systems.ingame_python.tests)

TestCmdCallback (class in evennia.contrib.base_systems.ingame_python.tests)

TestDefaultCallbacks (class in evennia.contrib.base_systems.ingame_python.tests)

get_event_handler() (in module evennia.contrib.base_systems.ingame_python.utils)

register_events() (in module evennia.contrib.base_systems.ingame_python.utils)

get_next_wait() (in module evennia.contrib.base_systems.ingame_python.utils)

time_event() (in module evennia.contrib.base_systems.ingame_python.utils)

keyword_event() (in module evennia.contrib.base_systems.ingame_python.utils)

phrase_event() (in module evennia.contrib.base_systems.ingame_python.utils)

CmdAddCom (class in evennia.contrib.base_systems.mux_comms_cmds.mux_comms_cmds)

CmdDelCom (class in evennia.contrib.base_systems.mux_comms_cmds.mux_comms_cmds)

CmdAllCom (class in evennia.contrib.base_systems.mux_comms_cmds.mux_comms_cmds)

CmdCdestroy (class in evennia.contrib.base_systems.mux_comms_cmds.mux_comms_cmds)

CmdCBoot (class in evennia.contrib.base_systems.mux_comms_cmds.mux_comms_cmds)

CmdCWho (class in evennia.contrib.base_systems.mux_comms_cmds.mux_comms_cmds)

CmdChannelCreate (class in evennia.contrib.base_systems.mux_comms_cmds.mux_comms_cmds)

CmdClock (class in evennia.contrib.base_systems.mux_comms_cmds.mux_comms_cmds)

CmdCdesc (class in evennia.contrib.base_systems.mux_comms_cmds.mux_comms_cmds)

CmdSetLegacyComms (class in evennia.contrib.base_systems.mux_comms_cmds.mux_comms_cmds)

TestLegacyMuxComms (class in evennia.contrib.base_systems.mux_comms_cmds.tests)

CmdDummy (class in evennia.contrib.base_systems.unixcommand.tests)

key (evennia.contrib.base_systems.unixcommand.tests.CmdDummy attribute)

init_parser() (evennia.contrib.base_systems.unixcommand.tests.CmdDummy method)

func() (evennia.contrib.base_systems.unixcommand.tests.CmdDummy method)

aliases (evennia.contrib.base_systems.unixcommand.tests.CmdDummy attribute)

lock_storage (evennia.contrib.base_systems.unixcommand.tests.CmdDummy attribute)

TestUnixCommand (class in evennia.contrib.base_systems.unixcommand.tests)

UnixCommandParser (class in evennia.contrib.base_systems.unixcommand.unixcommand)

HelpAction (class in evennia.contrib.base_systems.unixcommand.unixcommand)

UnixCommand (class in evennia.contrib.base_systems.unixcommand.unixcommand)

func() (evennia.contrib.base_systems.unixcommand.unixcommand.UnixCommand method)

key (evennia.contrib.base_systems.unixcommand.unixcommand.UnixCommand attribute)

CmdEvscapeRoom (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdEvscapeRoom attribute)

CmdGiveUp (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdGiveUp attribute)

aliases (evennia.contrib.full_systems.evscaperoom.commands.CmdGiveUp attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdGiveUp method)

CmdLook (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdLook attribute)

aliases (evennia.contrib.full_systems.evscaperoom.commands.CmdLook attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdLook method)

CmdWho (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdWho attribute)

obj1_search (evennia.contrib.full_systems.evscaperoom.commands.CmdWho attribute)

obj2_search (evennia.contrib.full_systems.evscaperoom.commands.CmdWho attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdWho method)

aliases (evennia.contrib.full_systems.evscaperoom.commands.CmdWho attribute)

CmdSpeak (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdSpeak attribute)

aliases (evennia.contrib.full_systems.evscaperoom.commands.CmdSpeak attribute)

arg_regex (evennia.contrib.full_systems.evscaperoom.commands.CmdSpeak attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdSpeak method)

CmdEmote (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdEmote attribute)

aliases (evennia.contrib.full_systems.evscaperoom.commands.CmdEmote attribute)

arg_regex (evennia.contrib.full_systems.evscaperoom.commands.CmdEmote attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdEmote method)

CmdFocus (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdFocus attribute)

aliases (evennia.contrib.full_systems.evscaperoom.commands.CmdFocus attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdFocus method)

CmdOptions (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdOptions attribute)

aliases (evennia.contrib.full_systems.evscaperoom.commands.CmdOptions attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdOptions method)

CmdGet (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdGet attribute)

aliases (evennia.contrib.full_systems.evscaperoom.commands.CmdGet attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdGet method)

CmdRerouter (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdRerouter attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdRerouter method)

CmdFocusInteraction (class in evennia.contrib.full_systems.evscaperoom.commands)

CmdStand (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdStand attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdStand method)

aliases (evennia.contrib.full_systems.evscaperoom.commands.CmdStand attribute)

CmdHelp (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdHelp attribute)

aliases (evennia.contrib.full_systems.evscaperoom.commands.CmdHelp attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdHelp method)

CmdCreateObj (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdCreateObj attribute)

locks (evennia.contrib.full_systems.evscaperoom.commands.CmdCreateObj attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdCreateObj method)

CmdSetFlag (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdSetFlag attribute)

aliases (evennia.contrib.full_systems.evscaperoom.commands.CmdSetFlag attribute)

locks (evennia.contrib.full_systems.evscaperoom.commands.CmdSetFlag attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdSetFlag method)

CmdJumpState (class in evennia.contrib.full_systems.evscaperoom.commands)

key (evennia.contrib.full_systems.evscaperoom.commands.CmdJumpState attribute)

locks (evennia.contrib.full_systems.evscaperoom.commands.CmdJumpState attribute)

func() (evennia.contrib.full_systems.evscaperoom.commands.CmdJumpState method)

CmdEvscapeRoomStart (class in evennia.contrib.full_systems.evscaperoom.commands)

CmdSetEvScapeRoom (class in evennia.contrib.full_systems.evscaperoom.commands)

node_set_desc() (in module evennia.contrib.full_systems.evscaperoom.menu)

node_create_room() (in module evennia.contrib.full_systems.evscaperoom.menu)

node_join_room() (in module evennia.contrib.full_systems.evscaperoom.menu)

node_quit() (in module evennia.contrib.full_systems.evscaperoom.menu)

EvscaperoomMenu (class in evennia.contrib.full_systems.evscaperoom.menu)

run_evscaperoom_menu() (in module evennia.contrib.full_systems.evscaperoom.menu)

node_options() (in module evennia.contrib.full_systems.evscaperoom.menu)

OptionsMenu (class in evennia.contrib.full_systems.evscaperoom.menu)

run_option_menu() (in module evennia.contrib.full_systems.evscaperoom.menu)

EvscaperoomObject (class in evennia.contrib.full_systems.evscaperoom.objects)

Feelable (class in evennia.contrib.full_systems.evscaperoom.objects)

path (evennia.contrib.full_systems.evscaperoom.objects.Feelable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Feelable attribute)

Listenable (class in evennia.contrib.full_systems.evscaperoom.objects)

path (evennia.contrib.full_systems.evscaperoom.objects.Listenable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Listenable attribute)

Smellable (class in evennia.contrib.full_systems.evscaperoom.objects)

path (evennia.contrib.full_systems.evscaperoom.objects.Smellable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Smellable attribute)

Rotatable (class in evennia.contrib.full_systems.evscaperoom.objects)

at_rotate() (evennia.contrib.full_systems.evscaperoom.objects.Rotatable method)

path (evennia.contrib.full_systems.evscaperoom.objects.Rotatable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Rotatable attribute)

Openable (class in evennia.contrib.full_systems.evscaperoom.objects)

open_flag (evennia.contrib.full_systems.evscaperoom.objects.Openable attribute)

start_open (evennia.contrib.full_systems.evscaperoom.objects.Openable attribute)

at_open() (evennia.contrib.full_systems.evscaperoom.objects.Openable method)

at_locked() (evennia.contrib.full_systems.evscaperoom.objects.Openable method)

at_close() (evennia.contrib.full_systems.evscaperoom.objects.Openable method)

path (evennia.contrib.full_systems.evscaperoom.objects.Openable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Openable attribute)

Readable (class in evennia.contrib.full_systems.evscaperoom.objects)

read_flag (evennia.contrib.full_systems.evscaperoom.objects.Readable attribute)

at_read() (evennia.contrib.full_systems.evscaperoom.objects.Readable method)

path (evennia.contrib.full_systems.evscaperoom.objects.Readable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Readable attribute)

IndexReadable (class in evennia.contrib.full_systems.evscaperoom.objects)

index (evennia.contrib.full_systems.evscaperoom.objects.IndexReadable attribute)

path (evennia.contrib.full_systems.evscaperoom.objects.IndexReadable attribute)

Movable (class in evennia.contrib.full_systems.evscaperoom.objects)

at_left() (evennia.contrib.full_systems.evscaperoom.objects.Movable method)

at_right() (evennia.contrib.full_systems.evscaperoom.objects.Movable method)

path (evennia.contrib.full_systems.evscaperoom.objects.Movable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Movable attribute)

BaseConsumable (class in evennia.contrib.full_systems.evscaperoom.objects)

path (evennia.contrib.full_systems.evscaperoom.objects.BaseConsumable attribute)

Edible (class in evennia.contrib.full_systems.evscaperoom.objects)

consume_flag (evennia.contrib.full_systems.evscaperoom.objects.Edible attribute)

at_focus_eat() (evennia.contrib.full_systems.evscaperoom.objects.Edible method)

path (evennia.contrib.full_systems.evscaperoom.objects.Edible attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Edible attribute)

Drinkable (class in evennia.contrib.full_systems.evscaperoom.objects)

at_consume() (evennia.contrib.full_systems.evscaperoom.objects.Drinkable method)

path (evennia.contrib.full_systems.evscaperoom.objects.Drinkable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Drinkable attribute)

BaseApplicable (class in evennia.contrib.full_systems.evscaperoom.objects)

path (evennia.contrib.full_systems.evscaperoom.objects.BaseApplicable attribute)

Usable (class in evennia.contrib.full_systems.evscaperoom.objects)

target_flag (evennia.contrib.full_systems.evscaperoom.objects.Usable attribute)

at_focus_use() (evennia.contrib.full_systems.evscaperoom.objects.Usable method)

at_apply() (evennia.contrib.full_systems.evscaperoom.objects.Usable method)

path (evennia.contrib.full_systems.evscaperoom.objects.Usable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Usable attribute)

Insertable (class in evennia.contrib.full_systems.evscaperoom.objects)

at_apply() (evennia.contrib.full_systems.evscaperoom.objects.Insertable method)

path (evennia.contrib.full_systems.evscaperoom.objects.Insertable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Insertable attribute)

Combinable (class in evennia.contrib.full_systems.evscaperoom.objects)

at_apply() (evennia.contrib.full_systems.evscaperoom.objects.Combinable method)

path (evennia.contrib.full_systems.evscaperoom.objects.Combinable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Combinable attribute)

Mixable (class in evennia.contrib.full_systems.evscaperoom.objects)

mixer_flag (evennia.contrib.full_systems.evscaperoom.objects.Mixable attribute)

handle_mix() (evennia.contrib.full_systems.evscaperoom.objects.Mixable method)

at_mix() (evennia.contrib.full_systems.evscaperoom.objects.Mixable method)

path (evennia.contrib.full_systems.evscaperoom.objects.Mixable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Mixable attribute)

HasButtons (class in evennia.contrib.full_systems.evscaperoom.objects)

buttons (evennia.contrib.full_systems.evscaperoom.objects.HasButtons attribute)

path (evennia.contrib.full_systems.evscaperoom.objects.HasButtons attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.HasButtons attribute)

CodeInput (class in evennia.contrib.full_systems.evscaperoom.objects)

code (evennia.contrib.full_systems.evscaperoom.objects.CodeInput attribute)

code_hint (evennia.contrib.full_systems.evscaperoom.objects.CodeInput attribute)

at_no_code() (evennia.contrib.full_systems.evscaperoom.objects.CodeInput method)

path (evennia.contrib.full_systems.evscaperoom.objects.CodeInput attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.CodeInput attribute)

BasePositionable (class in evennia.contrib.full_systems.evscaperoom.objects)

Sittable (class in evennia.contrib.full_systems.evscaperoom.objects)

path (evennia.contrib.full_systems.evscaperoom.objects.Sittable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Sittable attribute)

Liable (class in evennia.contrib.full_systems.evscaperoom.objects)

at_focus_lie() (evennia.contrib.full_systems.evscaperoom.objects.Liable method)

path (evennia.contrib.full_systems.evscaperoom.objects.Liable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Liable attribute)

Kneelable (class in evennia.contrib.full_systems.evscaperoom.objects)

path (evennia.contrib.full_systems.evscaperoom.objects.Kneelable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Kneelable attribute)

Climbable (class in evennia.contrib.full_systems.evscaperoom.objects)

path (evennia.contrib.full_systems.evscaperoom.objects.Climbable attribute)

typename (evennia.contrib.full_systems.evscaperoom.objects.Climbable attribute)

Positionable (class in evennia.contrib.full_systems.evscaperoom.objects)

path (evennia.contrib.full_systems.evscaperoom.objects.Positionable attribute)

EvscapeRoom (class in evennia.contrib.full_systems.evscaperoom.room)

state() (evennia.contrib.full_systems.evscaperoom.room.EvscapeRoom property)

log() (evennia.contrib.full_systems.evscaperoom.room.EvscapeRoom method)

score() (evennia.contrib.full_systems.evscaperoom.room.EvscapeRoom method)

progress() (evennia.contrib.full_systems.evscaperoom.room.EvscapeRoom method)

achievement() (evennia.contrib.full_systems.evscaperoom.room.EvscapeRoom method)

set_flag() (evennia.contrib.full_systems.evscaperoom.room.EvscapeRoom method)

unset_flag() (evennia.contrib.full_systems.evscaperoom.room.EvscapeRoom method)

check_flag() (evennia.contrib.full_systems.evscaperoom.room.EvscapeRoom method)

check_perm() (evennia.contrib.full_systems.evscaperoom.room.EvscapeRoom method)

delete() (evennia.contrib.full_systems.evscaperoom.room.EvscapeRoom method)

path (evennia.contrib.full_systems.evscaperoom.room.EvscapeRoom attribute)

typename (evennia.contrib.full_systems.evscaperoom.room.EvscapeRoom attribute)

CleanupScript (class in evennia.contrib.full_systems.evscaperoom.scripts)

path (evennia.contrib.full_systems.evscaperoom.scripts.CleanupScript attribute)

StateHandler (class in evennia.contrib.full_systems.evscaperoom.state)

__init__() (evennia.contrib.full_systems.evscaperoom.state.StateHandler method)

BaseState (class in evennia.contrib.full_systems.evscaperoom.state)

next_state (evennia.contrib.full_systems.evscaperoom.state.BaseState attribute)

hints (evennia.contrib.full_systems.evscaperoom.state.BaseState attribute)

__init__() (evennia.contrib.full_systems.evscaperoom.state.BaseState method)

get_hint() (evennia.contrib.full_systems.evscaperoom.state.BaseState method)

msg() (evennia.contrib.full_systems.evscaperoom.state.BaseState method)

cinematic() (evennia.contrib.full_systems.evscaperoom.state.BaseState method)

get_object() (evennia.contrib.full_systems.evscaperoom.state.BaseState method)

init() (evennia.contrib.full_systems.evscaperoom.state.BaseState method)

clean() (evennia.contrib.full_systems.evscaperoom.state.BaseState method)

next() (evennia.contrib.full_systems.evscaperoom.state.BaseState method)

TestEvscaperoomCommands (class in evennia.contrib.full_systems.evscaperoom.tests)

TestUtils (class in evennia.contrib.full_systems.evscaperoom.tests)

TestEvScapeRoom (class in evennia.contrib.full_systems.evscaperoom.tests)

setUp() (evennia.contrib.full_systems.evscaperoom.tests.TestEvScapeRoom method)

TestStates (class in evennia.contrib.full_systems.evscaperoom.tests)

setUp() (evennia.contrib.full_systems.evscaperoom.tests.TestStates method)

tearDown() (evennia.contrib.full_systems.evscaperoom.tests.TestStates method)

create_evscaperoom_object() (in module evennia.contrib.full_systems.evscaperoom.utils)

create_fantasy_word() (in module evennia.contrib.full_systems.evscaperoom.utils)

parse_for_perspectives() (in module evennia.contrib.full_systems.evscaperoom.utils)

parse_for_things() (in module evennia.contrib.full_systems.evscaperoom.utils)

add_msg_borders() (in module evennia.contrib.full_systems.evscaperoom.utils)

msg_cinematic() (in module evennia.contrib.full_systems.evscaperoom.utils)

TradeTimeout (class in evennia.contrib.game_systems.barter.barter)

at_repeat() (evennia.contrib.game_systems.barter.barter.TradeTimeout method)

is_valid() (evennia.contrib.game_systems.barter.barter.TradeTimeout method)

path (evennia.contrib.game_systems.barter.barter.TradeTimeout attribute)

typename (evennia.contrib.game_systems.barter.barter.TradeTimeout attribute)

TradeHandler (class in evennia.contrib.game_systems.barter.barter)

__init__() (evennia.contrib.game_systems.barter.barter.TradeHandler method)

msg_other() (evennia.contrib.game_systems.barter.barter.TradeHandler method)

get_other() (evennia.contrib.game_systems.barter.barter.TradeHandler method)

join() (evennia.contrib.game_systems.barter.barter.TradeHandler method)

unjoin() (evennia.contrib.game_systems.barter.barter.TradeHandler method)

offer() (evennia.contrib.game_systems.barter.barter.TradeHandler method)

list() (evennia.contrib.game_systems.barter.barter.TradeHandler method)

search() (evennia.contrib.game_systems.barter.barter.TradeHandler method)

accept() (evennia.contrib.game_systems.barter.barter.TradeHandler method)

decline() (evennia.contrib.game_systems.barter.barter.TradeHandler method)

finish() (evennia.contrib.game_systems.barter.barter.TradeHandler method)

CmdTradeBase (class in evennia.contrib.game_systems.barter.barter)

parse() (evennia.contrib.game_systems.barter.barter.CmdTradeBase method)

aliases (evennia.contrib.game_systems.barter.barter.CmdTradeBase attribute)

key (evennia.contrib.game_systems.barter.barter.CmdTradeBase attribute)

lock_storage (evennia.contrib.game_systems.barter.barter.CmdTradeBase attribute)

CmdTradeHelp (class in evennia.contrib.game_systems.barter.barter)

key (evennia.contrib.game_systems.barter.barter.CmdTradeHelp attribute)

locks (evennia.contrib.game_systems.barter.barter.CmdTradeHelp attribute)

func() (evennia.contrib.game_systems.barter.barter.CmdTradeHelp method)

aliases (evennia.contrib.game_systems.barter.barter.CmdTradeHelp attribute)

lock_storage (evennia.contrib.game_systems.barter.barter.CmdTradeHelp attribute)

key (evennia.contrib.game_systems.barter.barter.CmdOffer attribute)

locks (evennia.contrib.game_systems.barter.barter.CmdOffer attribute)

help_category (evennia.contrib.game_systems.barter.barter.CmdOffer attribute)

func() (evennia.contrib.game_systems.barter.barter.CmdOffer method)

aliases (evennia.contrib.game_systems.barter.barter.CmdOffer attribute)

lock_storage (evennia.contrib.game_systems.barter.barter.CmdOffer attribute)

key (evennia.contrib.game_systems.barter.barter.CmdAccept attribute)

aliases (evennia.contrib.game_systems.barter.barter.CmdAccept attribute)

locks (evennia.contrib.game_systems.barter.barter.CmdAccept attribute)

help_category (evennia.contrib.game_systems.barter.barter.CmdAccept attribute)

func() (evennia.contrib.game_systems.barter.barter.CmdAccept method)

lock_storage (evennia.contrib.game_systems.barter.barter.CmdAccept attribute)

CmdDecline (class in evennia.contrib.game_systems.barter.barter)

key (evennia.contrib.game_systems.barter.barter.CmdDecline attribute)

locks (evennia.contrib.game_systems.barter.barter.CmdDecline attribute)

help_category (evennia.contrib.game_systems.barter.barter.CmdDecline attribute)

func() (evennia.contrib.game_systems.barter.barter.CmdDecline method)

aliases (evennia.contrib.game_systems.barter.barter.CmdDecline attribute)

lock_storage (evennia.contrib.game_systems.barter.barter.CmdDecline attribute)

CmdEvaluate (class in evennia.contrib.game_systems.barter.barter)

key (evennia.contrib.game_systems.barter.barter.CmdEvaluate attribute)

aliases (evennia.contrib.game_systems.barter.barter.CmdEvaluate attribute)

locks (evennia.contrib.game_systems.barter.barter.CmdEvaluate attribute)

help_category (evennia.contrib.game_systems.barter.barter.CmdEvaluate attribute)

func() (evennia.contrib.game_systems.barter.barter.CmdEvaluate method)

lock_storage (evennia.contrib.game_systems.barter.barter.CmdEvaluate attribute)

key (evennia.contrib.game_systems.barter.barter.CmdStatus attribute)

aliases (evennia.contrib.game_systems.barter.barter.CmdStatus attribute)

locks (evennia.contrib.game_systems.barter.barter.CmdStatus attribute)

help_category (evennia.contrib.game_systems.barter.barter.CmdStatus attribute)

func() (evennia.contrib.game_systems.barter.barter.CmdStatus method)

lock_storage (evennia.contrib.game_systems.barter.barter.CmdStatus attribute)

key (evennia.contrib.game_systems.barter.barter.CmdFinish attribute)

aliases (evennia.contrib.game_systems.barter.barter.CmdFinish attribute)

locks (evennia.contrib.game_systems.barter.barter.CmdFinish attribute)

help_category (evennia.contrib.game_systems.barter.barter.CmdFinish attribute)

func() (evennia.contrib.game_systems.barter.barter.CmdFinish method)

lock_storage (evennia.contrib.game_systems.barter.barter.CmdFinish attribute)

CmdsetTrade (class in evennia.contrib.game_systems.barter.barter)

key (evennia.contrib.game_systems.barter.barter.CmdsetTrade attribute)

path (evennia.contrib.game_systems.barter.barter.CmdsetTrade attribute)

key (evennia.contrib.game_systems.barter.barter.CmdTrade attribute)

aliases (evennia.contrib.game_systems.barter.barter.CmdTrade attribute)

locks (evennia.contrib.game_systems.barter.barter.CmdTrade attribute)

help_category (evennia.contrib.game_systems.barter.barter.CmdTrade attribute)

func() (evennia.contrib.game_systems.barter.barter.CmdTrade method)

lock_storage (evennia.contrib.game_systems.barter.barter.CmdTrade attribute)

setUp() (evennia.contrib.game_systems.barter.tests.TestBarter method)

test_cmdtrade() (evennia.contrib.game_systems.barter.tests.TestBarter method)

order_clothes_list() (in module evennia.contrib.game_systems.clothing.clothing)

get_worn_clothes() (in module evennia.contrib.game_systems.clothing.clothing)

clothing_type_count() (in module evennia.contrib.game_systems.clothing.clothing)

single_type_count() (in module evennia.contrib.game_systems.clothing.clothing)

ContribClothing (class in evennia.contrib.game_systems.clothing.clothing)

wear() (evennia.contrib.game_systems.clothing.clothing.ContribClothing method)

remove() (evennia.contrib.game_systems.clothing.clothing.ContribClothing method)

at_get() (evennia.contrib.game_systems.clothing.clothing.ContribClothing method)

path (evennia.contrib.game_systems.clothing.clothing.ContribClothing attribute)

ClothedCharacter (class in evennia.contrib.game_systems.clothing.clothing)

path (evennia.contrib.game_systems.clothing.clothing.ClothedCharacter attribute)

CmdWear (class in evennia.contrib.game_systems.clothing.clothing)

key (evennia.contrib.game_systems.clothing.clothing.CmdWear attribute)

help_category (evennia.contrib.game_systems.clothing.clothing.CmdWear attribute)

func() (evennia.contrib.game_systems.clothing.clothing.CmdWear method)

aliases (evennia.contrib.game_systems.clothing.clothing.CmdWear attribute)

lock_storage (evennia.contrib.game_systems.clothing.clothing.CmdWear attribute)

CmdRemove (class in evennia.contrib.game_systems.clothing.clothing)

key (evennia.contrib.game_systems.clothing.clothing.CmdRemove attribute)

func() (evennia.contrib.game_systems.clothing.clothing.CmdRemove method)

aliases (evennia.contrib.game_systems.clothing.clothing.CmdRemove attribute)

CmdCover (class in evennia.contrib.game_systems.clothing.clothing)

key (evennia.contrib.game_systems.clothing.clothing.CmdCover attribute)

rhs_split (evennia.contrib.game_systems.clothing.clothing.CmdCover attribute)

func() (evennia.contrib.game_systems.clothing.clothing.CmdCover method)

aliases (evennia.contrib.game_systems.clothing.clothing.CmdCover attribute)

lock_storage (evennia.contrib.game_systems.clothing.clothing.CmdCover attribute)

CmdUncover (class in evennia.contrib.game_systems.clothing.clothing)

key (evennia.contrib.game_systems.clothing.clothing.CmdUncover attribute)

func() (evennia.contrib.game_systems.clothing.clothing.CmdUncover method)

aliases (evennia.contrib.game_systems.clothing.clothing.CmdUncover attribute)

CmdInventory (class in evennia.contrib.game_systems.clothing.clothing)

key (evennia.contrib.game_systems.clothing.clothing.CmdInventory attribute)

aliases (evennia.contrib.game_systems.clothing.clothing.CmdInventory attribute)

locks (evennia.contrib.game_systems.clothing.clothing.CmdInventory attribute)

func() (evennia.contrib.game_systems.clothing.clothing.CmdInventory method)

ClothedCharacterCmdSet (class in evennia.contrib.game_systems.clothing.clothing)

TestClothingCmd (class in evennia.contrib.game_systems.clothing.tests)

setUp() (evennia.contrib.game_systems.clothing.tests.TestClothingCmd method)

TestClothingFunc (class in evennia.contrib.game_systems.clothing.tests)

setUp() (evennia.contrib.game_systems.clothing.tests.TestClothingFunc method)

CooldownHandler (class in evennia.contrib.game_systems.cooldowns.cooldowns)

obj (evennia.contrib.game_systems.cooldowns.cooldowns.CooldownHandler attribute)

add() (evennia.contrib.game_systems.cooldowns.cooldowns.CooldownHandler method)

set() (evennia.contrib.game_systems.cooldowns.cooldowns.CooldownHandler method)

TestCooldowns (class in evennia.contrib.game_systems.cooldowns.tests)

setUp() (evennia.contrib.game_systems.cooldowns.tests.TestCooldowns method)

test_empty() (evennia.contrib.game_systems.cooldowns.tests.TestCooldowns method)

test_add() (evennia.contrib.game_systems.cooldowns.tests.TestCooldowns method)

test_reset() (evennia.contrib.game_systems.cooldowns.tests.TestCooldowns method)

test_clear() (evennia.contrib.game_systems.cooldowns.tests.TestCooldowns method)

CraftingRecipeBase (class in evennia.contrib.game_systems.crafting.crafting)

msg() (evennia.contrib.game_systems.crafting.crafting.CraftingRecipeBase method)

NonExistentRecipe (class in evennia.contrib.game_systems.crafting.crafting)

CraftingRecipe (class in evennia.contrib.game_systems.crafting.crafting)

name (evennia.contrib.game_systems.crafting.crafting.CraftingRecipe attribute)

seed() (evennia.contrib.game_systems.crafting.crafting.CraftingRecipe class method)

craft() (in module evennia.contrib.game_systems.crafting.crafting)

CraftingCmdSet (class in evennia.contrib.game_systems.crafting.crafting)

key (evennia.contrib.game_systems.crafting.crafting.CraftingCmdSet attribute)

path (evennia.contrib.game_systems.crafting.crafting.CraftingCmdSet attribute)

CmdCraft (class in evennia.contrib.game_systems.crafting.crafting)

key (evennia.contrib.game_systems.crafting.crafting.CmdCraft attribute)

locks (evennia.contrib.game_systems.crafting.crafting.CmdCraft attribute)

aliases (evennia.contrib.game_systems.crafting.crafting.CmdCraft attribute)

arg_regex (evennia.contrib.game_systems.crafting.crafting.CmdCraft attribute)

lock_storage (evennia.contrib.game_systems.crafting.crafting.CmdCraft attribute)

parse() (evennia.contrib.game_systems.crafting.crafting.CmdCraft method)

func() (evennia.contrib.game_systems.crafting.crafting.CmdCraft method)

random() (in module evennia.contrib.game_systems.crafting.example_recipes)

PigIronRecipe (class in evennia.contrib.game_systems.crafting.example_recipes)

CrucibleSteelRecipe (class in evennia.contrib.game_systems.crafting.example_recipes)

SwordBladeRecipe (class in evennia.contrib.game_systems.crafting.example_recipes)

SwordPommelRecipe (class in evennia.contrib.game_systems.crafting.example_recipes)

SwordGuardRecipe (class in evennia.contrib.game_systems.crafting.example_recipes)

RawhideRecipe (class in evennia.contrib.game_systems.crafting.example_recipes)

OakBarkRecipe (class in evennia.contrib.game_systems.crafting.example_recipes)

LeatherRecipe (class in evennia.contrib.game_systems.crafting.example_recipes)

SwordHandleRecipe (class in evennia.contrib.game_systems.crafting.example_recipes)

SwordRecipe (class in evennia.contrib.game_systems.crafting.example_recipes)

FireballRecipe (class in evennia.contrib.game_systems.crafting.example_recipes)

HealingRecipe (class in evennia.contrib.game_systems.crafting.example_recipes)

CmdCast (class in evennia.contrib.game_systems.crafting.example_recipes)

key (evennia.contrib.game_systems.crafting.example_recipes.CmdCast attribute)

parse() (evennia.contrib.game_systems.crafting.example_recipes.CmdCast method)

func() (evennia.contrib.game_systems.crafting.example_recipes.CmdCast method)

TestCraftUtils (class in evennia.contrib.game_systems.crafting.tests)

maxDiff (evennia.contrib.game_systems.crafting.tests.TestCraftUtils attribute)

TestCraftingRecipeBase (class in evennia.contrib.game_systems.crafting.tests)

TestCraftingRecipe (class in evennia.contrib.game_systems.crafting.tests)

setUp() (evennia.contrib.game_systems.crafting.tests.TestCraftingRecipe method)

TestCraftSword (class in evennia.contrib.game_systems.crafting.tests)

setUp() (evennia.contrib.game_systems.crafting.tests.TestCraftSword method)

TestCraftCommand (class in evennia.contrib.game_systems.crafting.tests)

setUp() (evennia.contrib.game_systems.crafting.tests.TestCraftCommand method)

SetGender (class in evennia.contrib.game_systems.gendersub.gendersub)

key (evennia.contrib.game_systems.gendersub.gendersub.SetGender attribute)

aliases (evennia.contrib.game_systems.gendersub.gendersub.SetGender attribute)

locks (evennia.contrib.game_systems.gendersub.gendersub.SetGender attribute)

func() (evennia.contrib.game_systems.gendersub.gendersub.SetGender method)

GenderCharacter (class in evennia.contrib.game_systems.gendersub.gendersub)

msg() (evennia.contrib.game_systems.gendersub.gendersub.GenderCharacter method)

TestGenderSub (class in evennia.contrib.game_systems.gendersub.tests)

aliases (evennia.contrib.game_systems.mail.mail.CmdMail attribute)

help_category (evennia.contrib.game_systems.mail.mail.CmdMail attribute)

search_targets() (evennia.contrib.game_systems.mail.mail.CmdMail method)

get_all_mail() (evennia.contrib.game_systems.mail.mail.CmdMail method)

send_mail() (evennia.contrib.game_systems.mail.mail.CmdMail method)

lock_storage (evennia.contrib.game_systems.mail.mail.CmdMail attribute)

search_index_entry (evennia.contrib.game_systems.mail.mail.CmdMail attribute)

CmdMailCharacter (class in evennia.contrib.game_systems.mail.mail)

aliases (evennia.contrib.game_systems.mail.mail.CmdMailCharacter attribute)

key (evennia.contrib.game_systems.mail.mail.CmdMailCharacter attribute)

lock_storage (evennia.contrib.game_systems.mail.mail.CmdMailCharacter attribute)

test_mail() (evennia.contrib.game_systems.mail.tests.TestMail method)

CmdMultiDesc (class in evennia.contrib.game_systems.multidescer.multidescer)

TestMultidescer (class in evennia.contrib.game_systems.multidescer.tests)

proto_def() (in module evennia.contrib.game_systems.puzzles.puzzles)

maskout_protodef() (in module evennia.contrib.game_systems.puzzles.puzzles)

PuzzleRecipe (class in evennia.contrib.game_systems.puzzles.puzzles)

save_recipe() (evennia.contrib.game_systems.puzzles.puzzles.PuzzleRecipe method)

path (evennia.contrib.game_systems.puzzles.puzzles.PuzzleRecipe attribute)

typename (evennia.contrib.game_systems.puzzles.puzzles.PuzzleRecipe attribute)

CmdCreatePuzzleRecipe (class in evennia.contrib.game_systems.puzzles.puzzles)

CmdEditPuzzle (class in evennia.contrib.game_systems.puzzles.puzzles)

key (evennia.contrib.game_systems.puzzles.puzzles.CmdEditPuzzle attribute)

locks (evennia.contrib.game_systems.puzzles.puzzles.CmdEditPuzzle attribute)

func() (evennia.contrib.game_systems.puzzles.puzzles.CmdEditPuzzle method)

aliases (evennia.contrib.game_systems.puzzles.puzzles.CmdEditPuzzle attribute)

CmdArmPuzzle (class in evennia.contrib.game_systems.puzzles.puzzles)

key (evennia.contrib.game_systems.puzzles.puzzles.CmdArmPuzzle attribute)

locks (evennia.contrib.game_systems.puzzles.puzzles.CmdArmPuzzle attribute)

func() (evennia.contrib.game_systems.puzzles.puzzles.CmdArmPuzzle method)

aliases (evennia.contrib.game_systems.puzzles.puzzles.CmdArmPuzzle attribute)

CmdUsePuzzleParts (class in evennia.contrib.game_systems.puzzles.puzzles)

key (evennia.contrib.game_systems.puzzles.puzzles.CmdUsePuzzleParts attribute)

locks (evennia.contrib.game_systems.puzzles.puzzles.CmdUsePuzzleParts attribute)

func() (evennia.contrib.game_systems.puzzles.puzzles.CmdUsePuzzleParts method)

CmdListPuzzleRecipes (class in evennia.contrib.game_systems.puzzles.puzzles)

CmdListArmedPuzzles (class in evennia.contrib.game_systems.puzzles.puzzles)

key (evennia.contrib.game_systems.puzzles.puzzles.CmdListArmedPuzzles attribute)

func() (evennia.contrib.game_systems.puzzles.puzzles.CmdListArmedPuzzles method)

PuzzleSystemCmdSet (class in evennia.contrib.game_systems.puzzles.puzzles)

path (evennia.contrib.game_systems.puzzles.puzzles.PuzzleSystemCmdSet attribute)

TestPuzzles (class in evennia.contrib.game_systems.puzzles.tests)

setUp() (evennia.contrib.game_systems.puzzles.tests.TestPuzzles method)

test_cmd_use() (evennia.contrib.game_systems.puzzles.tests.TestPuzzles method)

test_e2e() (evennia.contrib.game_systems.puzzles.tests.TestPuzzles method)

ACTIONS_PER_TURN (in module evennia.contrib.game_systems.turnbattle.tb_basic)

BasicCombatRules (class in evennia.contrib.game_systems.turnbattle.tb_basic)

COMBAT_RULES (in module evennia.contrib.game_systems.turnbattle.tb_basic)

TBBasicCharacter (class in evennia.contrib.game_systems.turnbattle.tb_basic)

TBBasicTurnHandler (class in evennia.contrib.game_systems.turnbattle.tb_basic)

CmdFight (class in evennia.contrib.game_systems.turnbattle.tb_basic)

key (evennia.contrib.game_systems.turnbattle.tb_basic.CmdFight attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_basic.CmdFight attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_basic.CmdFight method)

aliases (evennia.contrib.game_systems.turnbattle.tb_basic.CmdFight attribute)

CmdAttack (class in evennia.contrib.game_systems.turnbattle.tb_basic)

key (evennia.contrib.game_systems.turnbattle.tb_basic.CmdAttack attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_basic.CmdAttack attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_basic.CmdAttack method)

aliases (evennia.contrib.game_systems.turnbattle.tb_basic.CmdAttack attribute)

CmdPass (class in evennia.contrib.game_systems.turnbattle.tb_basic)

key (evennia.contrib.game_systems.turnbattle.tb_basic.CmdPass attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_basic.CmdPass attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_basic.CmdPass attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_basic.CmdPass method)

CmdDisengage (class in evennia.contrib.game_systems.turnbattle.tb_basic)

key (evennia.contrib.game_systems.turnbattle.tb_basic.CmdDisengage attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_basic.CmdDisengage attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_basic.CmdDisengage method)

CmdRest (class in evennia.contrib.game_systems.turnbattle.tb_basic)

key (evennia.contrib.game_systems.turnbattle.tb_basic.CmdRest attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_basic.CmdRest attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_basic.CmdRest method)

aliases (evennia.contrib.game_systems.turnbattle.tb_basic.CmdRest attribute)

CmdCombatHelp (class in evennia.contrib.game_systems.turnbattle.tb_basic)

rules (evennia.contrib.game_systems.turnbattle.tb_basic.CmdCombatHelp attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_basic.CmdCombatHelp method)

key (evennia.contrib.game_systems.turnbattle.tb_basic.CmdCombatHelp attribute)

BattleCmdSet (class in evennia.contrib.game_systems.turnbattle.tb_basic)

key (evennia.contrib.game_systems.turnbattle.tb_basic.BattleCmdSet attribute)

path (evennia.contrib.game_systems.turnbattle.tb_basic.BattleCmdSet attribute)

ACTIONS_PER_TURN (in module evennia.contrib.game_systems.turnbattle.tb_equip)

EquipmentCombatRules (class in evennia.contrib.game_systems.turnbattle.tb_equip)

COMBAT_RULES (in module evennia.contrib.game_systems.turnbattle.tb_equip)

TBEquipTurnHandler (class in evennia.contrib.game_systems.turnbattle.tb_equip)

TBEWeapon (class in evennia.contrib.game_systems.turnbattle.tb_equip)

rules (evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon attribute)

at_drop() (evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon method)

at_give() (evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon method)

path (evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon attribute)

typename (evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon attribute)

TBEArmor (class in evennia.contrib.game_systems.turnbattle.tb_equip)

at_pre_drop() (evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor method)

at_drop() (evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor method)

at_pre_give() (evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor method)

at_give() (evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor method)

path (evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor attribute)

typename (evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor attribute)

TBEquipCharacter (class in evennia.contrib.game_systems.turnbattle.tb_equip)

CmdFight (class in evennia.contrib.game_systems.turnbattle.tb_equip)

key (evennia.contrib.game_systems.turnbattle.tb_equip.CmdFight attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_equip.CmdFight attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_equip.CmdFight attribute)

CmdAttack (class in evennia.contrib.game_systems.turnbattle.tb_equip)

key (evennia.contrib.game_systems.turnbattle.tb_equip.CmdAttack attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_equip.CmdAttack attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_equip.CmdAttack attribute)

CmdPass (class in evennia.contrib.game_systems.turnbattle.tb_equip)

key (evennia.contrib.game_systems.turnbattle.tb_equip.CmdPass attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_equip.CmdPass attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_equip.CmdPass attribute)

CmdDisengage (class in evennia.contrib.game_systems.turnbattle.tb_equip)

key (evennia.contrib.game_systems.turnbattle.tb_equip.CmdDisengage attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_equip.CmdDisengage attribute)

CmdRest (class in evennia.contrib.game_systems.turnbattle.tb_equip)

key (evennia.contrib.game_systems.turnbattle.tb_equip.CmdRest attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_equip.CmdRest attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_equip.CmdRest attribute)

CmdCombatHelp (class in evennia.contrib.game_systems.turnbattle.tb_equip)

rules (evennia.contrib.game_systems.turnbattle.tb_equip.CmdCombatHelp attribute)

key (evennia.contrib.game_systems.turnbattle.tb_equip.CmdCombatHelp attribute)

CmdWield (class in evennia.contrib.game_systems.turnbattle.tb_equip)

key (evennia.contrib.game_systems.turnbattle.tb_equip.CmdWield attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_equip.CmdWield attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_equip.CmdWield method)

aliases (evennia.contrib.game_systems.turnbattle.tb_equip.CmdWield attribute)

CmdUnwield (class in evennia.contrib.game_systems.turnbattle.tb_equip)

key (evennia.contrib.game_systems.turnbattle.tb_equip.CmdUnwield attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_equip.CmdUnwield attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_equip.CmdUnwield method)

aliases (evennia.contrib.game_systems.turnbattle.tb_equip.CmdUnwield attribute)

CmdDon (class in evennia.contrib.game_systems.turnbattle.tb_equip)

key (evennia.contrib.game_systems.turnbattle.tb_equip.CmdDon attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_equip.CmdDon attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_equip.CmdDon method)

aliases (evennia.contrib.game_systems.turnbattle.tb_equip.CmdDon attribute)

lock_storage (evennia.contrib.game_systems.turnbattle.tb_equip.CmdDon attribute)

CmdDoff (class in evennia.contrib.game_systems.turnbattle.tb_equip)

key (evennia.contrib.game_systems.turnbattle.tb_equip.CmdDoff attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_equip.CmdDoff attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_equip.CmdDoff method)

aliases (evennia.contrib.game_systems.turnbattle.tb_equip.CmdDoff attribute)

BattleCmdSet (class in evennia.contrib.game_systems.turnbattle.tb_equip)

key (evennia.contrib.game_systems.turnbattle.tb_equip.BattleCmdSet attribute)

path (evennia.contrib.game_systems.turnbattle.tb_equip.BattleCmdSet attribute)

DEF_DOWN_MOD (in module evennia.contrib.game_systems.turnbattle.tb_items)

ItemCombatRules (class in evennia.contrib.game_systems.turnbattle.tb_items)

ITEMFUNCS (in module evennia.contrib.game_systems.turnbattle.tb_items)

AMULET_OF_WEAKNESS (in module evennia.contrib.game_systems.turnbattle.tb_items)

TBItemsCharacter (class in evennia.contrib.game_systems.turnbattle.tb_items)

TBItemsCharacterTest (class in evennia.contrib.game_systems.turnbattle.tb_items)

TBItemsTurnHandler (class in evennia.contrib.game_systems.turnbattle.tb_items)

CmdFight (class in evennia.contrib.game_systems.turnbattle.tb_items)

key (evennia.contrib.game_systems.turnbattle.tb_items.CmdFight attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_items.CmdFight attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_items.CmdFight attribute)

CmdAttack (class in evennia.contrib.game_systems.turnbattle.tb_items)

key (evennia.contrib.game_systems.turnbattle.tb_items.CmdAttack attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_items.CmdAttack attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_items.CmdAttack attribute)

CmdPass (class in evennia.contrib.game_systems.turnbattle.tb_items)

key (evennia.contrib.game_systems.turnbattle.tb_items.CmdPass attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_items.CmdPass attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_items.CmdPass attribute)

CmdDisengage (class in evennia.contrib.game_systems.turnbattle.tb_items)

key (evennia.contrib.game_systems.turnbattle.tb_items.CmdDisengage attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_items.CmdDisengage attribute)

CmdRest (class in evennia.contrib.game_systems.turnbattle.tb_items)

key (evennia.contrib.game_systems.turnbattle.tb_items.CmdRest attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_items.CmdRest attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_items.CmdRest attribute)

CmdCombatHelp (class in evennia.contrib.game_systems.turnbattle.tb_items)

rules (evennia.contrib.game_systems.turnbattle.tb_items.CmdCombatHelp attribute)

key (evennia.contrib.game_systems.turnbattle.tb_items.CmdCombatHelp attribute)

CmdUse (class in evennia.contrib.game_systems.turnbattle.tb_items)

key (evennia.contrib.game_systems.turnbattle.tb_items.CmdUse attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_items.CmdUse attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_items.CmdUse method)

aliases (evennia.contrib.game_systems.turnbattle.tb_items.CmdUse attribute)

lock_storage (evennia.contrib.game_systems.turnbattle.tb_items.CmdUse attribute)

BattleCmdSet (class in evennia.contrib.game_systems.turnbattle.tb_items)

path (evennia.contrib.game_systems.turnbattle.tb_items.BattleCmdSet attribute)

key (evennia.contrib.game_systems.turnbattle.tb_items.BattleCmdSet attribute)

MagicCombatRules (class in evennia.contrib.game_systems.turnbattle.tb_magic)

COMBAT_RULES (in module evennia.contrib.game_systems.turnbattle.tb_magic)

SPELLS (in module evennia.contrib.game_systems.turnbattle.tb_magic)

ACTIONS_PER_TURN (in module evennia.contrib.game_systems.turnbattle.tb_magic)

TBMagicCharacter (class in evennia.contrib.game_systems.turnbattle.tb_magic)

TBMagicTurnHandler (class in evennia.contrib.game_systems.turnbattle.tb_magic)

CmdFight (class in evennia.contrib.game_systems.turnbattle.tb_magic)

key (evennia.contrib.game_systems.turnbattle.tb_magic.CmdFight attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_magic.CmdFight attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_magic.CmdFight attribute)

CmdAttack (class in evennia.contrib.game_systems.turnbattle.tb_magic)

key (evennia.contrib.game_systems.turnbattle.tb_magic.CmdAttack attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_magic.CmdAttack attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_magic.CmdAttack attribute)

CmdPass (class in evennia.contrib.game_systems.turnbattle.tb_magic)

key (evennia.contrib.game_systems.turnbattle.tb_magic.CmdPass attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_magic.CmdPass attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_magic.CmdPass attribute)

CmdDisengage (class in evennia.contrib.game_systems.turnbattle.tb_magic)

key (evennia.contrib.game_systems.turnbattle.tb_magic.CmdDisengage attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_magic.CmdDisengage attribute)

CmdLearnSpell (class in evennia.contrib.game_systems.turnbattle.tb_magic)

key (evennia.contrib.game_systems.turnbattle.tb_magic.CmdLearnSpell attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_magic.CmdLearnSpell method)

CmdCast (class in evennia.contrib.game_systems.turnbattle.tb_magic)

key (evennia.contrib.game_systems.turnbattle.tb_magic.CmdCast attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_magic.CmdCast attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_magic.CmdCast method)

aliases (evennia.contrib.game_systems.turnbattle.tb_magic.CmdCast attribute)

CmdRest (class in evennia.contrib.game_systems.turnbattle.tb_magic)

key (evennia.contrib.game_systems.turnbattle.tb_magic.CmdRest attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_magic.CmdRest attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_magic.CmdRest method)

aliases (evennia.contrib.game_systems.turnbattle.tb_magic.CmdRest attribute)

CmdStatus (class in evennia.contrib.game_systems.turnbattle.tb_magic)

key (evennia.contrib.game_systems.turnbattle.tb_magic.CmdStatus attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_magic.CmdStatus method)

aliases (evennia.contrib.game_systems.turnbattle.tb_magic.CmdStatus attribute)

CmdCombatHelp (class in evennia.contrib.game_systems.turnbattle.tb_magic)

key (evennia.contrib.game_systems.turnbattle.tb_magic.CmdCombatHelp attribute)

BattleCmdSet (class in evennia.contrib.game_systems.turnbattle.tb_magic)

path (evennia.contrib.game_systems.turnbattle.tb_magic.BattleCmdSet attribute)

key (evennia.contrib.game_systems.turnbattle.tb_magic.BattleCmdSet attribute)

ACTIONS_PER_TURN (in module evennia.contrib.game_systems.turnbattle.tb_range)

RangedCombatRules (class in evennia.contrib.game_systems.turnbattle.tb_range)

COMBAT_RULES (in module evennia.contrib.game_systems.turnbattle.tb_range)

TBRangeTurnHandler (class in evennia.contrib.game_systems.turnbattle.tb_range)

TBRangeCharacter (class in evennia.contrib.game_systems.turnbattle.tb_range)

TBRangeObject (class in evennia.contrib.game_systems.turnbattle.tb_range)

at_get() (evennia.contrib.game_systems.turnbattle.tb_range.TBRangeObject method)

path (evennia.contrib.game_systems.turnbattle.tb_range.TBRangeObject attribute)

CmdFight (class in evennia.contrib.game_systems.turnbattle.tb_range)

key (evennia.contrib.game_systems.turnbattle.tb_range.CmdFight attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_range.CmdFight attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_range.CmdFight attribute)

CmdAttack (class in evennia.contrib.game_systems.turnbattle.tb_range)

key (evennia.contrib.game_systems.turnbattle.tb_range.CmdAttack attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_range.CmdAttack attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_range.CmdAttack method)

aliases (evennia.contrib.game_systems.turnbattle.tb_range.CmdAttack attribute)

CmdShoot (class in evennia.contrib.game_systems.turnbattle.tb_range)

key (evennia.contrib.game_systems.turnbattle.tb_range.CmdShoot attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_range.CmdShoot attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_range.CmdShoot method)

aliases (evennia.contrib.game_systems.turnbattle.tb_range.CmdShoot attribute)

CmdApproach (class in evennia.contrib.game_systems.turnbattle.tb_range)

key (evennia.contrib.game_systems.turnbattle.tb_range.CmdApproach attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_range.CmdApproach attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_range.CmdApproach method)

aliases (evennia.contrib.game_systems.turnbattle.tb_range.CmdApproach attribute)

CmdWithdraw (class in evennia.contrib.game_systems.turnbattle.tb_range)

key (evennia.contrib.game_systems.turnbattle.tb_range.CmdWithdraw attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_range.CmdWithdraw attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_range.CmdWithdraw method)

aliases (evennia.contrib.game_systems.turnbattle.tb_range.CmdWithdraw attribute)

CmdPass (class in evennia.contrib.game_systems.turnbattle.tb_range)

key (evennia.contrib.game_systems.turnbattle.tb_range.CmdPass attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_range.CmdPass attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_range.CmdPass attribute)

CmdDisengage (class in evennia.contrib.game_systems.turnbattle.tb_range)

key (evennia.contrib.game_systems.turnbattle.tb_range.CmdDisengage attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_range.CmdDisengage attribute)

CmdRest (class in evennia.contrib.game_systems.turnbattle.tb_range)

key (evennia.contrib.game_systems.turnbattle.tb_range.CmdRest attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_range.CmdRest attribute)

aliases (evennia.contrib.game_systems.turnbattle.tb_range.CmdRest attribute)

CmdStatus (class in evennia.contrib.game_systems.turnbattle.tb_range)

key (evennia.contrib.game_systems.turnbattle.tb_range.CmdStatus attribute)

rules (evennia.contrib.game_systems.turnbattle.tb_range.CmdStatus attribute)

func() (evennia.contrib.game_systems.turnbattle.tb_range.CmdStatus method)

aliases (evennia.contrib.game_systems.turnbattle.tb_range.CmdStatus attribute)

CmdCombatHelp (class in evennia.contrib.game_systems.turnbattle.tb_range)

rules (evennia.contrib.game_systems.turnbattle.tb_range.CmdCombatHelp attribute)

key (evennia.contrib.game_systems.turnbattle.tb_range.CmdCombatHelp attribute)

BattleCmdSet (class in evennia.contrib.game_systems.turnbattle.tb_range)

key (evennia.contrib.game_systems.turnbattle.tb_range.BattleCmdSet attribute)

path (evennia.contrib.game_systems.turnbattle.tb_range.BattleCmdSet attribute)

TestTurnBattleBasicCmd (class in evennia.contrib.game_systems.turnbattle.tests)

TestTurnBattleEquipCmd (class in evennia.contrib.game_systems.turnbattle.tests)

TestTurnBattleRangeCmd (class in evennia.contrib.game_systems.turnbattle.tests)

TestTurnBattleItemsCmd (class in evennia.contrib.game_systems.turnbattle.tests)

TestTurnBattleMagicCmd (class in evennia.contrib.game_systems.turnbattle.tests)

TestTurnBattleBasicFunc (class in evennia.contrib.game_systems.turnbattle.tests)

TestTurnBattleEquipFunc (class in evennia.contrib.game_systems.turnbattle.tests)

TestTurnBattleRangeFunc (class in evennia.contrib.game_systems.turnbattle.tests)

TestTurnBattleItemsFunc (class in evennia.contrib.game_systems.turnbattle.tests)

TestTurnBattleMagicFunc (class in evennia.contrib.game_systems.turnbattle.tests)

func_state() (in module evennia.contrib.grid.extended_room.extended_room)

ExtendedRoom (class in evennia.contrib.grid.extended_room.extended_room)

desc (evennia.contrib.grid.extended_room.extended_room.ExtendedRoom attribute)

at_init() (evennia.contrib.grid.extended_room.extended_room.ExtendedRoom method)

path (evennia.contrib.grid.extended_room.extended_room.ExtendedRoom attribute)

CmdExtendedRoomLook (class in evennia.contrib.grid.extended_room.extended_room)

CmdExtendedRoomDesc (class in evennia.contrib.grid.extended_room.extended_room)

CmdExtendedRoomDetail (class in evennia.contrib.grid.extended_room.extended_room)

CmdExtendedRoomState (class in evennia.contrib.grid.extended_room.extended_room)

CmdExtendedRoomGameTime (class in evennia.contrib.grid.extended_room.extended_room)

ExtendedRoomCmdSet (class in evennia.contrib.grid.extended_room.extended_room)

TestExtendedRoom (class in evennia.contrib.grid.extended_room.tests)

setUp() (evennia.contrib.grid.extended_room.tests.TestExtendedRoom method)

tearDown() (evennia.contrib.grid.extended_room.tests.TestExtendedRoom method)

TestExtendedRoomCommands (class in evennia.contrib.grid.extended_room.tests)

Map (class in evennia.contrib.grid.ingame_map_display.ingame_map_display)

draw() (evennia.contrib.grid.ingame_map_display.ingame_map_display.Map method)

CmdMap (class in evennia.contrib.grid.ingame_map_display.ingame_map_display)

MapDisplayCmdSet (class in evennia.contrib.grid.ingame_map_display.ingame_map_display)

SimpleDoor (class in evennia.contrib.grid.simpledoor.simpledoor)

setlock() (evennia.contrib.grid.simpledoor.simpledoor.SimpleDoor method)

setdesc() (evennia.contrib.grid.simpledoor.simpledoor.SimpleDoor method)

delete() (evennia.contrib.grid.simpledoor.simpledoor.SimpleDoor method)

path (evennia.contrib.grid.simpledoor.simpledoor.SimpleDoor attribute)

typename (evennia.contrib.grid.simpledoor.simpledoor.SimpleDoor attribute)

create_exit() (evennia.contrib.grid.simpledoor.simpledoor.CmdOpen method)

aliases (evennia.contrib.grid.simpledoor.simpledoor.CmdOpen attribute)

help_category (evennia.contrib.grid.simpledoor.simpledoor.CmdOpen attribute)

key (evennia.contrib.grid.simpledoor.simpledoor.CmdOpen attribute)

lock_storage (evennia.contrib.grid.simpledoor.simpledoor.CmdOpen attribute)

CmdOpenCloseDoor (class in evennia.contrib.grid.simpledoor.simpledoor)

key (evennia.contrib.grid.simpledoor.simpledoor.CmdOpenCloseDoor attribute)

aliases (evennia.contrib.grid.simpledoor.simpledoor.CmdOpenCloseDoor attribute)

locks (evennia.contrib.grid.simpledoor.simpledoor.CmdOpenCloseDoor attribute)

func() (evennia.contrib.grid.simpledoor.simpledoor.CmdOpenCloseDoor method)

SimpleDoorCmdSet (class in evennia.contrib.grid.simpledoor.simpledoor)

path (evennia.contrib.grid.simpledoor.simpledoor.SimpleDoorCmdSet attribute)

test_cmdopen() (evennia.contrib.grid.simpledoor.tests.TestSimpleDoor method)

at_traverse() (evennia.contrib.grid.slow_exit.slow_exit.SlowExit method)

path (evennia.contrib.grid.slow_exit.slow_exit.SlowExit attribute)

typename (evennia.contrib.grid.slow_exit.slow_exit.SlowExit attribute)

key (evennia.contrib.grid.slow_exit.slow_exit.CmdSetSpeed attribute)

func() (evennia.contrib.grid.slow_exit.slow_exit.CmdSetSpeed method)

aliases (evennia.contrib.grid.slow_exit.slow_exit.CmdSetSpeed attribute)

help_category (evennia.contrib.grid.slow_exit.slow_exit.CmdSetSpeed attribute)

lock_storage (evennia.contrib.grid.slow_exit.slow_exit.CmdSetSpeed attribute)

key (evennia.contrib.grid.slow_exit.slow_exit.CmdStop attribute)

func() (evennia.contrib.grid.slow_exit.slow_exit.CmdStop method)

aliases (evennia.contrib.grid.slow_exit.slow_exit.CmdStop attribute)

help_category (evennia.contrib.grid.slow_exit.slow_exit.CmdStop attribute)

lock_storage (evennia.contrib.grid.slow_exit.slow_exit.CmdStop attribute)

search_index_entry (evennia.contrib.grid.slow_exit.slow_exit.CmdStop attribute)

SlowExitCmdSet (class in evennia.contrib.grid.slow_exit.slow_exit)

path (evennia.contrib.grid.slow_exit.slow_exit.SlowExitCmdSet attribute)

test_exit() (evennia.contrib.grid.slow_exit.tests.TestSlowExit method)

setUp() (evennia.contrib.grid.wilderness.tests.TestWilderness method)

create_wilderness() (in module evennia.contrib.grid.wilderness.wilderness)

enter_wilderness() (in module evennia.contrib.grid.wilderness.wilderness)

get_new_coordinates() (in module evennia.contrib.grid.wilderness.wilderness)

WildernessScript (class in evennia.contrib.grid.wilderness.wilderness)

move_obj() (evennia.contrib.grid.wilderness.wilderness.WildernessScript method)

path (evennia.contrib.grid.wilderness.wilderness.WildernessScript attribute)

typename (evennia.contrib.grid.wilderness.wilderness.WildernessScript attribute)

WildernessRoom (class in evennia.contrib.grid.wilderness.wilderness)

path (evennia.contrib.grid.wilderness.wilderness.WildernessRoom attribute)

typename (evennia.contrib.grid.wilderness.wilderness.WildernessRoom attribute)

WildernessExit (class in evennia.contrib.grid.wilderness.wilderness)

at_traverse() (evennia.contrib.grid.wilderness.wilderness.WildernessExit method)

path (evennia.contrib.grid.wilderness.wilderness.WildernessExit attribute)

typename (evennia.contrib.grid.wilderness.wilderness.WildernessExit attribute)

WildernessMapProvider (class in evennia.contrib.grid.wilderness.wilderness)

directions (evennia.contrib.grid.xyzgrid.commands.PathData attribute)

step_sequence (evennia.contrib.grid.xyzgrid.commands.PathData attribute)

target (evennia.contrib.grid.xyzgrid.commands.PathData attribute)

xymap (evennia.contrib.grid.xyzgrid.commands.PathData attribute)

parse() (evennia.contrib.grid.xyzgrid.commands.CmdXYZTeleport method)

aliases (evennia.contrib.grid.xyzgrid.commands.CmdXYZTeleport attribute)

help_category (evennia.contrib.grid.xyzgrid.commands.CmdXYZTeleport attribute)

key (evennia.contrib.grid.xyzgrid.commands.CmdXYZTeleport attribute)

lock_storage (evennia.contrib.grid.xyzgrid.commands.CmdXYZTeleport attribute)

parse() (evennia.contrib.grid.xyzgrid.commands.CmdXYZOpen method)

aliases (evennia.contrib.grid.xyzgrid.commands.CmdXYZOpen attribute)

help_category (evennia.contrib.grid.xyzgrid.commands.CmdXYZOpen attribute)

key (evennia.contrib.grid.xyzgrid.commands.CmdXYZOpen attribute)

lock_storage (evennia.contrib.grid.xyzgrid.commands.CmdXYZOpen attribute)

search_index_entry (evennia.contrib.grid.xyzgrid.commands.CmdXYZOpen attribute)

aliases (evennia.contrib.grid.xyzgrid.commands.CmdGoto attribute)

help_category (evennia.contrib.grid.xyzgrid.commands.CmdGoto attribute)

auto_step_delay (evennia.contrib.grid.xyzgrid.commands.CmdGoto attribute)

lock_storage (evennia.contrib.grid.xyzgrid.commands.CmdGoto attribute)

search_index_entry (evennia.contrib.grid.xyzgrid.commands.CmdGoto attribute)

aliases (evennia.contrib.grid.xyzgrid.commands.CmdMap attribute)

help_category (evennia.contrib.grid.xyzgrid.commands.CmdMap attribute)

lock_storage (evennia.contrib.grid.xyzgrid.commands.CmdMap attribute)

search_index_entry (evennia.contrib.grid.xyzgrid.commands.CmdMap attribute)

key (evennia.contrib.grid.xyzgrid.commands.XYZGridCmdSet attribute)

path (evennia.contrib.grid.xyzgrid.commands.XYZGridCmdSet attribute)

key (evennia.contrib.grid.xyzgrid.commands.CmdFlyAndDive attribute)

aliases (evennia.contrib.grid.xyzgrid.commands.CmdFlyAndDive attribute)

func() (evennia.contrib.grid.xyzgrid.commands.CmdFlyAndDive method)

help_category (evennia.contrib.grid.xyzgrid.commands.CmdFlyAndDive attribute)

lock_storage (evennia.contrib.grid.xyzgrid.commands.CmdFlyAndDive attribute)

XYZGridFlyDiveCmdSet (class in evennia.contrib.grid.xyzgrid.commands)

key (evennia.contrib.grid.xyzgrid.commands.XYZGridFlyDiveCmdSet attribute)

path (evennia.contrib.grid.xyzgrid.commands.XYZGridFlyDiveCmdSet attribute)

TransitionToCave (class in evennia.contrib.grid.xyzgrid.example)

symbol (evennia.contrib.grid.xyzgrid.example.TransitionToCave attribute)

target_map_xyz (evennia.contrib.grid.xyzgrid.example.TransitionToCave attribute)

TransitionToLargeTree (class in evennia.contrib.grid.xyzgrid.example)

symbol (evennia.contrib.grid.xyzgrid.example.TransitionToLargeTree attribute)

test_str_output() (evennia.contrib.grid.xyzgrid.tests.TestMap1 method)

test_node_from_coord() (evennia.contrib.grid.xyzgrid.tests.TestMap1 method)

test_get_shortest_path() (evennia.contrib.grid.xyzgrid.tests.TestMap1 method)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestMap1 method)

map_data (evennia.contrib.grid.xyzgrid.tests.TestMap2 attribute)

map_display (evennia.contrib.grid.xyzgrid.tests.TestMap2 attribute)

test_str_output() (evennia.contrib.grid.xyzgrid.tests.TestMap2 method)

test_node_from_coord() (evennia.contrib.grid.xyzgrid.tests.TestMap2 method)

test_shortest_path (evennia.contrib.grid.xyzgrid.tests.TestMap2 attribute)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestMap2 method)

test_shortest_path_0() (evennia.contrib.grid.xyzgrid.tests.TestMap2 method)

test_shortest_path_1() (evennia.contrib.grid.xyzgrid.tests.TestMap2 method)

test_shortest_path_2() (evennia.contrib.grid.xyzgrid.tests.TestMap2 method)

test_shortest_path_3() (evennia.contrib.grid.xyzgrid.tests.TestMap2 method)

test_shortest_path_4() (evennia.contrib.grid.xyzgrid.tests.TestMap2 method)

test_shortest_path_5() (evennia.contrib.grid.xyzgrid.tests.TestMap2 method)

test_shortest_path_6() (evennia.contrib.grid.xyzgrid.tests.TestMap2 method)

map_data (evennia.contrib.grid.xyzgrid.tests.TestMap3 attribute)

map_display (evennia.contrib.grid.xyzgrid.tests.TestMap3 attribute)

test_str_output() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

test_shortest_path (evennia.contrib.grid.xyzgrid.tests.TestMap3 attribute)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

test_shortest_path_00() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

test_shortest_path_01() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

test_shortest_path_02() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

test_shortest_path_03() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

test_shortest_path_04() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

test_shortest_path_05() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

test_shortest_path_06() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

test_shortest_path_07() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

test_shortest_path_08() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

test_shortest_path_09() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

test_shortest_path_10() (evennia.contrib.grid.xyzgrid.tests.TestMap3 method)

map_data (evennia.contrib.grid.xyzgrid.tests.TestMap4 attribute)

map_display (evennia.contrib.grid.xyzgrid.tests.TestMap4 attribute)

test_str_output() (evennia.contrib.grid.xyzgrid.tests.TestMap4 method)

test_shortest_path (evennia.contrib.grid.xyzgrid.tests.TestMap4 attribute)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestMap4 method)

test_shortest_path_0() (evennia.contrib.grid.xyzgrid.tests.TestMap4 method)

test_shortest_path_1() (evennia.contrib.grid.xyzgrid.tests.TestMap4 method)

test_shortest_path_2() (evennia.contrib.grid.xyzgrid.tests.TestMap4 method)

test_shortest_path_3() (evennia.contrib.grid.xyzgrid.tests.TestMap4 method)

test_shortest_path_4() (evennia.contrib.grid.xyzgrid.tests.TestMap4 method)

test_shortest_path_5() (evennia.contrib.grid.xyzgrid.tests.TestMap4 method)

map_data (evennia.contrib.grid.xyzgrid.tests.TestMap5 attribute)

map_display (evennia.contrib.grid.xyzgrid.tests.TestMap5 attribute)

test_str_output() (evennia.contrib.grid.xyzgrid.tests.TestMap5 method)

test_shortest_path (evennia.contrib.grid.xyzgrid.tests.TestMap5 attribute)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestMap5 method)

test_shortest_path_0() (evennia.contrib.grid.xyzgrid.tests.TestMap5 method)

test_shortest_path_1() (evennia.contrib.grid.xyzgrid.tests.TestMap5 method)

test_shortest_path_2() (evennia.contrib.grid.xyzgrid.tests.TestMap5 method)

test_shortest_path_3() (evennia.contrib.grid.xyzgrid.tests.TestMap5 method)

map_data (evennia.contrib.grid.xyzgrid.tests.TestMap6 attribute)

map_display (evennia.contrib.grid.xyzgrid.tests.TestMap6 attribute)

test_str_output() (evennia.contrib.grid.xyzgrid.tests.TestMap6 method)

test_shortest_path (evennia.contrib.grid.xyzgrid.tests.TestMap6 attribute)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestMap6 method)

test_shortest_path_0() (evennia.contrib.grid.xyzgrid.tests.TestMap6 method)

test_shortest_path_1() (evennia.contrib.grid.xyzgrid.tests.TestMap6 method)

test_shortest_path_2() (evennia.contrib.grid.xyzgrid.tests.TestMap6 method)

test_shortest_path_3() (evennia.contrib.grid.xyzgrid.tests.TestMap6 method)

test_shortest_path_4() (evennia.contrib.grid.xyzgrid.tests.TestMap6 method)

test_shortest_path_5() (evennia.contrib.grid.xyzgrid.tests.TestMap6 method)

test_shortest_path_6() (evennia.contrib.grid.xyzgrid.tests.TestMap6 method)

test_shortest_path_7() (evennia.contrib.grid.xyzgrid.tests.TestMap6 method)

map_data (evennia.contrib.grid.xyzgrid.tests.TestMap7 attribute)

map_display (evennia.contrib.grid.xyzgrid.tests.TestMap7 attribute)

test_str_output() (evennia.contrib.grid.xyzgrid.tests.TestMap7 method)

test_shortest_path (evennia.contrib.grid.xyzgrid.tests.TestMap7 attribute)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestMap7 method)

test_shortest_path_0() (evennia.contrib.grid.xyzgrid.tests.TestMap7 method)

test_shortest_path_1() (evennia.contrib.grid.xyzgrid.tests.TestMap7 method)

test_shortest_path_2() (evennia.contrib.grid.xyzgrid.tests.TestMap7 method)

test_shortest_path_3() (evennia.contrib.grid.xyzgrid.tests.TestMap7 method)

map_data (evennia.contrib.grid.xyzgrid.tests.TestMap8 attribute)

map_display (evennia.contrib.grid.xyzgrid.tests.TestMap8 attribute)

test_str_output() (evennia.contrib.grid.xyzgrid.tests.TestMap8 method)

test_shortest_path (evennia.contrib.grid.xyzgrid.tests.TestMap8 attribute)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestMap8 method)

test_shortest_path_0() (evennia.contrib.grid.xyzgrid.tests.TestMap8 method)

test_shortest_path_1() (evennia.contrib.grid.xyzgrid.tests.TestMap8 method)

test_shortest_path_2() (evennia.contrib.grid.xyzgrid.tests.TestMap8 method)

test_shortest_path_3() (evennia.contrib.grid.xyzgrid.tests.TestMap8 method)

test_shortest_path_4() (evennia.contrib.grid.xyzgrid.tests.TestMap8 method)

test_shortest_path_5() (evennia.contrib.grid.xyzgrid.tests.TestMap8 method)

test_shortest_path_6() (evennia.contrib.grid.xyzgrid.tests.TestMap8 method)

map_data (evennia.contrib.grid.xyzgrid.tests.TestMap9 attribute)

map_display (evennia.contrib.grid.xyzgrid.tests.TestMap9 attribute)

test_str_output() (evennia.contrib.grid.xyzgrid.tests.TestMap9 method)

test_shortest_path (evennia.contrib.grid.xyzgrid.tests.TestMap9 attribute)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestMap9 method)

test_shortest_path_0() (evennia.contrib.grid.xyzgrid.tests.TestMap9 method)

test_shortest_path_1() (evennia.contrib.grid.xyzgrid.tests.TestMap9 method)

test_shortest_path_2() (evennia.contrib.grid.xyzgrid.tests.TestMap9 method)

test_shortest_path_3() (evennia.contrib.grid.xyzgrid.tests.TestMap9 method)

map_data (evennia.contrib.grid.xyzgrid.tests.TestMap10 attribute)

map_display (evennia.contrib.grid.xyzgrid.tests.TestMap10 attribute)

test_str_output() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_shortest_path (evennia.contrib.grid.xyzgrid.tests.TestMap10 attribute)

test_paths (evennia.contrib.grid.xyzgrid.tests.TestMap10 attribute)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_paths_0() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_paths_1() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_shortest_path_0() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_shortest_path_1() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_shortest_path_2() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_shortest_path_3() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_shortest_path_4() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_shortest_path_5() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_shortest_path_6() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_shortest_path_7() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_shortest_path_8() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

test_shortest_path_9() (evennia.contrib.grid.xyzgrid.tests.TestMap10 method)

map_data (evennia.contrib.grid.xyzgrid.tests.TestMap11 attribute)

map_display (evennia.contrib.grid.xyzgrid.tests.TestMap11 attribute)

test_str_output() (evennia.contrib.grid.xyzgrid.tests.TestMap11 method)

test_shortest_path (evennia.contrib.grid.xyzgrid.tests.TestMap11 attribute)

test_paths (evennia.contrib.grid.xyzgrid.tests.TestMap11 attribute)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestMap11 method)

test_paths_0() (evennia.contrib.grid.xyzgrid.tests.TestMap11 method)

test_paths_1() (evennia.contrib.grid.xyzgrid.tests.TestMap11 method)

test_shortest_path_0() (evennia.contrib.grid.xyzgrid.tests.TestMap11 method)

test_shortest_path_1() (evennia.contrib.grid.xyzgrid.tests.TestMap11 method)

zcoord (evennia.contrib.grid.xyzgrid.tests.TestXYZGrid attribute)

tearDown() (evennia.contrib.grid.xyzgrid.tests.TestXYZGrid method)

test_str_output() (evennia.contrib.grid.xyzgrid.tests.TestXYZGrid method)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestXYZGrid method)

symbol (evennia.contrib.grid.xyzgrid.tests.Map12aTransition attribute)

target_map_xyz (evennia.contrib.grid.xyzgrid.tests.Map12aTransition attribute)

symbol (evennia.contrib.grid.xyzgrid.tests.Map12bTransition attribute)

target_map_xyz (evennia.contrib.grid.xyzgrid.tests.Map12bTransition attribute)

TestXYZGridTransition (class in evennia.contrib.grid.xyzgrid.tests)

setUp() (evennia.contrib.grid.xyzgrid.tests.TestXYZGridTransition method)

tearDown() (evennia.contrib.grid.xyzgrid.tests.TestXYZGridTransition method)

test_spawn() (evennia.contrib.grid.xyzgrid.tests.TestXYZGridTransition method)

TestBuildExampleGrid (class in evennia.contrib.grid.xyzgrid.tests)

setUp() (evennia.contrib.grid.xyzgrid.tests.TestBuildExampleGrid method)

tearDown() (evennia.contrib.grid.xyzgrid.tests.TestBuildExampleGrid method)

test_build() (evennia.contrib.grid.xyzgrid.tests.TestBuildExampleGrid method)

at_object_creation() (evennia.contrib.grid.xyzgrid.tests.TestXyzRoom method)

typename (evennia.contrib.grid.xyzgrid.tests.TestXyzRoom attribute)

at_object_creation() (evennia.contrib.grid.xyzgrid.tests.TestXyzExit method)

typename (evennia.contrib.grid.xyzgrid.tests.TestXyzExit attribute)

setUp() (evennia.contrib.grid.xyzgrid.tests.TestCallbacks method)

setup_grid() (evennia.contrib.grid.xyzgrid.tests.TestCallbacks method)

tearDown() (evennia.contrib.grid.xyzgrid.tests.TestCallbacks method)

TestFlyDiveCommand (class in evennia.contrib.grid.xyzgrid.tests)

setUp() (evennia.contrib.grid.xyzgrid.tests.TestFlyDiveCommand method)

tearDown() (evennia.contrib.grid.xyzgrid.tests.TestFlyDiveCommand method)

mapcorner_symbol (evennia.contrib.grid.xyzgrid.xymap.XYMap attribute)

max_pathfinding_length (evennia.contrib.grid.xyzgrid.xymap.XYMap attribute)

empty_symbol (evennia.contrib.grid.xyzgrid.xymap.XYMap attribute)

legend_key_exceptions (evennia.contrib.grid.xyzgrid.xymap.XYMap attribute)

calculate_path_matrix() (evennia.contrib.grid.xyzgrid.xymap.XYMap method)

get_node_from_coord() (evennia.contrib.grid.xyzgrid.xymap.XYMap method)

get_components_with_symbol() (evennia.contrib.grid.xyzgrid.xymap.XYMap method)

get_shortest_path() (evennia.contrib.grid.xyzgrid.xymap.XYMap method)

get_visual_range() (evennia.contrib.grid.xyzgrid.xymap.XYMap method)

display_symbol (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode attribute)

interrupt_path (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode attribute)

multilink (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode attribute)

__init__() (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode method)

node_index (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode attribute)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode attribute)

log() (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode method)

build_links() (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode method)

linkweights() (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode method)

get_display_symbol() (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode method)

get_spawn_xyz() (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode method)

get_exit_spawn_name() (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode method)

spawn() (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode method)

spawn_links() (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode method)

unspawn() (evennia.contrib.grid.xyzgrid.xymap_legend.MapNode method)

TransitionMapNode (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.TransitionMapNode attribute)

display_symbol (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink attribute)

default_weight (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink attribute)

directions (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink attribute)

direction_aliases (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink attribute)

weights (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink attribute)

multilink (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink attribute)

interrupt_path (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink attribute)

spawn_aliases (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink attribute)

__init__() (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink method)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink attribute)

traverse() (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink method)

at_empty_target() (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink method)

get_direction() (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink method)

get_weight() (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink method)

get_display_symbol() (evennia.contrib.grid.xyzgrid.xymap_legend.MapLink method)

SmartRerouterMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

SmartTeleporterMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

SmartMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

multilink (evennia.contrib.grid.xyzgrid.xymap_legend.SmartMapLink attribute)

get_direction() (evennia.contrib.grid.xyzgrid.xymap_legend.SmartMapLink method)

InvisibleSmartMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

BasicMapNode (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.BasicMapNode attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.BasicMapNode attribute)

InterruptMapNode (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.InterruptMapNode attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.InterruptMapNode attribute)

MapTransitionNode (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.MapTransitionNode attribute)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.NSMapLink attribute)

display_symbol (evennia.contrib.grid.xyzgrid.xymap_legend.NSMapLink attribute)

directions (evennia.contrib.grid.xyzgrid.xymap_legend.NSMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.NSMapLink attribute)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.EWMapLink attribute)

directions (evennia.contrib.grid.xyzgrid.xymap_legend.EWMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.EWMapLink attribute)

NESWMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.NESWMapLink attribute)

directions (evennia.contrib.grid.xyzgrid.xymap_legend.NESWMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.NESWMapLink attribute)

SENWMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.SENWMapLink attribute)

directions (evennia.contrib.grid.xyzgrid.xymap_legend.SENWMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.SENWMapLink attribute)

PlusMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.PlusMapLink attribute)

directions (evennia.contrib.grid.xyzgrid.xymap_legend.PlusMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.PlusMapLink attribute)

CrossMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.CrossMapLink attribute)

directions (evennia.contrib.grid.xyzgrid.xymap_legend.CrossMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.CrossMapLink attribute)

NSOneWayMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.NSOneWayMapLink attribute)

directions (evennia.contrib.grid.xyzgrid.xymap_legend.NSOneWayMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.NSOneWayMapLink attribute)

SNOneWayMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.SNOneWayMapLink attribute)

directions (evennia.contrib.grid.xyzgrid.xymap_legend.SNOneWayMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.SNOneWayMapLink attribute)

EWOneWayMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.EWOneWayMapLink attribute)

directions (evennia.contrib.grid.xyzgrid.xymap_legend.EWOneWayMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.EWOneWayMapLink attribute)

WEOneWayMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.WEOneWayMapLink attribute)

directions (evennia.contrib.grid.xyzgrid.xymap_legend.WEOneWayMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.WEOneWayMapLink attribute)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.UpMapLink attribute)

spawn_aliases (evennia.contrib.grid.xyzgrid.xymap_legend.UpMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.UpMapLink attribute)

DownMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.DownMapLink attribute)

spawn_aliases (evennia.contrib.grid.xyzgrid.xymap_legend.DownMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.DownMapLink attribute)

InterruptMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.InterruptMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.InterruptMapLink attribute)

BlockedMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.BlockedMapLink attribute)

weights (evennia.contrib.grid.xyzgrid.xymap_legend.BlockedMapLink attribute)

prototype (evennia.contrib.grid.xyzgrid.xymap_legend.BlockedMapLink attribute)

RouterMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.RouterMapLink attribute)

TeleporterMapLink (class in evennia.contrib.grid.xyzgrid.xymap_legend)

symbol (evennia.contrib.grid.xyzgrid.xymap_legend.TeleporterMapLink attribute)

at_script_creation() (evennia.contrib.grid.xyzgrid.xyzgrid.XYZGrid method)

all_maps() (evennia.contrib.grid.xyzgrid.xyzgrid.XYZGrid method)

get_room() (evennia.contrib.grid.xyzgrid.xyzgrid.XYZGrid method)

get_exit() (evennia.contrib.grid.xyzgrid.xyzgrid.XYZGrid method)

maps_from_module() (evennia.contrib.grid.xyzgrid.xyzgrid.XYZGrid method)

add_maps() (evennia.contrib.grid.xyzgrid.xyzgrid.XYZGrid method)

remove_map() (evennia.contrib.grid.xyzgrid.xyzgrid.XYZGrid method)

typename (evennia.contrib.grid.xyzgrid.xyzgrid.XYZGrid attribute)

filter_xyz() (evennia.contrib.grid.xyzgrid.xyzroom.XYZManager method)

get_xyz() (evennia.contrib.grid.xyzgrid.xyzroom.XYZManager method)

filter_xyz_exit() (evennia.contrib.grid.xyzgrid.xyzroom.XYZExitManager method)

get_xyz_exit() (evennia.contrib.grid.xyzgrid.xyzroom.XYZExitManager method)

objects (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom attribute)

map_display (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom attribute)

map_mode (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom attribute)

map_visual_range (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom attribute)

map_character_symbol (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom attribute)

map_align (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom attribute)

map_target_path_style (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom attribute)

map_fill_all (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom attribute)

map_separator_char (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom attribute)

xyzgrid() (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom property)

create() (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom class method)

get_display_name() (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom method)

return_appearance() (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom method)

typename (evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom attribute)

objects (evennia.contrib.grid.xyzgrid.xyzroom.XYZExit attribute)

xyzgrid() (evennia.contrib.grid.xyzgrid.xyzroom.XYZExit property)

xyz_destination() (evennia.contrib.grid.xyzgrid.xyzroom.XYZExit property)

create() (evennia.contrib.grid.xyzgrid.xyzroom.XYZExit class method)

typename (evennia.contrib.grid.xyzgrid.xyzroom.XYZExit attribute)

at_post_check() (evennia.contrib.rpg.buffs.buff.BaseBuff method)

autopause (evennia.contrib.rpg.buffs.buff.BuffHandler attribute)

buffcache() (evennia.contrib.rpg.buffs.buff.BuffHandler property)

playtime() (evennia.contrib.rpg.buffs.buff.BuffHandler property)

remove_by_type() (evennia.contrib.rpg.buffs.buff.BuffHandler method)

remove_by_stat() (evennia.contrib.rpg.buffs.buff.BuffHandler method)

remove_by_trigger() (evennia.contrib.rpg.buffs.buff.BuffHandler method)

remove_by_source() (evennia.contrib.rpg.buffs.buff.BuffHandler method)

remove_by_cachevalue() (evennia.contrib.rpg.buffs.buff.BuffHandler method)

get_by_type() (evennia.contrib.rpg.buffs.buff.BuffHandler method)

get_by_stat() (evennia.contrib.rpg.buffs.buff.BuffHandler method)

get_by_trigger() (evennia.contrib.rpg.buffs.buff.BuffHandler method)

get_by_source() (evennia.contrib.rpg.buffs.buff.BuffHandler method)

get_by_cachevalue() (evennia.contrib.rpg.buffs.buff.BuffHandler method)

view_modifiers() (evennia.contrib.rpg.buffs.buff.BuffHandler method)

at_get() (evennia.contrib.rpg.buffs.buff.BuffableProperty method)

search_index_entry (evennia.contrib.rpg.buffs.buff.CmdBuff attribute)

help_category (evennia.contrib.rpg.buffs.buff.CmdBuff attribute)

flavor (evennia.contrib.rpg.buffs.samplebuffs.Exploit attribute)

duration (evennia.contrib.rpg.buffs.samplebuffs.Exploit attribute)

maxstacks (evennia.contrib.rpg.buffs.samplebuffs.Exploit attribute)

triggers (evennia.contrib.rpg.buffs.samplebuffs.Exploit attribute)

stack_msg (evennia.contrib.rpg.buffs.samplebuffs.Exploit attribute)

conditional() (evennia.contrib.rpg.buffs.samplebuffs.Exploit method)

at_trigger() (evennia.contrib.rpg.buffs.samplebuffs.Exploit method)

name (evennia.contrib.rpg.buffs.samplebuffs.Exploited attribute)

flavor (evennia.contrib.rpg.buffs.samplebuffs.Exploited attribute)

duration (evennia.contrib.rpg.buffs.samplebuffs.Exploited attribute)

mods (evennia.contrib.rpg.buffs.samplebuffs.Exploited attribute)

at_post_check() (evennia.contrib.rpg.buffs.samplebuffs.Exploited method)

at_remove() (evennia.contrib.rpg.buffs.samplebuffs.Exploited method)

flavor (evennia.contrib.rpg.buffs.samplebuffs.Leeching attribute)

duration (evennia.contrib.rpg.buffs.samplebuffs.Leeching attribute)

triggers (evennia.contrib.rpg.buffs.samplebuffs.Leeching attribute)

at_trigger() (evennia.contrib.rpg.buffs.samplebuffs.Leeching method)

duration (evennia.contrib.rpg.buffs.samplebuffs.Poison attribute)

maxstacks (evennia.contrib.rpg.buffs.samplebuffs.Poison attribute)

tickrate (evennia.contrib.rpg.buffs.samplebuffs.Poison attribute)

playtime (evennia.contrib.rpg.buffs.samplebuffs.Poison attribute)

at_pause() (evennia.contrib.rpg.buffs.samplebuffs.Poison method)

at_unpause() (evennia.contrib.rpg.buffs.samplebuffs.Poison method)

duration (evennia.contrib.rpg.buffs.samplebuffs.Sated attribute)

maxstacks (evennia.contrib.rpg.buffs.samplebuffs.Sated attribute)

maxstacks (evennia.contrib.rpg.buffs.samplebuffs.StatBuff attribute)

refresh (evennia.contrib.rpg.buffs.samplebuffs.StatBuff attribute)

unique (evennia.contrib.rpg.buffs.samplebuffs.StatBuff attribute)

cache (evennia.contrib.rpg.buffs.samplebuffs.StatBuff attribute)

__init__() (evennia.contrib.rpg.buffs.samplebuffs.StatBuff method)

flavor (evennia.contrib.rpg.buffs.samplebuffs.StatBuff attribute)

stat1 (evennia.contrib.rpg.buffs.tests.BuffableObject attribute)

buffs (evennia.contrib.rpg.buffs.tests.BuffableObject attribute)

at_init() (evennia.contrib.rpg.buffs.tests.BuffableObject method)

typename (evennia.contrib.rpg.buffs.tests.BuffableObject attribute)

setUp() (evennia.contrib.rpg.buffs.tests.TestBuffsAndHandler method)

tearDown() (evennia.contrib.rpg.buffs.tests.TestBuffsAndHandler method)

test_addremove() (evennia.contrib.rpg.buffs.tests.TestBuffsAndHandler method)

test_getters() (evennia.contrib.rpg.buffs.tests.TestBuffsAndHandler method)

test_details() (evennia.contrib.rpg.buffs.tests.TestBuffsAndHandler method)

test_modify() (evennia.contrib.rpg.buffs.tests.TestBuffsAndHandler method)

test_trigger() (evennia.contrib.rpg.buffs.tests.TestBuffsAndHandler method)

test_complex() (evennia.contrib.rpg.buffs.tests.TestBuffsAndHandler method)

test_timing() (evennia.contrib.rpg.buffs.tests.TestBuffsAndHandler method)

test_stresstest() (evennia.contrib.rpg.buffs.tests.TestBuffsAndHandler method)

test_modgen() (evennia.contrib.rpg.buffs.tests.TestBuffsAndHandler method)

ContribCmdCharCreate (class in evennia.contrib.rpg.character_creator.character_creator)

ContribChargenAccount (class in evennia.contrib.rpg.character_creator.character_creator)

TestCharacterCreator (class in evennia.contrib.rpg.character_creator.tests)

search_index_entry (evennia.contrib.rpg.dice.dice.CmdDice attribute)

at_cmdset_creation() (evennia.contrib.rpg.dice.dice.DiceCmdSet method)

test_roll_dice() (evennia.contrib.rpg.dice.tests.TestDice method)

test_string_form() (evennia.contrib.rpg.dice.tests.TestDice method)

display_meter() (in module evennia.contrib.rpg.health_bar.health_bar)

test_healthbar() (evennia.contrib.rpg.health_bar.tests.TestHealthBar method)

__init__() (evennia.contrib.rpg.llm.llm_client.StringProducer method)

startProducing() (evennia.contrib.rpg.llm.llm_client.StringProducer method)

pauseProducing() (evennia.contrib.rpg.llm.llm_client.StringProducer method)

stopProducing() (evennia.contrib.rpg.llm.llm_client.StringProducer method)

SimpleResponseReceiver (class in evennia.contrib.rpg.llm.llm_client)

__init__() (evennia.contrib.rpg.llm.llm_client.SimpleResponseReceiver method)

QuietHTTP11ClientFactory (class in evennia.contrib.rpg.llm.llm_client)

noisy (evennia.contrib.rpg.llm.llm_client.QuietHTTP11ClientFactory attribute)

__init__() (evennia.contrib.rpg.llm.llm_client.LLMClient method)

get_response() (evennia.contrib.rpg.llm.llm_client.LLMClient method)

prompt_prefix (evennia.contrib.rpg.llm.llm_npc.LLMNPC attribute)

response_template (evennia.contrib.rpg.llm.llm_npc.LLMNPC attribute)

thinking_timeout (evennia.contrib.rpg.llm.llm_npc.LLMNPC attribute)

thinking_messages (evennia.contrib.rpg.llm.llm_npc.LLMNPC attribute)

max_chat_memory_size (evennia.contrib.rpg.llm.llm_npc.LLMNPC attribute)

llm_prompt_prefix() (evennia.contrib.rpg.llm.llm_npc.LLMNPC property)

help_category (evennia.contrib.rpg.llm.llm_npc.CmdLLMTalk attribute)

lock_storage (evennia.contrib.rpg.llm.llm_npc.CmdLLMTalk attribute)

search_index_entry (evennia.contrib.rpg.llm.llm_npc.CmdLLMTalk attribute)

test_npc_at_talked_to() (evennia.contrib.rpg.llm.tests.TestLLMClient method)

LanguageHandler (class in evennia.contrib.rpg.rpsystem.rplanguage)

add() (evennia.contrib.rpg.rpsystem.rplanguage.LanguageHandler method)

translate() (evennia.contrib.rpg.rpsystem.rplanguage.LanguageHandler method)

path (evennia.contrib.rpg.rpsystem.rplanguage.LanguageHandler attribute)

typename (evennia.contrib.rpg.rpsystem.rplanguage.LanguageHandler attribute)

obfuscate_language() (in module evennia.contrib.rpg.rpsystem.rplanguage)

add_language() (in module evennia.contrib.rpg.rpsystem.rplanguage)

available_languages() (in module evennia.contrib.rpg.rpsystem.rplanguage)

obfuscate_whisper() (in module evennia.contrib.rpg.rpsystem.rplanguage)

parse_language() (in module evennia.contrib.rpg.rpsystem.rpsystem)

parse_sdescs_and_recogs() (in module evennia.contrib.rpg.rpsystem.rpsystem)

__init__() (evennia.contrib.rpg.rpsystem.rpsystem.SdescHandler method)

add() (evennia.contrib.rpg.rpsystem.rpsystem.SdescHandler method)

clear() (evennia.contrib.rpg.rpsystem.rpsystem.SdescHandler method)

get() (evennia.contrib.rpg.rpsystem.rpsystem.SdescHandler method)

__init__() (evennia.contrib.rpg.rpsystem.rpsystem.RecogHandler method)

add() (evennia.contrib.rpg.rpsystem.rpsystem.RecogHandler method)

get() (evennia.contrib.rpg.rpsystem.rpsystem.RecogHandler method)

all() (evennia.contrib.rpg.rpsystem.rpsystem.RecogHandler method)

remove() (evennia.contrib.rpg.rpsystem.rpsystem.RecogHandler method)

parse() (evennia.contrib.rpg.rpsystem.rpsystem.RPCommand method)

aliases (evennia.contrib.rpg.rpsystem.rpsystem.RPCommand attribute)

help_category (evennia.contrib.rpg.rpsystem.rpsystem.RPCommand attribute)

lock_storage (evennia.contrib.rpg.rpsystem.rpsystem.RPCommand attribute)

search_index_entry (evennia.contrib.rpg.rpsystem.rpsystem.RPCommand attribute)

aliases (evennia.contrib.rpg.rpsystem.rpsystem.CmdEmote attribute)

locks (evennia.contrib.rpg.rpsystem.rpsystem.CmdEmote attribute)

arg_regex (evennia.contrib.rpg.rpsystem.rpsystem.CmdEmote attribute)

help_category (evennia.contrib.rpg.rpsystem.rpsystem.CmdEmote attribute)

lock_storage (evennia.contrib.rpg.rpsystem.rpsystem.CmdEmote attribute)

search_index_entry (evennia.contrib.rpg.rpsystem.rpsystem.CmdEmote attribute)

aliases (evennia.contrib.rpg.rpsystem.rpsystem.CmdSay attribute)

arg_regex (evennia.contrib.rpg.rpsystem.rpsystem.CmdSay attribute)

help_category (evennia.contrib.rpg.rpsystem.rpsystem.CmdSay attribute)

lock_storage (evennia.contrib.rpg.rpsystem.rpsystem.CmdSay attribute)

search_index_entry (evennia.contrib.rpg.rpsystem.rpsystem.CmdSay attribute)

locks (evennia.contrib.rpg.rpsystem.rpsystem.CmdSdesc attribute)

aliases (evennia.contrib.rpg.rpsystem.rpsystem.CmdSdesc attribute)

help_category (evennia.contrib.rpg.rpsystem.rpsystem.CmdSdesc attribute)

lock_storage (evennia.contrib.rpg.rpsystem.rpsystem.CmdSdesc attribute)

search_index_entry (evennia.contrib.rpg.rpsystem.rpsystem.CmdSdesc attribute)

aliases (evennia.contrib.rpg.rpsystem.rpsystem.CmdPose attribute)

help_category (evennia.contrib.rpg.rpsystem.rpsystem.CmdPose attribute)

lock_storage (evennia.contrib.rpg.rpsystem.rpsystem.CmdPose attribute)

search_index_entry (evennia.contrib.rpg.rpsystem.rpsystem.CmdPose attribute)

aliases (evennia.contrib.rpg.rpsystem.rpsystem.CmdRecog attribute)

help_category (evennia.contrib.rpg.rpsystem.rpsystem.CmdRecog attribute)

lock_storage (evennia.contrib.rpg.rpsystem.rpsystem.CmdRecog attribute)

search_index_entry (evennia.contrib.rpg.rpsystem.rpsystem.CmdRecog attribute)

aliases (evennia.contrib.rpg.rpsystem.rpsystem.CmdMask attribute)

help_category (evennia.contrib.rpg.rpsystem.rpsystem.CmdMask attribute)

lock_storage (evennia.contrib.rpg.rpsystem.rpsystem.CmdMask attribute)

search_index_entry (evennia.contrib.rpg.rpsystem.rpsystem.CmdMask attribute)

path (evennia.contrib.rpg.rpsystem.rpsystem.RPSystemCmdSet attribute)

ContribRPObject (class in evennia.contrib.rpg.rpsystem.rpsystem)

sdesc (evennia.contrib.rpg.rpsystem.rpsystem.ContribRPObject attribute)

search() (evennia.contrib.rpg.rpsystem.rpsystem.ContribRPObject method)

get_posed_sdesc() (evennia.contrib.rpg.rpsystem.rpsystem.ContribRPObject method)

path (evennia.contrib.rpg.rpsystem.rpsystem.ContribRPObject attribute)

typename (evennia.contrib.rpg.rpsystem.rpsystem.ContribRPObject attribute)

path (evennia.contrib.rpg.rpsystem.rpsystem.ContribRPRoom attribute)

typename (evennia.contrib.rpg.rpsystem.rpsystem.ContribRPRoom attribute)

ContribRPCharacter (class in evennia.contrib.rpg.rpsystem.rpsystem)

recog (evennia.contrib.rpg.rpsystem.rpsystem.ContribRPCharacter attribute)

at_pre_say() (evennia.contrib.rpg.rpsystem.rpsystem.ContribRPCharacter method)

get_sdesc() (evennia.contrib.rpg.rpsystem.rpsystem.ContribRPCharacter method)

path (evennia.contrib.rpg.rpsystem.rpsystem.ContribRPCharacter attribute)

typename (evennia.contrib.rpg.rpsystem.rpsystem.ContribRPCharacter attribute)

setUp() (evennia.contrib.rpg.rpsystem.tests.TestLanguage method)

tearDown() (evennia.contrib.rpg.rpsystem.tests.TestLanguage method)

test_faulty_language() (evennia.contrib.rpg.rpsystem.tests.TestLanguage method)

maxDiff (evennia.contrib.rpg.rpsystem.tests.TestRPSystem attribute)

setUp() (evennia.contrib.rpg.rpsystem.tests.TestRPSystem method)

test_posed_contents() (evennia.contrib.rpg.rpsystem.tests.TestRPSystem method)

test_sdesc_handler() (evennia.contrib.rpg.rpsystem.tests.TestRPSystem method)

test_recog_handler() (evennia.contrib.rpg.rpsystem.tests.TestRPSystem method)

test_parse_language() (evennia.contrib.rpg.rpsystem.tests.TestRPSystem method)

test_get_sdesc() (evennia.contrib.rpg.rpsystem.tests.TestRPSystem method)

test_send_emote() (evennia.contrib.rpg.rpsystem.tests.TestRPSystem method)

test_rpsearch() (evennia.contrib.rpg.rpsystem.tests.TestRPSystem method)

TestRPSystemCommands (class in evennia.contrib.rpg.rpsystem.tests)

setUp() (evennia.contrib.rpg.rpsystem.tests.TestRPSystemCommands method)

test_commands() (evennia.contrib.rpg.rpsystem.tests.TestRPSystemCommands method)

setUp() (evennia.contrib.rpg.traits.tests.TraitHandlerTest method)

test_add_trait() (evennia.contrib.rpg.traits.tests.TraitHandlerTest method)

test_cache() (evennia.contrib.rpg.traits.tests.TraitHandlerTest method)

test_setting() (evennia.contrib.rpg.traits.tests.TraitHandlerTest method)

test_getting() (evennia.contrib.rpg.traits.tests.TraitHandlerTest method)

test_all() (evennia.contrib.rpg.traits.tests.TraitHandlerTest method)

test_remove() (evennia.contrib.rpg.traits.tests.TraitHandlerTest method)

test_clear() (evennia.contrib.rpg.traits.tests.TraitHandlerTest method)

test_validate_input__valid() (evennia.contrib.rpg.traits.tests.TestTrait method)

test_validate_input__fail() (evennia.contrib.rpg.traits.tests.TestTrait method)

test_trait_getset() (evennia.contrib.rpg.traits.tests.TestTrait method)

setUp() (evennia.contrib.rpg.traits.tests.TestTraitStatic method)

test_init() (evennia.contrib.rpg.traits.tests.TestTraitStatic method)

test_value() (evennia.contrib.rpg.traits.tests.TestTraitStatic method)

test_delete() (evennia.contrib.rpg.traits.tests.TestTraitStatic method)

setUp() (evennia.contrib.rpg.traits.tests.TestTraitCounter method)

test_init() (evennia.contrib.rpg.traits.tests.TestTraitCounter method)

test_value() (evennia.contrib.rpg.traits.tests.TestTraitCounter method)

test_current() (evennia.contrib.rpg.traits.tests.TestTraitCounter method)

test_delete() (evennia.contrib.rpg.traits.tests.TestTraitCounter method)

test_percentage() (evennia.contrib.rpg.traits.tests.TestTraitCounter method)

test_descs() (evennia.contrib.rpg.traits.tests.TestTraitCounter method)

TestTraitCounterTimed (class in evennia.contrib.rpg.traits.tests)

setUp() (evennia.contrib.rpg.traits.tests.TestTraitCounterTimed method)

setUp() (evennia.contrib.rpg.traits.tests.TestTraitGauge method)

test_init() (evennia.contrib.rpg.traits.tests.TestTraitGauge method)

test_value() (evennia.contrib.rpg.traits.tests.TestTraitGauge method)

test_current() (evennia.contrib.rpg.traits.tests.TestTraitGauge method)

test_delete() (evennia.contrib.rpg.traits.tests.TestTraitGauge method)

test_percentage() (evennia.contrib.rpg.traits.tests.TestTraitGauge method)

test_descs() (evennia.contrib.rpg.traits.tests.TestTraitGauge method)

setUp() (evennia.contrib.rpg.traits.tests.TestTraitGaugeTimed method)

test_timer_rate() (evennia.contrib.rpg.traits.tests.TestTraitGaugeTimed method)

TestNumericTraitOperators (class in evennia.contrib.rpg.traits.tests)

setUp() (evennia.contrib.rpg.traits.tests.TestNumericTraitOperators method)

tearDown() (evennia.contrib.rpg.traits.tests.TestNumericTraitOperators method)

strength (evennia.contrib.rpg.traits.tests.DummyCharacter attribute)

hunting (evennia.contrib.rpg.traits.tests.DummyCharacter attribute)

health (evennia.contrib.rpg.traits.tests.DummyCharacter attribute)

test_traitfields() (evennia.contrib.rpg.traits.tests.TestTraitFields method)

TraitContribTestingChar (class in evennia.contrib.rpg.traits.tests)

HP (evennia.contrib.rpg.traits.tests.TraitContribTestingChar attribute)

path (evennia.contrib.rpg.traits.tests.TraitContribTestingChar attribute)

typename (evennia.contrib.rpg.traits.tests.TraitContribTestingChar attribute)

TraitPropertyTestCase (class in evennia.contrib.rpg.traits.tests)

test_round1() (evennia.contrib.rpg.traits.tests.TraitPropertyTestCase method)

test_round2() (evennia.contrib.rpg.traits.tests.TraitPropertyTestCase method)

__init__() (evennia.contrib.rpg.traits.traits.TraitException method)

__init__() (evennia.contrib.rpg.traits.traits.TraitHandler method)

remove() (evennia.contrib.rpg.traits.traits.TraitHandler method)

__init__() (evennia.contrib.rpg.traits.traits.TraitProperty method)

default_keys (evennia.contrib.rpg.traits.traits.Trait attribute)

allow_extra_properties (evennia.contrib.rpg.traits.traits.Trait attribute)

validate_input() (evennia.contrib.rpg.traits.traits.Trait static method)

trait_type (evennia.contrib.rpg.traits.traits.StaticTrait attribute)

default_keys (evennia.contrib.rpg.traits.traits.StaticTrait attribute)

value() (evennia.contrib.rpg.traits.traits.StaticTrait property)

trait_type (evennia.contrib.rpg.traits.traits.CounterTrait attribute)

default_keys (evennia.contrib.rpg.traits.traits.CounterTrait attribute)

validate_input() (evennia.contrib.rpg.traits.traits.CounterTrait static method)

base() (evennia.contrib.rpg.traits.traits.CounterTrait property)

mult() (evennia.contrib.rpg.traits.traits.CounterTrait property)

current() (evennia.contrib.rpg.traits.traits.CounterTrait property)

value() (evennia.contrib.rpg.traits.traits.CounterTrait property)

ratetarget() (evennia.contrib.rpg.traits.traits.CounterTrait property)

percent() (evennia.contrib.rpg.traits.traits.CounterTrait method)

trait_type (evennia.contrib.rpg.traits.traits.GaugeTrait attribute)

default_keys (evennia.contrib.rpg.traits.traits.GaugeTrait attribute)

current() (evennia.contrib.rpg.traits.traits.GaugeTrait property)

BodyFunctions (class in evennia.contrib.tutorials.bodyfunctions.bodyfunctions)

TestBodyFunctions (class in evennia.contrib.tutorials.bodyfunctions.tests)

setUp() (evennia.contrib.tutorials.bodyfunctions.tests.TestBodyFunctions method)

__init__() (evennia.contrib.tutorials.evadventure.ai.AIHandler method)

state() (evennia.contrib.tutorials.evadventure.ai.AIHandler property)

states() (evennia.contrib.tutorials.evadventure.ai.AIHandler property)

transitions() (evennia.contrib.tutorials.evadventure.ai.AIHandler property)

add_aidict() (evennia.contrib.tutorials.evadventure.ai.AIHandler method)

get_next_state() (evennia.contrib.tutorials.evadventure.ai.AIHandler method)

get_next_action() (evennia.contrib.tutorials.evadventure.ai.AIHandler method)

execute_ai() (evennia.contrib.tutorials.evadventure.ai.AIHandler method)

LivingMixin (class in evennia.contrib.tutorials.evadventure.characters)

is_pc (evennia.contrib.tutorials.evadventure.characters.LivingMixin attribute)

heal() (evennia.contrib.tutorials.evadventure.characters.LivingMixin method)

at_death() (evennia.contrib.tutorials.evadventure.characters.LivingMixin method)

at_pay() (evennia.contrib.tutorials.evadventure.characters.LivingMixin method)

pre_loot() (evennia.contrib.tutorials.evadventure.characters.LivingMixin method)

EvAdventureCharacter (class in evennia.contrib.tutorials.evadventure.characters)

get_character_sheet() (in module evennia.contrib.tutorials.evadventure.characters)

TemporaryCharacterSheet (class in evennia.contrib.tutorials.evadventure.chargen)

node_chargen() (in module evennia.contrib.tutorials.evadventure.chargen)

node_change_name() (in module evennia.contrib.tutorials.evadventure.chargen)

node_swap_abilities() (in module evennia.contrib.tutorials.evadventure.chargen)

node_apply_character() (in module evennia.contrib.tutorials.evadventure.chargen)

start_chargen() (in module evennia.contrib.tutorials.evadventure.chargen)

CombatAction (class in evennia.contrib.tutorials.evadventure.combat_base)

msg() (evennia.contrib.tutorials.evadventure.combat_base.CombatAction method)

CombatActionHold (class in evennia.contrib.tutorials.evadventure.combat_base)

CombatActionAttack (class in evennia.contrib.tutorials.evadventure.combat_base)

CombatActionStunt (class in evennia.contrib.tutorials.evadventure.combat_base)

CombatActionUseItem (class in evennia.contrib.tutorials.evadventure.combat_base)

CombatActionWield (class in evennia.contrib.tutorials.evadventure.combat_base)

EvAdventureCombatBaseHandler (class in evennia.contrib.tutorials.evadventure.combat_base)

CombatActionFlee (class in evennia.contrib.tutorials.evadventure.combat_turnbased)

EvAdventureTurnbasedCombatHandler (class in evennia.contrib.tutorials.evadventure.combat_turnbased)

node_choose_enemy_target() (in module evennia.contrib.tutorials.evadventure.combat_turnbased)

node_choose_enemy_recipient() (in module evennia.contrib.tutorials.evadventure.combat_turnbased)

node_choose_allied_target() (in module evennia.contrib.tutorials.evadventure.combat_turnbased)

node_choose_allied_recipient() (in module evennia.contrib.tutorials.evadventure.combat_turnbased)

node_choose_ability() (in module evennia.contrib.tutorials.evadventure.combat_turnbased)

node_choose_use_item() (in module evennia.contrib.tutorials.evadventure.combat_turnbased)

node_choose_wield_item() (in module evennia.contrib.tutorials.evadventure.combat_turnbased)

node_combat() (in module evennia.contrib.tutorials.evadventure.combat_turnbased)

CmdTurnAttack (class in evennia.contrib.tutorials.evadventure.combat_turnbased)

TurnCombatCmdSet (class in evennia.contrib.tutorials.evadventure.combat_turnbased)

EvAdventureCombatTwitchHandler (class in evennia.contrib.tutorials.evadventure.combat_twitch)

CmdAttack (class in evennia.contrib.tutorials.evadventure.combat_twitch)

key (evennia.contrib.tutorials.evadventure.combat_twitch.CmdAttack attribute)

func() (evennia.contrib.tutorials.evadventure.combat_twitch.CmdAttack method)

CmdLook (class in evennia.contrib.tutorials.evadventure.combat_twitch)

func() (evennia.contrib.tutorials.evadventure.combat_twitch.CmdLook method)

aliases (evennia.contrib.tutorials.evadventure.combat_twitch.CmdLook attribute)

key (evennia.contrib.tutorials.evadventure.combat_twitch.CmdLook attribute)

CmdHold (class in evennia.contrib.tutorials.evadventure.combat_twitch)

key (evennia.contrib.tutorials.evadventure.combat_twitch.CmdHold attribute)

func() (evennia.contrib.tutorials.evadventure.combat_twitch.CmdHold method)

aliases (evennia.contrib.tutorials.evadventure.combat_twitch.CmdHold attribute)

CmdStunt (class in evennia.contrib.tutorials.evadventure.combat_twitch)

key (evennia.contrib.tutorials.evadventure.combat_twitch.CmdStunt attribute)

aliases (evennia.contrib.tutorials.evadventure.combat_twitch.CmdStunt attribute)

parse() (evennia.contrib.tutorials.evadventure.combat_twitch.CmdStunt method)

func() (evennia.contrib.tutorials.evadventure.combat_twitch.CmdStunt method)

CmdUseItem (class in evennia.contrib.tutorials.evadventure.combat_twitch)

key (evennia.contrib.tutorials.evadventure.combat_twitch.CmdUseItem attribute)

parse() (evennia.contrib.tutorials.evadventure.combat_twitch.CmdUseItem method)

func() (evennia.contrib.tutorials.evadventure.combat_twitch.CmdUseItem method)

CmdWield (class in evennia.contrib.tutorials.evadventure.combat_twitch)

key (evennia.contrib.tutorials.evadventure.combat_twitch.CmdWield attribute)

parse() (evennia.contrib.tutorials.evadventure.combat_twitch.CmdWield method)

func() (evennia.contrib.tutorials.evadventure.combat_twitch.CmdWield method)

aliases (evennia.contrib.tutorials.evadventure.combat_twitch.CmdWield attribute)

TwitchCombatCmdSet (class in evennia.contrib.tutorials.evadventure.combat_twitch)

TwitchLookCmdSet (class in evennia.contrib.tutorials.evadventure.combat_twitch)

EvAdventureCommand (class in evennia.contrib.tutorials.evadventure.commands)

CmdInventory (class in evennia.contrib.tutorials.evadventure.commands)

key (evennia.contrib.tutorials.evadventure.commands.CmdInventory attribute)

aliases (evennia.contrib.tutorials.evadventure.commands.CmdInventory attribute)

func() (evennia.contrib.tutorials.evadventure.commands.CmdInventory method)

CmdWieldOrWear (class in evennia.contrib.tutorials.evadventure.commands)

key (evennia.contrib.tutorials.evadventure.commands.CmdWieldOrWear attribute)

func() (evennia.contrib.tutorials.evadventure.commands.CmdWieldOrWear method)

CmdRemove (class in evennia.contrib.tutorials.evadventure.commands)

key (evennia.contrib.tutorials.evadventure.commands.CmdRemove attribute)

aliases (evennia.contrib.tutorials.evadventure.commands.CmdRemove attribute)

func() (evennia.contrib.tutorials.evadventure.commands.CmdRemove method)

node_give() (in module evennia.contrib.tutorials.evadventure.commands)

node_receive() (in module evennia.contrib.tutorials.evadventure.commands)

node_end() (in module evennia.contrib.tutorials.evadventure.commands)

CmdGive (class in evennia.contrib.tutorials.evadventure.commands)

key (evennia.contrib.tutorials.evadventure.commands.CmdGive attribute)

parse() (evennia.contrib.tutorials.evadventure.commands.CmdGive method)

func() (evennia.contrib.tutorials.evadventure.commands.CmdGive method)

aliases (evennia.contrib.tutorials.evadventure.commands.CmdGive attribute)

help_category (evennia.contrib.tutorials.evadventure.commands.CmdGive attribute)

lock_storage (evennia.contrib.tutorials.evadventure.commands.CmdGive attribute)

CmdTalk (class in evennia.contrib.tutorials.evadventure.commands)

key (evennia.contrib.tutorials.evadventure.commands.CmdTalk attribute)

func() (evennia.contrib.tutorials.evadventure.commands.CmdTalk method)

aliases (evennia.contrib.tutorials.evadventure.commands.CmdTalk attribute)

help_category (evennia.contrib.tutorials.evadventure.commands.CmdTalk attribute)

lock_storage (evennia.contrib.tutorials.evadventure.commands.CmdTalk attribute)

EvAdventureCmdSet (class in evennia.contrib.tutorials.evadventure.commands)

key (evennia.contrib.tutorials.evadventure.commands.EvAdventureCmdSet attribute)

random() (in module evennia.contrib.tutorials.evadventure.dungeon)

EvAdventureDungeonRoom (class in evennia.contrib.tutorials.evadventure.dungeon)

EvAdventureDungeonExit (class in evennia.contrib.tutorials.evadventure.dungeon)

room_generator() (in module evennia.contrib.tutorials.evadventure.dungeon)

EvAdventureDungeonOrchestrator (class in evennia.contrib.tutorials.evadventure.dungeon)

EvAdventureDungeonStartRoomExit (class in evennia.contrib.tutorials.evadventure.dungeon)

EvAdventureStartRoomResetter (class in evennia.contrib.tutorials.evadventure.dungeon)

EvAdventureDungeonBranchDeleter (class in evennia.contrib.tutorials.evadventure.dungeon)

EvAdventureDungeonStartRoom (class in evennia.contrib.tutorials.evadventure.dungeon)

STR (evennia.contrib.tutorials.evadventure.enums.Ability attribute)

DEX (evennia.contrib.tutorials.evadventure.enums.Ability attribute)

CON (evennia.contrib.tutorials.evadventure.enums.Ability attribute)

INT (evennia.contrib.tutorials.evadventure.enums.Ability attribute)

WIS (evennia.contrib.tutorials.evadventure.enums.Ability attribute)

CHA (evennia.contrib.tutorials.evadventure.enums.Ability attribute)

ARMOR (evennia.contrib.tutorials.evadventure.enums.Ability attribute)

CRITICAL_FAILURE (evennia.contrib.tutorials.evadventure.enums.Ability attribute)

CRITICAL_SUCCESS (evennia.contrib.tutorials.evadventure.enums.Ability attribute)

WieldLocation (class in evennia.contrib.tutorials.evadventure.enums)

BACKPACK (evennia.contrib.tutorials.evadventure.enums.WieldLocation attribute)

TWO_HANDS (evennia.contrib.tutorials.evadventure.enums.WieldLocation attribute)

BODY (evennia.contrib.tutorials.evadventure.enums.WieldLocation attribute)

HEAD (evennia.contrib.tutorials.evadventure.enums.WieldLocation attribute)

WEAPON (evennia.contrib.tutorials.evadventure.enums.ObjType attribute)

ARMOR (evennia.contrib.tutorials.evadventure.enums.ObjType attribute)

SHIELD (evennia.contrib.tutorials.evadventure.enums.ObjType attribute)

HELMET (evennia.contrib.tutorials.evadventure.enums.ObjType attribute)

CONSUMABLE (evennia.contrib.tutorials.evadventure.enums.ObjType attribute)

GEAR (evennia.contrib.tutorials.evadventure.enums.ObjType attribute)

THROWABLE (evennia.contrib.tutorials.evadventure.enums.ObjType attribute)

MAGIC (evennia.contrib.tutorials.evadventure.enums.ObjType attribute)

QUEST (evennia.contrib.tutorials.evadventure.enums.ObjType attribute)

TREASURE (evennia.contrib.tutorials.evadventure.enums.ObjType attribute)

EquipmentHandler (class in evennia.contrib.tutorials.evadventure.equipment)

move() (evennia.contrib.tutorials.evadventure.equipment.EquipmentHandler method)

add() (evennia.contrib.tutorials.evadventure.equipment.EquipmentHandler method)

all() (evennia.contrib.tutorials.evadventure.equipment.EquipmentHandler method)

EvAdventureNPC (class in evennia.contrib.tutorials.evadventure.npcs)

is_pc (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC attribute)

hit_dice (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC attribute)

armor (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC attribute)

morale (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC attribute)

hp (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC attribute)

allegiance (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC attribute)

is_idle (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC attribute)

weapon (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC attribute)

coins (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC attribute)

group (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC attribute)

strength() (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC property)

dexterity() (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC property)

wisdom() (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC property)

charisma() (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC property)

hp_max() (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC property)

at_attacked() (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC method)

path (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC attribute)

typename (evennia.contrib.tutorials.evadventure.npcs.EvAdventureNPC attribute)

EvAdventureTalkativeNPC (class in evennia.contrib.tutorials.evadventure.npcs)

create() (evennia.contrib.tutorials.evadventure.npcs.EvAdventureTalkativeNPC class method)

node_start() (in module evennia.contrib.tutorials.evadventure.npcs)

EvAdventureQuestGiver (class in evennia.contrib.tutorials.evadventure.npcs)

EvAdventureShopKeeper (class in evennia.contrib.tutorials.evadventure.npcs)

EvAdventureMob (class in evennia.contrib.tutorials.evadventure.npcs)

at_defeat() (evennia.contrib.tutorials.evadventure.npcs.EvAdventureMob method)

at_do_loot() (evennia.contrib.tutorials.evadventure.npcs.EvAdventureMob method)

path (evennia.contrib.tutorials.evadventure.npcs.EvAdventureMob attribute)

typename (evennia.contrib.tutorials.evadventure.npcs.EvAdventureMob attribute)

EvAdventureObject (class in evennia.contrib.tutorials.evadventure.objects)

size (evennia.contrib.tutorials.evadventure.objects.EvAdventureObject attribute)

use() (evennia.contrib.tutorials.evadventure.objects.EvAdventureObject method)

path (evennia.contrib.tutorials.evadventure.objects.EvAdventureObject attribute)

EvAdventureObjectFiller (class in evennia.contrib.tutorials.evadventure.objects)

EvAdventureQuestObject (class in evennia.contrib.tutorials.evadventure.objects)

EvAdventureTreasure (class in evennia.contrib.tutorials.evadventure.objects)

EvAdventureConsumable (class in evennia.contrib.tutorials.evadventure.objects)

EvAdventureWeapon (class in evennia.contrib.tutorials.evadventure.objects)

use() (evennia.contrib.tutorials.evadventure.objects.EvAdventureWeapon method)

path (evennia.contrib.tutorials.evadventure.objects.EvAdventureWeapon attribute)

EvAdventureThrowable (class in evennia.contrib.tutorials.evadventure.objects)

EvAdventureRunestone (class in evennia.contrib.tutorials.evadventure.objects)

EvAdventureArmor (class in evennia.contrib.tutorials.evadventure.objects)

armor (evennia.contrib.tutorials.evadventure.objects.EvAdventureArmor attribute)

path (evennia.contrib.tutorials.evadventure.objects.EvAdventureArmor attribute)

EvAdventureShield (class in evennia.contrib.tutorials.evadventure.objects)

path (evennia.contrib.tutorials.evadventure.objects.EvAdventureShield attribute)

EvAdventureHelmet (class in evennia.contrib.tutorials.evadventure.objects)

path (evennia.contrib.tutorials.evadventure.objects.EvAdventureHelmet attribute)

WeaponBareHands (class in evennia.contrib.tutorials.evadventure.objects)

key (evennia.contrib.tutorials.evadventure.objects.WeaponBareHands attribute)

path (evennia.contrib.tutorials.evadventure.objects.WeaponBareHands attribute)

get_bare_hands() (in module evennia.contrib.tutorials.evadventure.objects)

EvAdventureQuest (class in evennia.contrib.tutorials.evadventure.quests)

key (evennia.contrib.tutorials.evadventure.quests.EvAdventureQuest attribute)

desc (evennia.contrib.tutorials.evadventure.quests.EvAdventureQuest attribute)

abandon() (evennia.contrib.tutorials.evadventure.quests.EvAdventureQuest method)

help() (evennia.contrib.tutorials.evadventure.quests.EvAdventureQuest method)

cleanup() (evennia.contrib.tutorials.evadventure.quests.EvAdventureQuest method)

EvAdventureQuestHandler (class in evennia.contrib.tutorials.evadventure.quests)

EvAdventureRoom (class in evennia.contrib.tutorials.evadventure.rooms)

path (evennia.contrib.tutorials.evadventure.rooms.EvAdventureRoom attribute)

typename (evennia.contrib.tutorials.evadventure.rooms.EvAdventureRoom attribute)

EvAdventurePvPRoom (class in evennia.contrib.tutorials.evadventure.rooms)

path (evennia.contrib.tutorials.evadventure.rooms.EvAdventurePvPRoom attribute)

EvAdventureRollEngine (class in evennia.contrib.tutorials.evadventure.rules)

key (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

desc (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

obj_type (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

size (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

value (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

use_slot (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

uses (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

quality (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

attack_type (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

defense_type (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

damage_roll (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

obj (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

prototype (evennia.contrib.tutorials.evadventure.shops.BuyItem attribute)

create_from_obj() (evennia.contrib.tutorials.evadventure.shops.BuyItem static method)

create_from_prototype() (evennia.contrib.tutorials.evadventure.shops.BuyItem static method)

get_detail() (evennia.contrib.tutorials.evadventure.shops.BuyItem method)

to_obj() (evennia.contrib.tutorials.evadventure.shops.BuyItem method)

__init__() (evennia.contrib.tutorials.evadventure.shops.BuyItem method)

node_confirm_buy() (in module evennia.contrib.tutorials.evadventure.shops)

node_confirm_sell() (in module evennia.contrib.tutorials.evadventure.shops)

EvAdventureMixin (class in evennia.contrib.tutorials.evadventure.tests.mixins)

TestCharacters (class in evennia.contrib.tutorials.evadventure.tests.test_characters)

EvAdventureCharacterGenerationTest (class in evennia.contrib.tutorials.evadventure.tests.test_chargen)

TestEvAdventureCombatBaseHandler (class in evennia.contrib.tutorials.evadventure.tests.test_combat)

TestCombatActionsBase (class in evennia.contrib.tutorials.evadventure.tests.test_combat)

EvAdventureTurnbasedCombatHandlerTest (class in evennia.contrib.tutorials.evadventure.tests.test_combat)

TestEvAdventureTwitchCombatHandler (class in evennia.contrib.tutorials.evadventure.tests.test_combat)

TestEvAdventureCommands (class in evennia.contrib.tutorials.evadventure.tests.test_commands)

TestDungeon (class in evennia.contrib.tutorials.evadventure.tests.test_dungeon)

TestEquipment (class in evennia.contrib.tutorials.evadventure.tests.test_equipment)

TestNPCBase (class in evennia.contrib.tutorials.evadventure.tests.test_npcs)

EvAdventureQuestTest (class in evennia.contrib.tutorials.evadventure.tests.test_quests)

EvAdventureRoomTest (class in evennia.contrib.tutorials.evadventure.tests.test_rooms)

EvAdventureRollEngineTest (class in evennia.contrib.tutorials.evadventure.tests.test_rules)

TestUtils (class in evennia.contrib.tutorials.evadventure.tests.test_utils)

get_obj_stats() (in module evennia.contrib.tutorials.evadventure.utils)

TutorialMirror (class in evennia.contrib.tutorials.mirror.mirror)

msg() (evennia.contrib.tutorials.mirror.mirror.TutorialMirror method)

path (evennia.contrib.tutorials.mirror.mirror.TutorialMirror attribute)

typename (evennia.contrib.tutorials.mirror.mirror.TutorialMirror attribute)

CmdPushLidClosed (class in evennia.contrib.tutorials.red_button.red_button)

key (evennia.contrib.tutorials.red_button.red_button.CmdPushLidClosed attribute)

func() (evennia.contrib.tutorials.red_button.red_button.CmdPushLidClosed method)

CmdNudge (class in evennia.contrib.tutorials.red_button.red_button)

key (evennia.contrib.tutorials.red_button.red_button.CmdNudge attribute)

aliases (evennia.contrib.tutorials.red_button.red_button.CmdNudge attribute)

locks (evennia.contrib.tutorials.red_button.red_button.CmdNudge attribute)

func() (evennia.contrib.tutorials.red_button.red_button.CmdNudge method)

CmdSmashGlass (class in evennia.contrib.tutorials.red_button.red_button)

key (evennia.contrib.tutorials.red_button.red_button.CmdSmashGlass attribute)

locks (evennia.contrib.tutorials.red_button.red_button.CmdSmashGlass attribute)

func() (evennia.contrib.tutorials.red_button.red_button.CmdSmashGlass method)

CmdOpenLid (class in evennia.contrib.tutorials.red_button.red_button)

key (evennia.contrib.tutorials.red_button.red_button.CmdOpenLid attribute)

aliases (evennia.contrib.tutorials.red_button.red_button.CmdOpenLid attribute)

locks (evennia.contrib.tutorials.red_button.red_button.CmdOpenLid attribute)

func() (evennia.contrib.tutorials.red_button.red_button.CmdOpenLid method)

LidClosedCmdSet (class in evennia.contrib.tutorials.red_button.red_button)

key (evennia.contrib.tutorials.red_button.red_button.LidClosedCmdSet attribute)

path (evennia.contrib.tutorials.red_button.red_button.LidClosedCmdSet attribute)

CmdPushLidOpen (class in evennia.contrib.tutorials.red_button.red_button)

key (evennia.contrib.tutorials.red_button.red_button.CmdPushLidOpen attribute)

locks (evennia.contrib.tutorials.red_button.red_button.CmdPushLidOpen attribute)

func() (evennia.contrib.tutorials.red_button.red_button.CmdPushLidOpen method)

CmdCloseLid (class in evennia.contrib.tutorials.red_button.red_button)

key (evennia.contrib.tutorials.red_button.red_button.CmdCloseLid attribute)

aliases (evennia.contrib.tutorials.red_button.red_button.CmdCloseLid attribute)

locks (evennia.contrib.tutorials.red_button.red_button.CmdCloseLid attribute)

func() (evennia.contrib.tutorials.red_button.red_button.CmdCloseLid method)

LidOpenCmdSet (class in evennia.contrib.tutorials.red_button.red_button)

key (evennia.contrib.tutorials.red_button.red_button.LidOpenCmdSet attribute)

path (evennia.contrib.tutorials.red_button.red_button.LidOpenCmdSet attribute)

CmdBlindLook (class in evennia.contrib.tutorials.red_button.red_button)

key (evennia.contrib.tutorials.red_button.red_button.CmdBlindLook attribute)

aliases (evennia.contrib.tutorials.red_button.red_button.CmdBlindLook attribute)

locks (evennia.contrib.tutorials.red_button.red_button.CmdBlindLook attribute)

func() (evennia.contrib.tutorials.red_button.red_button.CmdBlindLook method)

CmdBlindHelp (class in evennia.contrib.tutorials.red_button.red_button)

key (evennia.contrib.tutorials.red_button.red_button.CmdBlindHelp attribute)

aliases (evennia.contrib.tutorials.red_button.red_button.CmdBlindHelp attribute)

locks (evennia.contrib.tutorials.red_button.red_button.CmdBlindHelp attribute)

func() (evennia.contrib.tutorials.red_button.red_button.CmdBlindHelp method)

BlindCmdSet (class in evennia.contrib.tutorials.red_button.red_button)

key (evennia.contrib.tutorials.red_button.red_button.BlindCmdSet attribute)

no_exits (evennia.contrib.tutorials.red_button.red_button.BlindCmdSet attribute)

no_objs (evennia.contrib.tutorials.red_button.red_button.BlindCmdSet attribute)

path (evennia.contrib.tutorials.red_button.red_button.BlindCmdSet attribute)

RedButton (class in evennia.contrib.tutorials.red_button.red_button)

blink_msgs (evennia.contrib.tutorials.red_button.red_button.RedButton attribute)

break_lamp() (evennia.contrib.tutorials.red_button.red_button.RedButton method)

path (evennia.contrib.tutorials.red_button.red_button.RedButton attribute)

typename (evennia.contrib.tutorials.red_button.red_button.RedButton attribute)

menu_start_node() (in module evennia.contrib.tutorials.talking_npc.talking_npc)

info1() (in module evennia.contrib.tutorials.talking_npc.talking_npc)

info2() (in module evennia.contrib.tutorials.talking_npc.talking_npc)

info3() (in module evennia.contrib.tutorials.talking_npc.talking_npc)

END() (in module evennia.contrib.tutorials.talking_npc.talking_npc)

CmdTalk (class in evennia.contrib.tutorials.talking_npc.talking_npc)

key (evennia.contrib.tutorials.talking_npc.talking_npc.CmdTalk attribute)

locks (evennia.contrib.tutorials.talking_npc.talking_npc.CmdTalk attribute)

func() (evennia.contrib.tutorials.talking_npc.talking_npc.CmdTalk method)

aliases (evennia.contrib.tutorials.talking_npc.talking_npc.CmdTalk attribute)

TalkingCmdSet (class in evennia.contrib.tutorials.talking_npc.talking_npc)

key (evennia.contrib.tutorials.talking_npc.talking_npc.TalkingCmdSet attribute)

path (evennia.contrib.tutorials.talking_npc.talking_npc.TalkingCmdSet attribute)

TalkingNPC (class in evennia.contrib.tutorials.talking_npc.talking_npc)

path (evennia.contrib.tutorials.talking_npc.talking_npc.TalkingNPC attribute)

TestTalkingNPC (class in evennia.contrib.tutorials.talking_npc.tests)

do_nothing() (in module evennia.contrib.tutorials.tutorial_world.intro_menu)

send_testing_tagged() (in module evennia.contrib.tutorials.tutorial_world.intro_menu)

DemoCommandSetHelp (class in evennia.contrib.tutorials.tutorial_world.intro_menu)

goto_command_demo_help() (in module evennia.contrib.tutorials.tutorial_world.intro_menu)

DemoCommandSetComms (class in evennia.contrib.tutorials.tutorial_world.intro_menu)

goto_command_demo_comms() (in module evennia.contrib.tutorials.tutorial_world.intro_menu)

DemoCommandSetRoom (class in evennia.contrib.tutorials.tutorial_world.intro_menu)

goto_command_demo_room() (in module evennia.contrib.tutorials.tutorial_world.intro_menu)

goto_cleanup_cmdsets() (in module evennia.contrib.tutorials.tutorial_world.intro_menu)

TutorialEvMenu (class in evennia.contrib.tutorials.tutorial_world.intro_menu)

init_menu() (in module evennia.contrib.tutorials.tutorial_world.intro_menu)

CmdMobOnOff (class in evennia.contrib.tutorials.tutorial_world.mob)

key (evennia.contrib.tutorials.tutorial_world.mob.CmdMobOnOff attribute)

aliases (evennia.contrib.tutorials.tutorial_world.mob.CmdMobOnOff attribute)

locks (evennia.contrib.tutorials.tutorial_world.mob.CmdMobOnOff attribute)

func() (evennia.contrib.tutorials.tutorial_world.mob.CmdMobOnOff method)

MobCmdSet (class in evennia.contrib.tutorials.tutorial_world.mob)

path (evennia.contrib.tutorials.tutorial_world.mob.MobCmdSet attribute)

at_init() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

at_object_creation() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

set_alive() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

set_dead() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

start_idle() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

start_patrolling() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

start_hunting() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

start_attacking() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

do_patrol() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

do_hunting() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

do_attack() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

at_hit() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

at_new_arrival() (evennia.contrib.tutorials.tutorial_world.mob.Mob method)

path (evennia.contrib.tutorials.tutorial_world.mob.Mob attribute)

typename (evennia.contrib.tutorials.tutorial_world.mob.Mob attribute)

TutorialObject (class in evennia.contrib.tutorials.tutorial_world.objects)

reset() (evennia.contrib.tutorials.tutorial_world.objects.TutorialObject method)

path (evennia.contrib.tutorials.tutorial_world.objects.TutorialObject attribute)

CmdRead (class in evennia.contrib.tutorials.tutorial_world.objects)

key (evennia.contrib.tutorials.tutorial_world.objects.CmdRead attribute)

locks (evennia.contrib.tutorials.tutorial_world.objects.CmdRead attribute)

func() (evennia.contrib.tutorials.tutorial_world.objects.CmdRead method)

aliases (evennia.contrib.tutorials.tutorial_world.objects.CmdRead attribute)

CmdSetReadable (class in evennia.contrib.tutorials.tutorial_world.objects)

path (evennia.contrib.tutorials.tutorial_world.objects.CmdSetReadable attribute)

TutorialReadable (class in evennia.contrib.tutorials.tutorial_world.objects)

CmdClimb (class in evennia.contrib.tutorials.tutorial_world.objects)

key (evennia.contrib.tutorials.tutorial_world.objects.CmdClimb attribute)

locks (evennia.contrib.tutorials.tutorial_world.objects.CmdClimb attribute)

func() (evennia.contrib.tutorials.tutorial_world.objects.CmdClimb method)

aliases (evennia.contrib.tutorials.tutorial_world.objects.CmdClimb attribute)

CmdSetClimbable (class in evennia.contrib.tutorials.tutorial_world.objects)

TutorialClimbable (class in evennia.contrib.tutorials.tutorial_world.objects)

Obelisk (class in evennia.contrib.tutorials.tutorial_world.objects)

path (evennia.contrib.tutorials.tutorial_world.objects.Obelisk attribute)

typename (evennia.contrib.tutorials.tutorial_world.objects.Obelisk attribute)

CmdLight (class in evennia.contrib.tutorials.tutorial_world.objects)

key (evennia.contrib.tutorials.tutorial_world.objects.CmdLight attribute)

aliases (evennia.contrib.tutorials.tutorial_world.objects.CmdLight attribute)

locks (evennia.contrib.tutorials.tutorial_world.objects.CmdLight attribute)

func() (evennia.contrib.tutorials.tutorial_world.objects.CmdLight method)

CmdSetLight (class in evennia.contrib.tutorials.tutorial_world.objects)

key (evennia.contrib.tutorials.tutorial_world.objects.CmdSetLight attribute)

path (evennia.contrib.tutorials.tutorial_world.objects.CmdSetLight attribute)

LightSource (class in evennia.contrib.tutorials.tutorial_world.objects)

at_init() (evennia.contrib.tutorials.tutorial_world.objects.LightSource method)

light() (evennia.contrib.tutorials.tutorial_world.objects.LightSource method)

path (evennia.contrib.tutorials.tutorial_world.objects.LightSource attribute)

CmdShiftRoot (class in evennia.contrib.tutorials.tutorial_world.objects)

key (evennia.contrib.tutorials.tutorial_world.objects.CmdShiftRoot attribute)

locks (evennia.contrib.tutorials.tutorial_world.objects.CmdShiftRoot attribute)

parse() (evennia.contrib.tutorials.tutorial_world.objects.CmdShiftRoot method)

func() (evennia.contrib.tutorials.tutorial_world.objects.CmdShiftRoot method)

CmdPressButton (class in evennia.contrib.tutorials.tutorial_world.objects)

key (evennia.contrib.tutorials.tutorial_world.objects.CmdPressButton attribute)

func() (evennia.contrib.tutorials.tutorial_world.objects.CmdPressButton method)

CmdSetCrumblingWall (class in evennia.contrib.tutorials.tutorial_world.objects)

CrumblingWall (class in evennia.contrib.tutorials.tutorial_world.objects)

reset() (evennia.contrib.tutorials.tutorial_world.objects.CrumblingWall method)

path (evennia.contrib.tutorials.tutorial_world.objects.CrumblingWall attribute)

CmdAttack (class in evennia.contrib.tutorials.tutorial_world.objects)

key (evennia.contrib.tutorials.tutorial_world.objects.CmdAttack attribute)

aliases (evennia.contrib.tutorials.tutorial_world.objects.CmdAttack attribute)

locks (evennia.contrib.tutorials.tutorial_world.objects.CmdAttack attribute)

func() (evennia.contrib.tutorials.tutorial_world.objects.CmdAttack method)

CmdSetWeapon (class in evennia.contrib.tutorials.tutorial_world.objects)

path (evennia.contrib.tutorials.tutorial_world.objects.CmdSetWeapon attribute)

TutorialWeapon (class in evennia.contrib.tutorials.tutorial_world.objects)

reset() (evennia.contrib.tutorials.tutorial_world.objects.TutorialWeapon method)

path (evennia.contrib.tutorials.tutorial_world.objects.TutorialWeapon attribute)

CmdGetWeapon (class in evennia.contrib.tutorials.tutorial_world.objects)

key (evennia.contrib.tutorials.tutorial_world.objects.CmdGetWeapon attribute)

locks (evennia.contrib.tutorials.tutorial_world.objects.CmdGetWeapon attribute)

func() (evennia.contrib.tutorials.tutorial_world.objects.CmdGetWeapon method)

CmdSetWeaponRack (class in evennia.contrib.tutorials.tutorial_world.objects)

TutorialWeaponRack (class in evennia.contrib.tutorials.tutorial_world.objects)

CmdTutorial (class in evennia.contrib.tutorials.tutorial_world.rooms)

key (evennia.contrib.tutorials.tutorial_world.rooms.CmdTutorial attribute)

aliases (evennia.contrib.tutorials.tutorial_world.rooms.CmdTutorial attribute)

locks (evennia.contrib.tutorials.tutorial_world.rooms.CmdTutorial attribute)

func() (evennia.contrib.tutorials.tutorial_world.rooms.CmdTutorial method)

CmdTutorialSetDetail (class in evennia.contrib.tutorials.tutorial_world.rooms)

CmdTutorialLook (class in evennia.contrib.tutorials.tutorial_world.rooms)

func() (evennia.contrib.tutorials.tutorial_world.rooms.CmdTutorialLook method)

key (evennia.contrib.tutorials.tutorial_world.rooms.CmdTutorialLook attribute)

CmdTutorialGiveUp (class in evennia.contrib.tutorials.tutorial_world.rooms)

key (evennia.contrib.tutorials.tutorial_world.rooms.CmdTutorialGiveUp attribute)

func() (evennia.contrib.tutorials.tutorial_world.rooms.CmdTutorialGiveUp method)

TutorialRoomCmdSet (class in evennia.contrib.tutorials.tutorial_world.rooms)

TutorialRoom (class in evennia.contrib.tutorials.tutorial_world.rooms)

path (evennia.contrib.tutorials.tutorial_world.rooms.TutorialRoom attribute)

typename (evennia.contrib.tutorials.tutorial_world.rooms.TutorialRoom attribute)

TutorialStartExit (class in evennia.contrib.tutorials.tutorial_world.rooms)

WeatherRoom (class in evennia.contrib.tutorials.tutorial_world.rooms)

path (evennia.contrib.tutorials.tutorial_world.rooms.WeatherRoom attribute)

typename (evennia.contrib.tutorials.tutorial_world.rooms.WeatherRoom attribute)

CmdEvenniaIntro (class in evennia.contrib.tutorials.tutorial_world.rooms)

key (evennia.contrib.tutorials.tutorial_world.rooms.CmdEvenniaIntro attribute)

func() (evennia.contrib.tutorials.tutorial_world.rooms.CmdEvenniaIntro method)

CmdSetEvenniaIntro (class in evennia.contrib.tutorials.tutorial_world.rooms)

IntroRoom (class in evennia.contrib.tutorials.tutorial_world.rooms)

path (evennia.contrib.tutorials.tutorial_world.rooms.IntroRoom attribute)

typename (evennia.contrib.tutorials.tutorial_world.rooms.IntroRoom attribute)

CmdEast (class in evennia.contrib.tutorials.tutorial_world.rooms)

key (evennia.contrib.tutorials.tutorial_world.rooms.CmdEast attribute)

aliases (evennia.contrib.tutorials.tutorial_world.rooms.CmdEast attribute)

locks (evennia.contrib.tutorials.tutorial_world.rooms.CmdEast attribute)

help_category (evennia.contrib.tutorials.tutorial_world.rooms.CmdEast attribute)

func() (evennia.contrib.tutorials.tutorial_world.rooms.CmdEast method)

lock_storage (evennia.contrib.tutorials.tutorial_world.rooms.CmdEast attribute)

CmdWest (class in evennia.contrib.tutorials.tutorial_world.rooms)

key (evennia.contrib.tutorials.tutorial_world.rooms.CmdWest attribute)

aliases (evennia.contrib.tutorials.tutorial_world.rooms.CmdWest attribute)

locks (evennia.contrib.tutorials.tutorial_world.rooms.CmdWest attribute)

help_category (evennia.contrib.tutorials.tutorial_world.rooms.CmdWest attribute)

func() (evennia.contrib.tutorials.tutorial_world.rooms.CmdWest method)

lock_storage (evennia.contrib.tutorials.tutorial_world.rooms.CmdWest attribute)

CmdLookBridge (class in evennia.contrib.tutorials.tutorial_world.rooms)

key (evennia.contrib.tutorials.tutorial_world.rooms.CmdLookBridge attribute)

aliases (evennia.contrib.tutorials.tutorial_world.rooms.CmdLookBridge attribute)

locks (evennia.contrib.tutorials.tutorial_world.rooms.CmdLookBridge attribute)

func() (evennia.contrib.tutorials.tutorial_world.rooms.CmdLookBridge method)

CmdBridgeHelp (class in evennia.contrib.tutorials.tutorial_world.rooms)

key (evennia.contrib.tutorials.tutorial_world.rooms.CmdBridgeHelp attribute)

aliases (evennia.contrib.tutorials.tutorial_world.rooms.CmdBridgeHelp attribute)

locks (evennia.contrib.tutorials.tutorial_world.rooms.CmdBridgeHelp attribute)

func() (evennia.contrib.tutorials.tutorial_world.rooms.CmdBridgeHelp method)

BridgeCmdSet (class in evennia.contrib.tutorials.tutorial_world.rooms)

key (evennia.contrib.tutorials.tutorial_world.rooms.BridgeCmdSet attribute)

priority (evennia.contrib.tutorials.tutorial_world.rooms.BridgeCmdSet attribute)

path (evennia.contrib.tutorials.tutorial_world.rooms.BridgeCmdSet attribute)

BridgeRoom (class in evennia.contrib.tutorials.tutorial_world.rooms)

path (evennia.contrib.tutorials.tutorial_world.rooms.BridgeRoom attribute)

typename (evennia.contrib.tutorials.tutorial_world.rooms.BridgeRoom attribute)

CmdLookDark (class in evennia.contrib.tutorials.tutorial_world.rooms)

key (evennia.contrib.tutorials.tutorial_world.rooms.CmdLookDark attribute)

aliases (evennia.contrib.tutorials.tutorial_world.rooms.CmdLookDark attribute)

locks (evennia.contrib.tutorials.tutorial_world.rooms.CmdLookDark attribute)

func() (evennia.contrib.tutorials.tutorial_world.rooms.CmdLookDark method)

CmdDarkHelp (class in evennia.contrib.tutorials.tutorial_world.rooms)

key (evennia.contrib.tutorials.tutorial_world.rooms.CmdDarkHelp attribute)

locks (evennia.contrib.tutorials.tutorial_world.rooms.CmdDarkHelp attribute)

func() (evennia.contrib.tutorials.tutorial_world.rooms.CmdDarkHelp method)

aliases (evennia.contrib.tutorials.tutorial_world.rooms.CmdDarkHelp attribute)

CmdDarkNoMatch (class in evennia.contrib.tutorials.tutorial_world.rooms)

key (evennia.contrib.tutorials.tutorial_world.rooms.CmdDarkNoMatch attribute)

locks (evennia.contrib.tutorials.tutorial_world.rooms.CmdDarkNoMatch attribute)

func() (evennia.contrib.tutorials.tutorial_world.rooms.CmdDarkNoMatch method)

DarkCmdSet (class in evennia.contrib.tutorials.tutorial_world.rooms)

key (evennia.contrib.tutorials.tutorial_world.rooms.DarkCmdSet attribute)

mergetype (evennia.contrib.tutorials.tutorial_world.rooms.DarkCmdSet attribute)

priority (evennia.contrib.tutorials.tutorial_world.rooms.DarkCmdSet attribute)

path (evennia.contrib.tutorials.tutorial_world.rooms.DarkCmdSet attribute)

DarkRoom (class in evennia.contrib.tutorials.tutorial_world.rooms)

at_init() (evennia.contrib.tutorials.tutorial_world.rooms.DarkRoom method)

path (evennia.contrib.tutorials.tutorial_world.rooms.DarkRoom attribute)

typename (evennia.contrib.tutorials.tutorial_world.rooms.DarkRoom attribute)

TeleportRoom (class in evennia.contrib.tutorials.tutorial_world.rooms)

path (evennia.contrib.tutorials.tutorial_world.rooms.TeleportRoom attribute)

typename (evennia.contrib.tutorials.tutorial_world.rooms.TeleportRoom attribute)

OutroRoom (class in evennia.contrib.tutorials.tutorial_world.rooms)

path (evennia.contrib.tutorials.tutorial_world.rooms.OutroRoom attribute)

typename (evennia.contrib.tutorials.tutorial_world.rooms.OutroRoom attribute)

TestTutorialWorldMob (class in evennia.contrib.tutorials.tutorial_world.tests)

TestTutorialWorldObjects (class in evennia.contrib.tutorials.tutorial_world.tests)

TestTutorialWorldRooms (class in evennia.contrib.tutorials.tutorial_world.tests)

AuditedServerSession (class in evennia.contrib.utils.auditing.server)

audit() (evennia.contrib.utils.auditing.server.AuditedServerSession method)

mask() (evennia.contrib.utils.auditing.server.AuditedServerSession method)

data_out() (evennia.contrib.utils.auditing.server.AuditedServerSession method)

data_in() (evennia.contrib.utils.auditing.server.AuditedServerSession method)

setup_session() (evennia.contrib.utils.auditing.tests.AuditingTest method)

test_mask() (evennia.contrib.utils.auditing.tests.AuditingTest method)

test_audit() (evennia.contrib.utils.auditing.tests.AuditingTest method)

FieldEvMenu (class in evennia.contrib.utils.fieldfill.fieldfill)

node_formatter() (evennia.contrib.utils.fieldfill.fieldfill.FieldEvMenu method)

init_fill_field() (in module evennia.contrib.utils.fieldfill.fieldfill)

menunode_fieldfill() (in module evennia.contrib.utils.fieldfill.fieldfill)

form_template_to_dict() (in module evennia.contrib.utils.fieldfill.fieldfill)

display_formdata() (in module evennia.contrib.utils.fieldfill.fieldfill)

verify_online_player() (in module evennia.contrib.utils.fieldfill.fieldfill)

CmdTestMenu (class in evennia.contrib.utils.fieldfill.fieldfill)

key (evennia.contrib.utils.fieldfill.fieldfill.CmdTestMenu attribute)

func() (evennia.contrib.utils.fieldfill.fieldfill.CmdTestMenu method)

aliases (evennia.contrib.utils.fieldfill.fieldfill.CmdTestMenu attribute)

help_category (evennia.contrib.utils.fieldfill.fieldfill.CmdTestMenu attribute)

lock_storage (evennia.contrib.utils.fieldfill.fieldfill.CmdTestMenu attribute)

sendmessage() (in module evennia.contrib.utils.fieldfill.fieldfill)

init_delayed_message() (in module evennia.contrib.utils.fieldfill.fieldfill)

GitCommand (class in evennia.contrib.utils.git_integration.git_integration)

pull() (evennia.contrib.utils.git_integration.git_integration.GitCommand method)

func() (evennia.contrib.utils.git_integration.git_integration.GitCommand method)

key (evennia.contrib.utils.git_integration.git_integration.GitCommand attribute)

CmdGitEvennia (class in evennia.contrib.utils.git_integration.git_integration)

CmdGit (class in evennia.contrib.utils.git_integration.git_integration)

key (evennia.contrib.utils.git_integration.git_integration.CmdGit attribute)

locks (evennia.contrib.utils.git_integration.git_integration.CmdGit attribute)

aliases (evennia.contrib.utils.git_integration.git_integration.CmdGit attribute)

GitCmdSet (class in evennia.contrib.utils.git_integration.git_integration)

path (evennia.contrib.utils.git_integration.git_integration.GitCmdSet attribute)

TestGitIntegration (class in evennia.contrib.utils.git_integration.tests)

setUp() (evennia.contrib.utils.git_integration.tests.TestGitIntegration method)

fantasy_name() (in module evennia.contrib.utils.name_generator.namegen)

first_name() (in module evennia.contrib.utils.name_generator.namegen)

last_name() (in module evennia.contrib.utils.name_generator.namegen)

full_name() (in module evennia.contrib.utils.name_generator.namegen)

TestNameGenerator (class in evennia.contrib.utils.name_generator.tests)

RandomStringGeneratorScript (class in evennia.contrib.utils.random_string_generator.random_string_generator)

RandomStringGenerator (class in evennia.contrib.utils.random_string_generator.random_string_generator)

TestRandomStringGenerator (class in evennia.contrib.utils.random_string_generator.tests)

TestTreeSelectFunc (class in evennia.contrib.utils.tree_select.tests)

TestFieldFillFunc (class in evennia.contrib.utils.tree_select.tests)

init_tree_selection() (in module evennia.contrib.utils.tree_select.tree_select)

dashcount() (in module evennia.contrib.utils.tree_select.tree_select)

is_category() (in module evennia.contrib.utils.tree_select.tree_select)

parse_opts() (in module evennia.contrib.utils.tree_select.tree_select)

index_to_selection() (in module evennia.contrib.utils.tree_select.tree_select)

go_up_one_category() (in module evennia.contrib.utils.tree_select.tree_select)

optlist_to_menuoptions() (in module evennia.contrib.utils.tree_select.tree_select)

menunode_treeselect() (in module evennia.contrib.utils.tree_select.tree_select)

CmdNameColor (class in evennia.contrib.utils.tree_select.tree_select)

key (evennia.contrib.utils.tree_select.tree_select.CmdNameColor attribute)

func() (evennia.contrib.utils.tree_select.tree_select.CmdNameColor method)

aliases (evennia.contrib.utils.tree_select.tree_select.CmdNameColor attribute)

change_name_color() (in module evennia.contrib.utils.tree_select.tree_select)

search_index_entry() (evennia.help.filehelp.FileHelpEntry property)

web_get_detail_url() (evennia.help.filehelp.FileHelpEntry method)

web_get_admin_url() (evennia.help.filehelp.FileHelpEntry method)

__init__() (evennia.help.filehelp.FileHelpStorageHandler method)

find_topicmatch() (evennia.help.manager.HelpEntryManager method)

find_topicsuggestions() (evennia.help.manager.HelpEntryManager method)

find_topics_with_category() (evennia.help.manager.HelpEntryManager method)

get_all_categories() (evennia.help.manager.HelpEntryManager method)

all_to_category() (evennia.help.manager.HelpEntryManager method)

web_get_create_url() (evennia.help.models.HelpEntry class method)

get_next_by_db_date_created() (evennia.help.models.HelpEntry method)

get_previous_by_db_date_created() (evennia.help.models.HelpEntry method)

cache_lock_bypass() (evennia.locks.lockhandler.LockHandler method)

check_lockstring() (evennia.locks.lockhandler.LockHandler method)

get_object_with_account() (evennia.objects.manager.ObjectDBManager method)

get_objs_with_attr() (evennia.objects.manager.ObjectDBManager method)

get_objs_with_attr_value() (evennia.objects.manager.ObjectDBManager method)

get_objs_with_db_property() (evennia.objects.manager.ObjectDBManager method)

get_objs_with_key_or_alias() (evennia.objects.manager.ObjectDBManager method)

search_object() (evennia.objects.manager.ObjectDBManager method)

object_search() (evennia.objects.manager.ObjectDBManager method)

clear_all_sessids() (evennia.objects.manager.ObjectDBManager method)

create_object() (evennia.objects.manager.ObjectDBManager method)

at_db_location_postsave() (evennia.objects.models.ObjectDB method)

get_next_by_db_date_created() (evennia.objects.models.ObjectDB method)

get_previous_by_db_date_created() (evennia.objects.models.ObjectDB method)

hide_from_objects_set (evennia.objects.models.ObjectDB attribute)

object_subscription_set (evennia.objects.models.ObjectDB attribute)

__init__() (evennia.objects.objects.ObjectSessionHandler method)

appearance_template (evennia.objects.objects.DefaultObject attribute)

at_object_post_copy() (evennia.objects.objects.DefaultObject method)

get_display_name() (evennia.objects.objects.DefaultObject method)

get_numbered_name() (evennia.objects.objects.DefaultObject method)

get_display_header() (evennia.objects.objects.DefaultObject method)

get_display_desc() (evennia.objects.objects.DefaultObject method)

get_display_exits() (evennia.objects.objects.DefaultObject method)

get_display_characters() (evennia.objects.objects.DefaultObject method)

get_display_things() (evennia.objects.objects.DefaultObject method)

get_display_footer() (evennia.objects.objects.DefaultObject method)

format_appearance() (evennia.objects.objects.DefaultObject method)

return_appearance() (evennia.objects.objects.DefaultObject method)

basetype_posthook_setup() (evennia.objects.objects.DefaultObject method)

at_object_creation() (evennia.objects.objects.DefaultObject method)

at_object_delete() (evennia.objects.objects.DefaultObject method)

at_pre_unpuppet() (evennia.objects.objects.DefaultObject method)

at_post_unpuppet() (evennia.objects.objects.DefaultObject method)

at_server_reload() (evennia.objects.objects.DefaultObject method)

at_server_shutdown() (evennia.objects.objects.DefaultObject method)

at_pre_object_leave() (evennia.objects.objects.DefaultObject method)

at_pre_object_receive() (evennia.objects.objects.DefaultObject method)

announce_move_from() (evennia.objects.objects.DefaultObject method)

announce_move_to() (evennia.objects.objects.DefaultObject method)

at_object_leave() (evennia.objects.objects.DefaultObject method)

at_object_receive() (evennia.objects.objects.DefaultObject method)

at_post_traverse() (evennia.objects.objects.DefaultObject method)

at_after_traverse() (evennia.objects.objects.DefaultObject method)

at_failed_traverse() (evennia.objects.objects.DefaultObject method)

get_visible_contents() (evennia.objects.objects.DefaultObject method)

get_content_names() (evennia.objects.objects.DefaultObject method)

create() (evennia.objects.objects.DefaultCharacter class method)

normalize_name() (evennia.objects.objects.DefaultCharacter class method)

validate_name() (evennia.objects.objects.DefaultCharacter class method)

basetype_setup() (evennia.objects.objects.DefaultCharacter method)

at_post_move() (evennia.objects.objects.DefaultCharacter method)

at_after_move() (evennia.objects.objects.DefaultCharacter method)

at_pre_puppet() (evennia.objects.objects.DefaultCharacter method)

at_post_puppet() (evennia.objects.objects.DefaultCharacter method)

at_post_unpuppet() (evennia.objects.objects.DefaultCharacter method)

connection_time() (evennia.objects.objects.DefaultCharacter property)

search_index_entry (evennia.objects.objects.ExitCommand attribute)

create_exit_cmdset() (evennia.objects.objects.DefaultExit method)

at_failed_traverse() (evennia.objects.objects.DefaultExit method)

protfunc_callable_protkey() (in module evennia.prototypes.protfuncs)

homogenize_prototype() (in module evennia.prototypes.prototypes)

load_module_prototypes() (in module evennia.prototypes.prototypes)

__init__() (evennia.prototypes.prototypes.DBPrototypeCache method)

remove() (evennia.prototypes.prototypes.DBPrototypeCache method)

replace() (evennia.prototypes.prototypes.DBPrototypeCache method)

at_script_creation() (evennia.prototypes.prototypes.DbPrototype method)

prototype() (evennia.prototypes.prototypes.DbPrototype property)

search_objects_with_prototype() (in module evennia.prototypes.prototypes)

__init__() (evennia.prototypes.prototypes.PrototypeEvMore method)

init_pages() (evennia.prototypes.prototypes.PrototypeEvMore method)

prototype_paginator() (evennia.prototypes.prototypes.PrototypeEvMore method)

page_formatter() (evennia.prototypes.prototypes.PrototypeEvMore method)

format_available_protfuncs() (in module evennia.prototypes.prototypes)

prototype_diff_from_object() (in module evennia.prototypes.spawner)

batch_update_objects_with_prototype() (in module evennia.prototypes.spawner)

get_all_scripts_on_obj() (evennia.scripts.manager.ScriptDBManager method)

get_all_scripts() (evennia.scripts.manager.ScriptDBManager method)

delete_script() (evennia.scripts.manager.ScriptDBManager method)

search_script() (evennia.scripts.manager.ScriptDBManager method)

script_search() (evennia.scripts.manager.ScriptDBManager method)

create_script() (evennia.scripts.manager.ScriptDBManager method)

get_next_by_db_date_created() (evennia.scripts.models.ScriptDB method)

get_previous_by_db_date_created() (evennia.scripts.models.ScriptDB method)

__init__() (evennia.scripts.monitorhandler.MonitorHandler method)

restore() (evennia.scripts.monitorhandler.MonitorHandler method)

at_update() (evennia.scripts.monitorhandler.MonitorHandler method)

at_script_creation() (evennia.scripts.scripts.DefaultScript method)

at_script_delete() (evennia.scripts.scripts.DefaultScript method)

at_server_reload() (evennia.scripts.scripts.DefaultScript method)

at_server_shutdown() (evennia.scripts.scripts.DefaultScript method)

at_server_start() (evennia.scripts.scripts.DefaultScript method)

get_deferred() (evennia.scripts.taskhandler.TaskHandlerTask method)

clean_stale_tasks() (evennia.scripts.taskhandler.TaskHandler method)

create_delays() (evennia.scripts.taskhandler.TaskHandler method)

ticker_class (evennia.scripts.tickerhandler.TickerPool attribute)

ticker_pool_class (evennia.scripts.tickerhandler.TickerHandler attribute)

all_display() (evennia.scripts.tickerhandler.TickerHandler method)

initialDelay (evennia.server.amp_client.AMPClientFactory attribute)

startedConnecting() (evennia.server.amp_client.AMPClientFactory method)

buildProtocol() (evennia.server.amp_client.AMPClientFactory method)

clientConnectionLost() (evennia.server.amp_client.AMPClientFactory method)

clientConnectionFailed() (evennia.server.amp_client.AMPClientFactory method)

connectionMade() (evennia.server.amp_client.AMPServerClientProtocol method)

data_to_portal() (evennia.server.amp_client.AMPServerClientProtocol method)

__init__() (evennia.server.connection_wizard.ConnectionWizard method)

display() (evennia.server.connection_wizard.ConnectionWizard method)

ask_continue() (evennia.server.connection_wizard.ConnectionWizard method)

ask_node() (evennia.server.connection_wizard.ConnectionWizard method)

ask_yesno() (evennia.server.connection_wizard.ConnectionWizard method)

ask_choice() (evennia.server.connection_wizard.ConnectionWizard method)

ask_input() (evennia.server.connection_wizard.ConnectionWizard method)

node_game_index_start() (in module evennia.server.connection_wizard)

node_game_index_fields() (in module evennia.server.connection_wizard)

node_view_and_apply_settings() (in module evennia.server.connection_wizard)

commandName (evennia.server.evennia_launcher.MsgStatus attribute)

reverseErrors (evennia.server.evennia_launcher.MsgStatus attribute)

key (evennia.server.evennia_launcher.MsgLauncher2Portal attribute)

arguments (evennia.server.evennia_launcher.MsgLauncher2Portal attribute)

errors (evennia.server.evennia_launcher.MsgLauncher2Portal attribute)

response (evennia.server.evennia_launcher.MsgLauncher2Portal attribute)

allErrors (evennia.server.evennia_launcher.MsgLauncher2Portal attribute)

commandName (evennia.server.evennia_launcher.MsgLauncher2Portal attribute)

reverseErrors (evennia.server.evennia_launcher.MsgLauncher2Portal attribute)

__init__() (evennia.server.evennia_launcher.AMPLauncherProtocol method)

wait_for_status() (evennia.server.evennia_launcher.AMPLauncherProtocol method)

wait_for_status_reply() (in module evennia.server.evennia_launcher)

start_server_interactive() (in module evennia.server.evennia_launcher)

start_portal_interactive() (in module evennia.server.evennia_launcher)

check_main_evennia_dependencies() (in module evennia.server.evennia_launcher)

create_settings_file() (in module evennia.server.evennia_launcher)

create_game_directory() (in module evennia.server.evennia_launcher)

error_check_python_modules() (in module evennia.server.evennia_launcher)

init_game_directory() (in module evennia.server.evennia_launcher)

run_connect_wizard() (in module evennia.server.evennia_launcher)

run_custom_commands() (in module evennia.server.evennia_launcher)

EvenniaGameIndexClient (class in evennia.server.game_index_client.client)

SimpleResponseReceiver (class in evennia.server.game_index_client.client)

StringProducer (class in evennia.server.game_index_client.client)

__init__() (evennia.server.game_index_client.client.StringProducer method)

startProducing() (evennia.server.game_index_client.client.StringProducer method)

pauseProducing() (evennia.server.game_index_client.client.StringProducer method)

stopProducing() (evennia.server.game_index_client.client.StringProducer method)

QuietHTTP11ClientFactory (class in evennia.server.game_index_client.client)

EvenniaGameIndexService (class in evennia.server.game_index_client.service)

arguments (evennia.server.portal.amp.MsgLauncher2Portal attribute)

response (evennia.server.portal.amp.MsgLauncher2Portal attribute)

allErrors (evennia.server.portal.amp.MsgLauncher2Portal attribute)

commandName (evennia.server.portal.amp.MsgLauncher2Portal attribute)

reverseErrors (evennia.server.portal.amp.MsgLauncher2Portal attribute)

arguments (evennia.server.portal.amp.MsgPortal2Server attribute)

allErrors (evennia.server.portal.amp.MsgPortal2Server attribute)

commandName (evennia.server.portal.amp.MsgPortal2Server attribute)

reverseErrors (evennia.server.portal.amp.MsgPortal2Server attribute)

arguments (evennia.server.portal.amp.MsgServer2Portal attribute)

allErrors (evennia.server.portal.amp.MsgServer2Portal attribute)

commandName (evennia.server.portal.amp.MsgServer2Portal attribute)

reverseErrors (evennia.server.portal.amp.MsgServer2Portal attribute)

arguments (evennia.server.portal.amp.AdminPortal2Server attribute)

response (evennia.server.portal.amp.AdminPortal2Server attribute)

allErrors (evennia.server.portal.amp.AdminPortal2Server attribute)

commandName (evennia.server.portal.amp.AdminPortal2Server attribute)

reverseErrors (evennia.server.portal.amp.AdminPortal2Server attribute)

arguments (evennia.server.portal.amp.AdminServer2Portal attribute)

response (evennia.server.portal.amp.AdminServer2Portal attribute)

allErrors (evennia.server.portal.amp.AdminServer2Portal attribute)

commandName (evennia.server.portal.amp.AdminServer2Portal attribute)

reverseErrors (evennia.server.portal.amp.AdminServer2Portal attribute)

reverseErrors (evennia.server.portal.amp.FunctionCall attribute)

__init__() (evennia.server.portal.amp.AMPMultiConnectionProtocol method)

stringReceived() (evennia.server.portal.amp.AMPMultiConnectionProtocol method)

dataReceived() (evennia.server.portal.amp.AMPMultiConnectionProtocol method)

makeConnection() (evennia.server.portal.amp.AMPMultiConnectionProtocol method)

connectionMade() (evennia.server.portal.amp.AMPMultiConnectionProtocol method)

connectionLost() (evennia.server.portal.amp.AMPMultiConnectionProtocol method)

errback() (evennia.server.portal.amp.AMPMultiConnectionProtocol method)

data_in() (evennia.server.portal.amp.AMPMultiConnectionProtocol method)

broadcast() (evennia.server.portal.amp.AMPMultiConnectionProtocol method)

noisy (evennia.server.portal.amp_server.AMPServerFactory attribute)

logPrefix() (evennia.server.portal.amp_server.AMPServerFactory method)

__init__() (evennia.server.portal.amp_server.AMPServerFactory method)

buildProtocol() (evennia.server.portal.amp_server.AMPServerFactory method)

connectionLost() (evennia.server.portal.amp_server.AMPServerProtocol method)

get_status() (evennia.server.portal.amp_server.AMPServerProtocol method)

data_to_server() (evennia.server.portal.amp_server.AMPServerProtocol method)

start_server() (evennia.server.portal.amp_server.AMPServerProtocol method)

stop_server() (evennia.server.portal.amp_server.AMPServerProtocol method)

__init__() (evennia.server.portal.discord.QuietConnectionPool method)

DiscordWebsocketServerFactory (class in evennia.server.portal.discord)

factor (evennia.server.portal.discord.DiscordWebsocketServerFactory attribute)

maxDelay (evennia.server.portal.discord.DiscordWebsocketServerFactory attribute)

noisy (evennia.server.portal.discord.DiscordWebsocketServerFactory attribute)

gateway (evennia.server.portal.discord.DiscordWebsocketServerFactory attribute)

__init__() (evennia.server.portal.discord.DiscordWebsocketServerFactory method)

reconnect() (evennia.server.portal.discord.DiscordWebsocketServerFactory method)

start() (evennia.server.portal.discord.DiscordWebsocketServerFactory method)

nextHeartbeatCall (evennia.server.portal.discord.DiscordClient attribute)

pending_heartbeat (evennia.server.portal.discord.DiscordClient attribute)

heartbeat_interval (evennia.server.portal.discord.DiscordClient attribute)

last_sequence (evennia.server.portal.discord.DiscordClient attribute)

session_id (evennia.server.portal.discord.DiscordClient attribute)

discord_id (evennia.server.portal.discord.DiscordClient attribute)

onMessage() (evennia.server.portal.discord.DiscordClient method)

post_response() (evennia.server.portal.discord.DiscordClient method)

handle_error() (evennia.server.portal.discord.DiscordClient method)

disconnect() (evennia.server.portal.discord.DiscordClient method)

connection_ready() (evennia.server.portal.discord.DiscordClient method)

doHeartbeat() (evennia.server.portal.discord.DiscordClient method)

send_channel() (evennia.server.portal.discord.DiscordClient method)

send_nickname() (evennia.server.portal.discord.DiscordClient method)

send_role() (evennia.server.portal.discord.DiscordClient method)

send_default() (evennia.server.portal.discord.DiscordClient method)

RestartingWebsocketServerFactory (class in evennia.server.portal.grapevine)

__init__() (evennia.server.portal.grapevine.GrapevineClient method)

at_login() (evennia.server.portal.grapevine.GrapevineClient method)

onOpen() (evennia.server.portal.grapevine.GrapevineClient method)

onMessage() (evennia.server.portal.grapevine.GrapevineClient method)

onClose() (evennia.server.portal.grapevine.GrapevineClient method)

disconnect() (evennia.server.portal.grapevine.GrapevineClient method)

send_authenticate() (evennia.server.portal.grapevine.GrapevineClient method)

send_heartbeat() (evennia.server.portal.grapevine.GrapevineClient method)

send_subscribe() (evennia.server.portal.grapevine.GrapevineClient method)

send_unsubscribe() (evennia.server.portal.grapevine.GrapevineClient method)

send_channel() (evennia.server.portal.grapevine.GrapevineClient method)

send_default() (evennia.server.portal.grapevine.GrapevineClient method)

data_in() (evennia.server.portal.grapevine.GrapevineClient method)

send_request_nicklist() (evennia.server.portal.irc.IRCBot method)

initialDelay (evennia.server.portal.irc.IRCBotFactory attribute)

buildProtocol() (evennia.server.portal.irc.IRCBotFactory method)

startedConnecting() (evennia.server.portal.irc.IRCBotFactory method)

clientConnectionFailed() (evennia.server.portal.irc.IRCBotFactory method)

clientConnectionLost() (evennia.server.portal.irc.IRCBotFactory method)

PortalSessionHandler (class in evennia.server.portal.portalsessionhandler)

sync() (evennia.server.portal.portalsessionhandler.PortalSessionHandler method)

__init__() (evennia.server.portal.service.EvenniaPortalService method)

portal_maintenance() (evennia.server.portal.service.EvenniaPortalService method)

register_plugins() (evennia.server.portal.service.EvenniaPortalService method)

check_lockdown() (evennia.server.portal.service.EvenniaPortalService method)

register_ssl() (evennia.server.portal.service.EvenniaPortalService method)

register_ssh() (evennia.server.portal.service.EvenniaPortalService method)

register_webserver() (evennia.server.portal.service.EvenniaPortalService method)

register_telnet() (evennia.server.portal.service.EvenniaPortalService method)

register_amp() (evennia.server.portal.service.EvenniaPortalService method)

get_info_dict() (evennia.server.portal.service.EvenniaPortalService method)

shutdown() (evennia.server.portal.service.EvenniaPortalService method)

getClientAddress() (evennia.server.portal.ssh.SshProtocol method)

auth_password() (evennia.server.portal.ssh.ExtraInfoAuthServer method)

noisy (evennia.server.portal.ssh.AccountDBPasswordChecker attribute)

__init__() (evennia.server.portal.ssh.AccountDBPasswordChecker method)

requestAvatarId() (evennia.server.portal.ssh.AccountDBPasswordChecker method)

noisy (evennia.server.portal.ssh.PassAvatarIdTerminalRealm attribute)

TerminalSessionTransport_getPeer (class in evennia.server.portal.ssh)

noisy (evennia.server.portal.ssh.TerminalSessionTransport_getPeer attribute)

__init__() (evennia.server.portal.ssh.TerminalSessionTransport_getPeer method)

__init__() (evennia.server.portal.suppress_ga.SuppressGA method)

wont_suppress_ga() (evennia.server.portal.suppress_ga.SuppressGA method)

will_suppress_ga() (evennia.server.portal.suppress_ga.SuppressGA method)

noisy (evennia.server.portal.telnet.TelnetServerFactory attribute)

logPrefix() (evennia.server.portal.telnet.TelnetServerFactory method)

dataReceived() (evennia.server.portal.telnet.TelnetProtocol method)

connectionMade() (evennia.server.portal.telnet.TelnetProtocol method)

toggle_nop_keepalive() (evennia.server.portal.telnet.TelnetProtocol method)

handshake_done() (evennia.server.portal.telnet.TelnetProtocol method)

enableRemote() (evennia.server.portal.telnet.TelnetProtocol method)

disableRemote() (evennia.server.portal.telnet.TelnetProtocol method)

enableLocal() (evennia.server.portal.telnet.TelnetProtocol method)

disableLocal() (evennia.server.portal.telnet.TelnetProtocol method)

connectionLost() (evennia.server.portal.telnet.TelnetProtocol method)

applicationDataReceived() (evennia.server.portal.telnet.TelnetProtocol method)

disconnect() (evennia.server.portal.telnet.TelnetProtocol method)

send_text() (evennia.server.portal.telnet.TelnetProtocol method)

send_prompt() (evennia.server.portal.telnet.TelnetProtocol method)

send_default() (evennia.server.portal.telnet.TelnetProtocol method)

encode_msdp() (evennia.server.portal.telnet_oob.TelnetOOB method)

encode_gmcp() (evennia.server.portal.telnet_oob.TelnetOOB method)

decode_msdp() (evennia.server.portal.telnet_oob.TelnetOOB method)

decode_gmcp() (evennia.server.portal.telnet_oob.TelnetOOB method)

__init__() (evennia.server.portal.telnet_ssl.SSLProtocol method)

verify_or_create_SSL_key_and_cert() (in module evennia.server.portal.telnet_ssl)

test_amp_out() (evennia.server.portal.tests.TestAMPServer method)

test_amp_in() (evennia.server.portal.tests.TestAMPServer method)

test_large_msg() (evennia.server.portal.tests.TestAMPServer method)

test_mudlet_ttype() (evennia.server.portal.tests.TestTelnet method)

test_data_in() (evennia.server.portal.tests.TestWebSocket method)

test_data_out() (evennia.server.portal.tests.TestWebSocket method)

nonce (evennia.server.portal.webclient.WebSocketClient attribute)

__init__() (evennia.server.portal.webclient.WebSocketClient method)

get_client_session() (evennia.server.portal.webclient.WebSocketClient method)

onOpen() (evennia.server.portal.webclient.WebSocketClient method)

disconnect() (evennia.server.portal.webclient.WebSocketClient method)

onClose() (evennia.server.portal.webclient.WebSocketClient method)

onMessage() (evennia.server.portal.webclient.WebSocketClient method)

sendLine() (evennia.server.portal.webclient.WebSocketClient method)

at_login() (evennia.server.portal.webclient.WebSocketClient method)

data_in() (evennia.server.portal.webclient.WebSocketClient method)

send_text() (evennia.server.portal.webclient.WebSocketClient method)

send_prompt() (evennia.server.portal.webclient.WebSocketClient method)

send_default() (evennia.server.portal.webclient.WebSocketClient method)

default() (evennia.server.portal.webclient_ajax.LazyEncoder method)

isLeaf (evennia.server.portal.webclient_ajax.AjaxWebClient attribute)

allowedMethods (evennia.server.portal.webclient_ajax.AjaxWebClient attribute)

__init__() (evennia.server.portal.webclient_ajax.AjaxWebClient method)

get_client_sessid() (evennia.server.portal.webclient_ajax.AjaxWebClient method)

get_browserstr() (evennia.server.portal.webclient_ajax.AjaxWebClient method)

at_login() (evennia.server.portal.webclient_ajax.AjaxWebClient method)

lineSend() (evennia.server.portal.webclient_ajax.AjaxWebClient method)

client_disconnect() (evennia.server.portal.webclient_ajax.AjaxWebClient method)

mode_init() (evennia.server.portal.webclient_ajax.AjaxWebClient method)

mode_keepalive() (evennia.server.portal.webclient_ajax.AjaxWebClient method)

mode_input() (evennia.server.portal.webclient_ajax.AjaxWebClient method)

mode_receive() (evennia.server.portal.webclient_ajax.AjaxWebClient method)

mode_close() (evennia.server.portal.webclient_ajax.AjaxWebClient method)

render_POST() (evennia.server.portal.webclient_ajax.AjaxWebClient method)

AjaxWebClientSession (class in evennia.server.portal.webclient_ajax)

__init__() (evennia.server.portal.webclient_ajax.AjaxWebClientSession method)

disconnect() (evennia.server.portal.webclient_ajax.AjaxWebClientSession method)

at_login() (evennia.server.portal.webclient_ajax.AjaxWebClientSession method)

data_in() (evennia.server.portal.webclient_ajax.AjaxWebClientSession method)

data_out() (evennia.server.portal.webclient_ajax.AjaxWebClientSession method)

send_text() (evennia.server.portal.webclient_ajax.AjaxWebClientSession method)

send_prompt() (evennia.server.portal.webclient_ajax.AjaxWebClientSession method)

CmdDummyRunnerEchoResponse (class in evennia.server.profiling.dummyrunner)

key (evennia.server.profiling.dummyrunner.CmdDummyRunnerEchoResponse attribute)

func() (evennia.server.profiling.dummyrunner.CmdDummyRunnerEchoResponse method)

DummyRunnerCmdSet (class in evennia.server.profiling.dummyrunner)

path (evennia.server.profiling.dummyrunner.DummyRunnerCmdSet attribute)

report() (evennia.server.profiling.dummyrunner.DummyClient method)

connectionMade() (evennia.server.profiling.dummyrunner.DummyClient method)

dataReceived() (evennia.server.profiling.dummyrunner.DummyClient method)

connectionLost() (evennia.server.profiling.dummyrunner.DummyClient method)

error() (evennia.server.profiling.dummyrunner.DummyClient method)

counter() (evennia.server.profiling.dummyrunner.DummyClient method)

logout() (evennia.server.profiling.dummyrunner.DummyClient method)

step() (evennia.server.profiling.dummyrunner.DummyClient method)

protocol (evennia.server.profiling.dummyrunner.DummyFactory attribute)

initialDelay (evennia.server.profiling.dummyrunner.DummyFactory attribute)

maxDelay (evennia.server.profiling.dummyrunner.DummyFactory attribute)

noisy (evennia.server.profiling.dummyrunner.DummyFactory attribute)

__init__() (evennia.server.profiling.dummyrunner.DummyFactory method)

start_all_dummy_clients() (in module evennia.server.profiling.dummyrunner)

c_login() (in module evennia.server.profiling.dummyrunner_settings)

c_login_nodig() (in module evennia.server.profiling.dummyrunner_settings)

c_logout() (in module evennia.server.profiling.dummyrunner_settings)

c_looks() (in module evennia.server.profiling.dummyrunner_settings)

c_examines() (in module evennia.server.profiling.dummyrunner_settings)

c_idles() (in module evennia.server.profiling.dummyrunner_settings)

c_help() (in module evennia.server.profiling.dummyrunner_settings)

c_digs() (in module evennia.server.profiling.dummyrunner_settings)

c_creates_obj() (in module evennia.server.profiling.dummyrunner_settings)

c_creates_button() (in module evennia.server.profiling.dummyrunner_settings)

c_socialize() (in module evennia.server.profiling.dummyrunner_settings)

c_moves() (in module evennia.server.profiling.dummyrunner_settings)

c_moves_n() (in module evennia.server.profiling.dummyrunner_settings)

c_moves_s() (in module evennia.server.profiling.dummyrunner_settings)

c_measure_lag() (in module evennia.server.profiling.dummyrunner_settings)

at_script_creation() (evennia.server.profiling.memplot.Memplot method)

count_queries() (in module evennia.server.profiling.test_queries)

TestDummyrunnerSettings (class in evennia.server.profiling.tests)

setUp() (evennia.server.profiling.tests.TestDummyrunnerSettings method)

test_c_login() (evennia.server.profiling.tests.TestDummyrunnerSettings method)

test_c_logout() (evennia.server.profiling.tests.TestDummyrunnerSettings method)

test_c_looks() (evennia.server.profiling.tests.TestDummyrunnerSettings method)

test_idles() (evennia.server.profiling.tests.TestDummyrunnerSettings method)

test_c_help() (evennia.server.profiling.tests.TestDummyrunnerSettings method)

test_c_digs() (evennia.server.profiling.tests.TestDummyrunnerSettings method)

test_c_moves() (evennia.server.profiling.tests.TestDummyrunnerSettings method)

test_c_move_n() (evennia.server.profiling.tests.TestDummyrunnerSettings method)

test_c_move_s() (evennia.server.profiling.tests.TestDummyrunnerSettings method)

test_memplot() (evennia.server.profiling.tests.TestMemPlot method)

cmdset_storage() (evennia.server.serversession.ServerSession property)

at_disconnect() (evennia.server.serversession.ServerSession method)

get_account() (evennia.server.serversession.ServerSession method)

get_puppet() (evennia.server.serversession.ServerSession method)

get_character() (evennia.server.serversession.ServerSession method)

get_puppet_or_account() (evennia.server.serversession.ServerSession method)

get_client_size() (evennia.server.serversession.ServerSession method)

update_session_counters() (evennia.server.serversession.ServerSession method)

update_flags() (evennia.server.serversession.ServerSession method)

execute_cmd() (evennia.server.serversession.ServerSession method)

at_cmdset_get() (evennia.server.serversession.ServerSession method)

nattributes (evennia.server.serversession.ServerSession attribute)

attributes (evennia.server.serversession.ServerSession attribute)

get_display_name() (evennia.server.serversession.ServerSession method)

server_maintenance() (evennia.server.service.EvenniaServerService method)

process_idle_timeouts() (evennia.server.service.EvenniaServerService method)

privilegedStartService() (evennia.server.service.EvenniaServerService method)

register_plugins() (evennia.server.service.EvenniaServerService method)

register_amp() (evennia.server.service.EvenniaServerService method)

register_webserver() (evennia.server.service.EvenniaServerService method)

sqlite3_prep() (evennia.server.service.EvenniaServerService method)

update_defaults() (evennia.server.service.EvenniaServerService method)

run_initial_setup() (evennia.server.service.EvenniaServerService method)

create_default_channels() (evennia.server.service.EvenniaServerService method)

run_init_hooks() (evennia.server.service.EvenniaServerService method)

get_info_dict() (evennia.server.service.EvenniaServerService method)

at_server_init() (evennia.server.service.EvenniaServerService method)

at_server_start() (evennia.server.service.EvenniaServerService method)

at_server_stop() (evennia.server.service.EvenniaServerService method)

at_server_reload_start() (evennia.server.service.EvenniaServerService method)

at_post_portal_sync() (evennia.server.service.EvenniaServerService method)

at_server_reload_stop() (evennia.server.service.EvenniaServerService method)

at_server_cold_start() (evennia.server.service.EvenniaServerService method)

at_server_cold_stop() (evennia.server.service.EvenniaServerService method)

get_sessions() (evennia.server.sessionhandler.SessionHandler method)

get_all_sync_data() (evennia.server.sessionhandler.SessionHandler method)

clean_senddata() (evennia.server.sessionhandler.SessionHandler method)

__init__() (evennia.server.sessionhandler.ServerSessionHandler method)

portal_connect() (evennia.server.sessionhandler.ServerSessionHandler method)

portal_disconnect() (evennia.server.sessionhandler.ServerSessionHandler method)

start_bot_session() (evennia.server.sessionhandler.ServerSessionHandler method)

portal_shutdown() (evennia.server.sessionhandler.ServerSessionHandler method)

login() (evennia.server.sessionhandler.ServerSessionHandler method)

disconnect() (evennia.server.sessionhandler.ServerSessionHandler method)

validate_sessions() (evennia.server.sessionhandler.ServerSessionHandler method)

account_count() (evennia.server.sessionhandler.ServerSessionHandler method)

announce_all() (evennia.server.sessionhandler.ServerSessionHandler method)

data_out() (evennia.server.sessionhandler.ServerSessionHandler method)

get_inputfuncs() (evennia.server.sessionhandler.ServerSessionHandler method)

data_in() (evennia.server.sessionhandler.ServerSessionHandler method)

call_inputfuncs() (evennia.server.sessionhandler.ServerSessionHandler method)

EvenniaUsernameAvailabilityValidator (class in evennia.server.validators)

__init__() (evennia.server.validators.EvenniaPasswordValidator method)

validate() (evennia.server.validators.EvenniaPasswordValidator method)

get_help_text() (evennia.server.validators.EvenniaPasswordValidator method)

callInThread() (evennia.server.webserver.LockableThreadPool method)

HTTPChannelWithXForwardedFor (class in evennia.server.webserver)

getChild() (evennia.server.webserver.EvenniaReverseProxyResource method)

render() (evennia.server.webserver.EvenniaReverseProxyResource method)

empty_threadpool() (evennia.server.webserver.DjangoWebRoot method)

directoryListing() (evennia.server.webserver.PrivateStaticRoot method)

date_created() (evennia.typeclasses.attributes.IAttribute property)

lock_storage() (evennia.typeclasses.attributes.IAttribute property)

__init__() (evennia.typeclasses.attributes.InMemoryAttribute method)

value() (evennia.typeclasses.attributes.InMemoryAttribute property)

attrhandler_name (evennia.typeclasses.attributes.AttributeProperty attribute)

__init__() (evennia.typeclasses.attributes.AttributeProperty method)

at_set() (evennia.typeclasses.attributes.AttributeProperty method)

at_get() (evennia.typeclasses.attributes.AttributeProperty method)

attrhandler_name (evennia.typeclasses.attributes.NAttributeProperty attribute)

db_strvalue (evennia.typeclasses.attributes.Attribute attribute)

db_category (evennia.typeclasses.attributes.Attribute attribute)

db_lock_storage (evennia.typeclasses.attributes.Attribute attribute)

db_attrtype (evennia.typeclasses.attributes.Attribute attribute)

db_date_created (evennia.typeclasses.attributes.Attribute attribute)

lock_storage() (evennia.typeclasses.attributes.Attribute property)

accountdb_set (evennia.typeclasses.attributes.Attribute attribute)

channeldb_set (evennia.typeclasses.attributes.Attribute attribute)

date_created() (evennia.typeclasses.attributes.Attribute property)

get_next_by_db_date_created() (evennia.typeclasses.attributes.Attribute method)

objectdb_set (evennia.typeclasses.attributes.Attribute attribute)

scriptdb_set (evennia.typeclasses.attributes.Attribute attribute)

__init__() (evennia.typeclasses.attributes.IAttributeBackend method)

query_all() (evennia.typeclasses.attributes.IAttributeBackend method)

query_key() (evennia.typeclasses.attributes.IAttributeBackend method)

query_category() (evennia.typeclasses.attributes.IAttributeBackend method)

reset_cache() (evennia.typeclasses.attributes.IAttributeBackend method)

do_create_attribute() (evennia.typeclasses.attributes.IAttributeBackend method)

create_attribute() (evennia.typeclasses.attributes.IAttributeBackend method)

do_update_attribute() (evennia.typeclasses.attributes.IAttributeBackend method)

do_batch_finish() (evennia.typeclasses.attributes.IAttributeBackend method)

batch_add() (evennia.typeclasses.attributes.IAttributeBackend method)

do_delete_attribute() (evennia.typeclasses.attributes.IAttributeBackend method)

delete_attribute() (evennia.typeclasses.attributes.IAttributeBackend method)

update_attribute() (evennia.typeclasses.attributes.IAttributeBackend method)

do_batch_delete() (evennia.typeclasses.attributes.IAttributeBackend method)

clear_attributes() (evennia.typeclasses.attributes.IAttributeBackend method)

get_all_attributes() (evennia.typeclasses.attributes.IAttributeBackend method)

InMemoryAttributeBackend (class in evennia.typeclasses.attributes)

__init__() (evennia.typeclasses.attributes.InMemoryAttributeBackend method)

query_all() (evennia.typeclasses.attributes.InMemoryAttributeBackend method)

query_key() (evennia.typeclasses.attributes.InMemoryAttributeBackend method)

__init__() (evennia.typeclasses.attributes.ModelAttributeBackend method)

query_all() (evennia.typeclasses.attributes.ModelAttributeBackend method)

query_key() (evennia.typeclasses.attributes.ModelAttributeBackend method)

query_category() (evennia.typeclasses.attributes.ModelAttributeBackend method)

do_batch_finish() (evennia.typeclasses.attributes.ModelAttributeBackend method)

__init__() (evennia.typeclasses.attributes.AttributeHandler method)

batch_add() (evennia.typeclasses.attributes.AttributeHandler method)

remove() (evennia.typeclasses.attributes.AttributeHandler method)

clear() (evennia.typeclasses.attributes.AttributeHandler method)

reset_cache() (evennia.typeclasses.attributes.AttributeHandler method)

initialize_nick_templates() (in module evennia.typeclasses.attributes)

parse_nick_template() (in module evennia.typeclasses.attributes)

nickreplace() (evennia.typeclasses.attributes.NickHandler method)

get_attribute() (evennia.typeclasses.managers.TypedObjectManager method)

get_nick() (evennia.typeclasses.managers.TypedObjectManager method)

get_by_attribute() (evennia.typeclasses.managers.TypedObjectManager method)

get_by_nick() (evennia.typeclasses.managers.TypedObjectManager method)

get_tag() (evennia.typeclasses.managers.TypedObjectManager method)

get_permission() (evennia.typeclasses.managers.TypedObjectManager method)

get_alias() (evennia.typeclasses.managers.TypedObjectManager method)

get_by_tag() (evennia.typeclasses.managers.TypedObjectManager method)

get_by_permission() (evennia.typeclasses.managers.TypedObjectManager method)

get_by_alias() (evennia.typeclasses.managers.TypedObjectManager method)

create_tag() (evennia.typeclasses.managers.TypedObjectManager method)

dbref() (evennia.typeclasses.managers.TypedObjectManager method)

get_id() (evennia.typeclasses.managers.TypedObjectManager method)

dbref_search() (evennia.typeclasses.managers.TypedObjectManager method)

search_dbref() (evennia.typeclasses.managers.TypedObjectManager method)

get_dbref_range() (evennia.typeclasses.managers.TypedObjectManager method)

get_typeclass_totals() (evennia.typeclasses.managers.TypedObjectManager method)

object_totals() (evennia.typeclasses.managers.TypedObjectManager method)

typeclass_search() (evennia.typeclasses.managers.TypedObjectManager method)

db_typeclass_path (evennia.typeclasses.models.TypedObject attribute)

db_date_created (evennia.typeclasses.models.TypedObject attribute)

db_lock_storage (evennia.typeclasses.models.TypedObject attribute)

db_attributes (evennia.typeclasses.models.TypedObject attribute)

set_class_from_typeclass() (evennia.typeclasses.models.TypedObject method)

init_evennia_properties() (evennia.typeclasses.models.TypedObject method)

abstract (evennia.typeclasses.models.TypedObject.Meta attribute)

verbose_name (evennia.typeclasses.models.TypedObject.Meta attribute)

ordering (evennia.typeclasses.models.TypedObject.Meta attribute)

at_idmapper_flush() (evennia.typeclasses.models.TypedObject method)

swap_typeclass() (evennia.typeclasses.models.TypedObject method)

check_permstring() (evennia.typeclasses.models.TypedObject method)

get_display_name() (evennia.typeclasses.models.TypedObject method)

get_extra_info() (evennia.typeclasses.models.TypedObject method)

web_get_admin_url() (evennia.typeclasses.models.TypedObject method)

web_get_create_url() (evennia.typeclasses.models.TypedObject class method)

date_created() (evennia.typeclasses.models.TypedObject property)

get_next_by_db_date_created() (evennia.typeclasses.models.TypedObject method)

lock_storage() (evennia.typeclasses.models.TypedObject property)

typeclass_path() (evennia.typeclasses.models.TypedObject property)

web_get_detail_url() (evennia.typeclasses.models.TypedObject method)

web_get_puppet_url() (evennia.typeclasses.models.TypedObject method)

web_get_update_url() (evennia.typeclasses.models.TypedObject method)

web_get_delete_url() (evennia.typeclasses.models.TypedObject method)

get_absolute_url() (evennia.typeclasses.models.TypedObject method)

taghandler_name (evennia.typeclasses.tags.TagProperty attribute)

taghandler_name (evennia.typeclasses.tags.TagCategoryProperty attribute)

__init__() (evennia.typeclasses.tags.TagCategoryProperty method)

taghandler_name (evennia.typeclasses.tags.AliasProperty attribute)

taghandler_name (evennia.typeclasses.tags.PermissionProperty attribute)

ansi_xterm256_bright_bg_map (evennia.utils.ansi.ANSIParser attribute)

ansi_xterm256_bright_bg_map_dict (evennia.utils.ansi.ANSIParser attribute)

parse_file() (evennia.utils.batchprocessors.BatchCommandProcessor method)

parse_file() (evennia.utils.batchprocessors.BatchCodeProcessor method)

code_exec() (evennia.utils.batchprocessors.BatchCodeProcessor method)

storage_modules (evennia.utils.containers.OptionContainer attribute)

__init__() (evennia.utils.containers.GlobalScriptContainer method)

load_data() (evennia.utils.containers.GlobalScriptContainer method)

search_index_entry (evennia.utils.eveditor.CmdSaveYesNo attribute)

at_cmdset_creation() (evennia.utils.eveditor.SaveYesNoCmdSet method)

search_index_entry (evennia.utils.eveditor.CmdEditorBase attribute)

search_index_entry (evennia.utils.eveditor.CmdLineInput attribute)

search_index_entry (evennia.utils.eveditor.CmdEditorGroup attribute)

at_cmdset_creation() (evennia.utils.eveditor.EvEditorCmdSet method)

auto_help_display_key (evennia.utils.evmenu.CmdEvMenuNode attribute)

search_index_entry (evennia.utils.evmenu.CmdEvMenuNode attribute)

search_index_entry (evennia.utils.evmenu.CmdYesNoQuestion attribute)

no_channels (evennia.utils.evmenu.YesNoQuestionCmdSet attribute)

at_cmdset_creation() (evennia.utils.evmenu.YesNoQuestionCmdSet method)

validate_callables() (evennia.utils.funcparser.FuncParser method)

funcparser_callable_toint() (in module evennia.utils.funcparser)

funcparser_callable_int2str() (in module evennia.utils.funcparser)

funcparser_callable_round() (in module evennia.utils.funcparser)

funcparser_callable_random() (in module evennia.utils.funcparser)

funcparser_callable_randint() (in module evennia.utils.funcparser)

funcparser_callable_choice() (in module evennia.utils.funcparser)

funcparser_callable_space() (in module evennia.utils.funcparser)

funcparser_callable_justify() (in module evennia.utils.funcparser)

funcparser_callable_left_justify() (in module evennia.utils.funcparser)

funcparser_callable_right_justify() (in module evennia.utils.funcparser)

funcparser_callable_center_justify() (in module evennia.utils.funcparser)

funcparser_callable_pluralize() (in module evennia.utils.funcparser)

funcparser_callable_search() (in module evennia.utils.funcparser)

funcparser_callable_search_list() (in module evennia.utils.funcparser)

funcparser_callable_you_capitalize() (in module evennia.utils.funcparser)

funcparser_callable_your_capitalize() (in module evennia.utils.funcparser)

funcparser_callable_conjugate() (in module evennia.utils.funcparser)

funcparser_callable_pronoun() (in module evennia.utils.funcparser)

funcparser_callable_pronoun_capitalize() (in module evennia.utils.funcparser)

get() (evennia.utils.idmapper.manager.SharedMemoryManager method)

objects (evennia.utils.idmapper.models.SharedMemoryModel attribute)

abstract (evennia.utils.idmapper.models.SharedMemoryModel.Meta attribute)

get_cached_instance() (evennia.utils.idmapper.models.SharedMemoryModel class method)

cache_instance() (evennia.utils.idmapper.models.SharedMemoryModel class method)

get_all_cached_instances() (evennia.utils.idmapper.models.SharedMemoryModel class method)

flush_cached_instance() (evennia.utils.idmapper.models.SharedMemoryModel class method)

flush_instance_cache() (evennia.utils.idmapper.models.SharedMemoryModel class method)

at_idmapper_flush() (evennia.utils.idmapper.models.SharedMemoryModel method)

flush_from_cache() (evennia.utils.idmapper.models.SharedMemoryModel method)

delete() (evennia.utils.idmapper.models.SharedMemoryModel method)

path (evennia.utils.idmapper.models.SharedMemoryModel attribute)

typename (evennia.utils.idmapper.models.SharedMemoryModel attribute)

WeakSharedMemoryModelBase (class in evennia.utils.idmapper.models)

WeakSharedMemoryModel.Meta (class in evennia.utils.idmapper.models)

abstract (evennia.utils.idmapper.models.WeakSharedMemoryModel.Meta attribute)

path (evennia.utils.idmapper.models.WeakSharedMemoryModel attribute)

typename (evennia.utils.idmapper.models.WeakSharedMemoryModel attribute)

flush_cached_instance() (in module evennia.utils.idmapper.models)

update_cached_instance() (in module evennia.utils.idmapper.models)

regulararticle_set (evennia.utils.idmapper.tests.Category attribute)

article_set (evennia.utils.idmapper.tests.RegularCategory attribute)

objects (evennia.utils.idmapper.tests.RegularCategory attribute)

regulararticle_set (evennia.utils.idmapper.tests.RegularCategory attribute)

category (evennia.utils.idmapper.tests.RegularArticle attribute)

category2 (evennia.utils.idmapper.tests.RegularArticle attribute)

category2_id (evennia.utils.idmapper.tests.RegularArticle attribute)

category_id (evennia.utils.idmapper.tests.RegularArticle attribute)

testRegularReferences() (evennia.utils.idmapper.tests.SharedMemorysTest method)

testMixedReferences() (evennia.utils.idmapper.tests.SharedMemorysTest method)

testObjectDeletion() (evennia.utils.idmapper.tests.SharedMemorysTest method)

component_prefix (evennia.utils.logger.GetLogObserver attribute)

component_prefix (evennia.utils.logger.GetPortalLogObserver attribute)

component_prefix (evennia.utils.logger.GetServerLogObserver attribute)

num_lines_to_append (evennia.utils.logger.EvenniaLogFile attribute)

validator_key (evennia.utils.optionclasses.UnsignedInteger attribute)

deserialize() (evennia.utils.optionclasses.UnsignedInteger method)

deserialize() (evennia.utils.optionclasses.SignedInteger method)

deserialize() (evennia.utils.optionclasses.PositiveInteger method)

__init__() (evennia.utils.optionhandler.InMemorySaveHandler method)

value_from_datadict() (evennia.utils.picklefield.PickledWidget method)

default_error_messages (evennia.utils.picklefield.PickledFormField attribute)

__init__() (evennia.utils.picklefield.PickledObjectField method)

get_default() (evennia.utils.picklefield.PickledObjectField method)

from_db_value() (evennia.utils.picklefield.PickledObjectField method)

formfield() (evennia.utils.picklefield.PickledObjectField method)

pre_save() (evennia.utils.picklefield.PickledObjectField method)

get_db_prep_value() (evennia.utils.picklefield.PickledObjectField method)

value_to_string() (evennia.utils.picklefield.PickledObjectField method)

get_internal_type() (evennia.utils.picklefield.PickledObjectField method)

get_db_prep_lookup() (evennia.utils.picklefield.PickledObjectField method)

account_typeclass (evennia.utils.test_resources.EvenniaTestMixin attribute)

object_typeclass (evennia.utils.test_resources.EvenniaTestMixin attribute)

character_typeclass (evennia.utils.test_resources.EvenniaTestMixin attribute)

exit_typeclass (evennia.utils.test_resources.EvenniaTestMixin attribute)

room_typeclass (evennia.utils.test_resources.EvenniaTestMixin attribute)

script_typeclass (evennia.utils.test_resources.EvenniaTestMixin attribute)

create_accounts() (evennia.utils.test_resources.EvenniaTestMixin method)

teardown_accounts() (evennia.utils.test_resources.EvenniaTestMixin method)

create_rooms() (evennia.utils.test_resources.EvenniaTestMixin method)

create_objs() (evennia.utils.test_resources.EvenniaTestMixin method)

create_chars() (evennia.utils.test_resources.EvenniaTestMixin method)

create_script() (evennia.utils.test_resources.EvenniaTestMixin method)

setup_session() (evennia.utils.test_resources.EvenniaTestMixin method)

teardown_session() (evennia.utils.test_resources.EvenniaTestMixin method)

tearDown() (evennia.utils.test_resources.EvenniaTestMixin method)

call() (evennia.utils.test_resources.EvenniaCommandTestMixin method)

tearDown() (evennia.utils.test_resources.BaseEvenniaTestCase method)

tearDown() (evennia.utils.test_resources.EvenniaTestCase method)

account_typeclass (evennia.utils.test_resources.EvenniaTest attribute)

object_typeclass (evennia.utils.test_resources.EvenniaTest attribute)

character_typeclass (evennia.utils.test_resources.EvenniaTest attribute)

exit_typeclass (evennia.utils.test_resources.EvenniaTest attribute)

room_typeclass (evennia.utils.test_resources.EvenniaTest attribute)

script_typeclass (evennia.utils.test_resources.EvenniaTest attribute)

style_codes (evennia.utils.text2html.TextToHTMLparser attribute)

ansi_color_codes (evennia.utils.text2html.TextToHTMLparser attribute)

xterm_fg_codes (evennia.utils.text2html.TextToHTMLparser attribute)

ansi_bg_codes (evennia.utils.text2html.TextToHTMLparser attribute)

xterm_bg_codes (evennia.utils.text2html.TextToHTMLparser attribute)

re_protocol (evennia.utils.text2html.TextToHTMLparser attribute)

re_valid_no_protocol (evennia.utils.text2html.TextToHTMLparser attribute)

remove_bells() (evennia.utils.text2html.TextToHTMLparser method)

remove_backspaces() (evennia.utils.text2html.TextToHTMLparser method)

convert_linebreaks() (evennia.utils.text2html.TextToHTMLparser method)

convert_urls() (evennia.utils.text2html.TextToHTMLparser method)

sub_mxp_links() (evennia.utils.text2html.TextToHTMLparser method)

sub_mxp_urls() (evennia.utils.text2html.TextToHTMLparser method)

format_styles() (evennia.utils.text2html.TextToHTMLparser method)

verb_infinitive() (in module evennia.utils.verb_conjugation.conjugate)

verb_conjugate() (in module evennia.utils.verb_conjugation.conjugate)

verb_present() (in module evennia.utils.verb_conjugation.conjugate)

verb_present_participle() (in module evennia.utils.verb_conjugation.conjugate)

verb_past() (in module evennia.utils.verb_conjugation.conjugate)

verb_past_participle() (in module evennia.utils.verb_conjugation.conjugate)

verb_all_tenses() (in module evennia.utils.verb_conjugation.conjugate)

verb_tense() (in module evennia.utils.verb_conjugation.conjugate)

verb_is_tense() (in module evennia.utils.verb_conjugation.conjugate)

verb_is_present() (in module evennia.utils.verb_conjugation.conjugate)

verb_is_present_participle() (in module evennia.utils.verb_conjugation.conjugate)

verb_is_past() (in module evennia.utils.verb_conjugation.conjugate)

verb_is_past_participle() (in module evennia.utils.verb_conjugation.conjugate)

verb_actor_stance_components() (in module evennia.utils.verb_conjugation.conjugate)

pronoun_to_viewpoints() (in module evennia.utils.verb_conjugation.pronouns)

TestVerbConjugate (class in evennia.utils.verb_conjugation.tests)

TestPronounMapping (class in evennia.utils.verb_conjugation.tests)

model (evennia.web.admin.accounts.AccountChangeForm.Meta attribute)

fields (evennia.web.admin.accounts.AccountChangeForm.Meta attribute)

clean_username() (evennia.web.admin.accounts.AccountChangeForm method)

__init__() (evennia.web.admin.accounts.AccountChangeForm method)

base_fields (evennia.web.admin.accounts.AccountChangeForm attribute)

declared_fields (evennia.web.admin.accounts.AccountChangeForm attribute)

model (evennia.web.admin.accounts.AccountCreationForm.Meta attribute)

fields (evennia.web.admin.accounts.AccountCreationForm.Meta attribute)

clean_username() (evennia.web.admin.accounts.AccountCreationForm method)

base_fields (evennia.web.admin.accounts.AccountCreationForm attribute)

declared_fields (evennia.web.admin.accounts.AccountCreationForm attribute)

media() (evennia.web.admin.accounts.AccountCreationForm property)

related_field (evennia.web.admin.accounts.AccountTagInline attribute)

model (evennia.web.admin.accounts.AccountAttributeInline attribute)

related_field (evennia.web.admin.accounts.AccountAttributeInline attribute)

media() (evennia.web.admin.accounts.AccountAttributeInline property)

ObjectPuppetInline.ObjectCreateForm (class in evennia.web.admin.accounts)

ObjectPuppetInline.ObjectCreateForm.Meta (class in evennia.web.admin.accounts)

verbose_name (evennia.web.admin.accounts.ObjectPuppetInline attribute)

view_on_site (evennia.web.admin.accounts.ObjectPuppetInline attribute)

show_change_link (evennia.web.admin.accounts.ObjectPuppetInline attribute)

fieldsets (evennia.web.admin.accounts.ObjectPuppetInline attribute)

readonly_fields (evennia.web.admin.accounts.ObjectPuppetInline attribute)

has_add_permission() (evennia.web.admin.accounts.ObjectPuppetInline method)

has_delete_permission() (evennia.web.admin.accounts.ObjectPuppetInline method)

media() (evennia.web.admin.accounts.ObjectPuppetInline property)

list_display (evennia.web.admin.accounts.AccountAdmin attribute)

list_display_links (evennia.web.admin.accounts.AccountAdmin attribute)

search_fields (evennia.web.admin.accounts.AccountAdmin attribute)

readonly_fields (evennia.web.admin.accounts.AccountAdmin attribute)

view_on_site (evennia.web.admin.accounts.AccountAdmin attribute)

add_fieldsets (evennia.web.admin.accounts.AccountAdmin attribute)

serialized_string() (evennia.web.admin.accounts.AccountAdmin method)

puppeted_objects() (evennia.web.admin.accounts.AccountAdmin method)

user_change_password() (evennia.web.admin.accounts.AccountAdmin method)

fields (evennia.web.admin.attributes.AttributeForm.Meta attribute)

clean_attr_value() (evennia.web.admin.attributes.AttributeForm method)

base_fields (evennia.web.admin.attributes.AttributeForm attribute)

declared_fields (evennia.web.admin.attributes.AttributeForm attribute)

verbose_name (evennia.web.admin.attributes.AttributeInline attribute)

verbose_name_plural (evennia.web.admin.attributes.AttributeInline attribute)

formset (evennia.web.admin.attributes.AttributeInline attribute)

related_field (evennia.web.admin.attributes.AttributeInline attribute)

get_formset() (evennia.web.admin.attributes.AttributeInline method)

list_select_related (evennia.web.admin.comms.MsgAdmin attribute)

model (evennia.web.admin.comms.ChannelAttributeInline attribute)

related_field (evennia.web.admin.comms.ChannelAttributeInline attribute)

media() (evennia.web.admin.comms.ChannelAttributeInline property)

related_field (evennia.web.admin.comms.ChannelTagInline attribute)

list_display_links (evennia.web.admin.comms.ChannelAdmin attribute)

readonly_fields (evennia.web.admin.comms.ChannelAdmin attribute)

list_select_related (evennia.web.admin.comms.ChannelAdmin attribute)

no_of_subscribers() (evennia.web.admin.comms.ChannelAdmin method)

serialized_string() (evennia.web.admin.comms.ChannelAdmin method)

declared_fields (evennia.web.admin.help.HelpEntryForm attribute)

list_display_links (evennia.web.admin.help.HelpEntryAdmin attribute)

list_select_related (evennia.web.admin.help.HelpEntryAdmin attribute)

model (evennia.web.admin.objects.ObjectAttributeInline attribute)

related_field (evennia.web.admin.objects.ObjectAttributeInline attribute)

media() (evennia.web.admin.objects.ObjectAttributeInline property)

related_field (evennia.web.admin.objects.ObjectTagInline attribute)

model (evennia.web.admin.objects.ObjectCreateForm.Meta attribute)

fields (evennia.web.admin.objects.ObjectCreateForm.Meta attribute)

base_fields (evennia.web.admin.objects.ObjectCreateForm attribute)

declared_fields (evennia.web.admin.objects.ObjectCreateForm attribute)

fields (evennia.web.admin.objects.ObjectEditForm.Meta attribute)

base_fields (evennia.web.admin.objects.ObjectEditForm attribute)

declared_fields (evennia.web.admin.objects.ObjectEditForm attribute)

list_display_links (evennia.web.admin.objects.ObjectAdmin attribute)

readonly_fields (evennia.web.admin.objects.ObjectAdmin attribute)

list_select_related (evennia.web.admin.objects.ObjectAdmin attribute)

serialized_string() (evennia.web.admin.objects.ObjectAdmin method)

link_object_to_account() (evennia.web.admin.objects.ObjectAdmin method)

declared_fields (evennia.web.admin.scripts.ScriptForm attribute)

related_field (evennia.web.admin.scripts.ScriptTagInline attribute)

model (evennia.web.admin.scripts.ScriptAttributeInline attribute)

related_field (evennia.web.admin.scripts.ScriptAttributeInline attribute)

media() (evennia.web.admin.scripts.ScriptAttributeInline property)

list_display_links (evennia.web.admin.scripts.ScriptAdmin attribute)

readonly_fields (evennia.web.admin.scripts.ScriptAdmin attribute)

list_select_related (evennia.web.admin.scripts.ScriptAdmin attribute)

serialized_string() (evennia.web.admin.scripts.ScriptAdmin method)

list_display (evennia.web.admin.server.ServerConfigAdmin attribute)

list_display_links (evennia.web.admin.server.ServerConfigAdmin attribute)

search_fields (evennia.web.admin.server.ServerConfigAdmin attribute)

save_on_top (evennia.web.admin.server.ServerConfigAdmin attribute)

list_select_related (evennia.web.admin.server.ServerConfigAdmin attribute)

declared_fields (evennia.web.admin.tags.InlineTagForm attribute)

verbose_name_plural (evennia.web.admin.tags.TagFormSet attribute)

verbose_name_plural (evennia.web.admin.tags.TagInline attribute)

filter_name() (evennia.web.api.filters.BaseTypeclassFilterSet static method)

base_filters (evennia.web.api.filters.BaseTypeclassFilterSet attribute)

declared_filters (evennia.web.api.filters.BaseTypeclassFilterSet attribute)

model (evennia.web.api.filters.ObjectDBFilterSet.Meta attribute)

fields (evennia.web.api.filters.ObjectDBFilterSet.Meta attribute)

base_filters (evennia.web.api.filters.ObjectDBFilterSet attribute)

declared_filters (evennia.web.api.filters.ObjectDBFilterSet attribute)

model (evennia.web.api.filters.AccountDBFilterSet.Meta attribute)

fields (evennia.web.api.filters.AccountDBFilterSet.Meta attribute)

base_filters (evennia.web.api.filters.AccountDBFilterSet attribute)

declared_filters (evennia.web.api.filters.AccountDBFilterSet attribute)

model (evennia.web.api.filters.ScriptDBFilterSet.Meta attribute)

fields (evennia.web.api.filters.ScriptDBFilterSet.Meta attribute)

base_filters (evennia.web.api.filters.ScriptDBFilterSet attribute)

declared_filters (evennia.web.api.filters.ScriptDBFilterSet attribute)

declared_filters (evennia.web.api.filters.HelpFilterSet attribute)

view_locks (evennia.web.api.permissions.EvenniaPermission attribute)

destroy_locks (evennia.web.api.permissions.EvenniaPermission attribute)

update_locks (evennia.web.api.permissions.EvenniaPermission attribute)

has_permission() (evennia.web.api.permissions.EvenniaPermission method)

check_locks() (evennia.web.api.permissions.EvenniaPermission static method)

has_object_permission() (evennia.web.api.permissions.EvenniaPermission method)

model (evennia.web.api.serializers.AttributeSerializer.Meta attribute)

fields (evennia.web.api.serializers.AttributeSerializer.Meta attribute)

get_value_display() (evennia.web.api.serializers.AttributeSerializer static method)

model (evennia.web.api.serializers.TagSerializer.Meta attribute)

fields (evennia.web.api.serializers.TagSerializer.Meta attribute)

SimpleObjectDBSerializer.Meta (class in evennia.web.api.serializers)

model (evennia.web.api.serializers.SimpleObjectDBSerializer.Meta attribute)

fields (evennia.web.api.serializers.SimpleObjectDBSerializer.Meta attribute)

shared_fields (evennia.web.api.serializers.TypeclassSerializerMixin attribute)

get_tags() (evennia.web.api.serializers.TypeclassSerializerMixin static method)

get_aliases() (evennia.web.api.serializers.TypeclassSerializerMixin static method)

get_permissions() (evennia.web.api.serializers.TypeclassSerializerMixin static method)

get_attributes() (evennia.web.api.serializers.TypeclassSerializerMixin static method)

get_nicks() (evennia.web.api.serializers.TypeclassSerializerMixin static method)

TypeclassListSerializerMixin (class in evennia.web.api.serializers)

model (evennia.web.api.serializers.ObjectDBSerializer.Meta attribute)

fields (evennia.web.api.serializers.ObjectDBSerializer.Meta attribute)

read_only_fields (evennia.web.api.serializers.ObjectDBSerializer.Meta attribute)

get_exits() (evennia.web.api.serializers.ObjectDBSerializer static method)

get_contents() (evennia.web.api.serializers.ObjectDBSerializer static method)

ObjectListSerializer.Meta (class in evennia.web.api.serializers)

model (evennia.web.api.serializers.ObjectListSerializer.Meta attribute)

fields (evennia.web.api.serializers.ObjectListSerializer.Meta attribute)

get_session_ids() (evennia.web.api.serializers.AccountSerializer static method)

model (evennia.web.api.serializers.AccountSerializer.Meta attribute)

fields (evennia.web.api.serializers.AccountSerializer.Meta attribute)

read_only_fields (evennia.web.api.serializers.AccountSerializer.Meta attribute)

AccountListSerializer.Meta (class in evennia.web.api.serializers)

model (evennia.web.api.serializers.AccountListSerializer.Meta attribute)

fields (evennia.web.api.serializers.AccountListSerializer.Meta attribute)

model (evennia.web.api.serializers.ScriptDBSerializer.Meta attribute)

fields (evennia.web.api.serializers.ScriptDBSerializer.Meta attribute)

read_only_fields (evennia.web.api.serializers.ScriptDBSerializer.Meta attribute)

ScriptListSerializer.Meta (class in evennia.web.api.serializers)

model (evennia.web.api.serializers.ScriptListSerializer.Meta attribute)

fields (evennia.web.api.serializers.ScriptListSerializer.Meta attribute)

model (evennia.web.api.serializers.HelpSerializer.Meta attribute)

fields (evennia.web.api.serializers.HelpSerializer.Meta attribute)

read_only_fields (evennia.web.api.serializers.HelpSerializer.Meta attribute)

model (evennia.web.api.serializers.HelpListSerializer.Meta attribute)

fields (evennia.web.api.serializers.HelpListSerializer.Meta attribute)

read_only_fields (evennia.web.api.serializers.HelpListSerializer.Meta attribute)

client_class (evennia.web.api.tests.TestEvenniaRESTApi attribute)

get_view_details() (evennia.web.api.tests.TestEvenniaRESTApi method)

test_retrieve() (evennia.web.api.tests.TestEvenniaRESTApi method)

test_set_attribute() (evennia.web.api.tests.TestEvenniaRESTApi method)

get_serializer_class() (evennia.web.api.views.GeneralViewSetMixin method)

permission_classes (evennia.web.api.views.TypeclassViewSetMixin attribute)

filter_backends (evennia.web.api.views.TypeclassViewSetMixin attribute)

set_attribute() (evennia.web.api.views.TypeclassViewSetMixin method)

serializer_class (evennia.web.api.views.ObjectDBViewSet attribute)

filterset_class (evennia.web.api.views.ObjectDBViewSet attribute)

list_serializer_class (evennia.web.api.views.ObjectDBViewSet attribute)

serializer_class (evennia.web.api.views.AccountDBViewSet attribute)

filterset_class (evennia.web.api.views.AccountDBViewSet attribute)

list_serializer_class (evennia.web.api.views.AccountDBViewSet attribute)

serializer_class (evennia.web.api.views.ScriptDBViewSet attribute)

filterset_class (evennia.web.api.views.ScriptDBViewSet attribute)

list_serializer_class (evennia.web.api.views.ScriptDBViewSet attribute)

list_serializer_class (evennia.web.api.views.HelpViewSet attribute)

default_site (evennia.web.utils.adminsite.EvenniaAdminApp attribute)

site_header (evennia.web.utils.adminsite.EvenniaAdminSite attribute)

get_app_list() (evennia.web.utils.adminsite.EvenniaAdminSite method)

CaseInsensitiveModelBackend (class in evennia.web.utils.backends)

authenticate() (evennia.web.utils.backends.CaseInsensitiveModelBackend method)

load_game_settings() (in module evennia.web.utils.general_context)

__init__() (evennia.web.utils.middleware.OriginIpMiddleware method)

__init__() (evennia.web.utils.middleware.SharedLoginMiddleware method)

make_shared_login() (evennia.web.utils.middleware.SharedLoginMiddleware class method)

test_general_context() (evennia.web.utils.tests.TestGeneralContext method)

declared_fields (evennia.web.website.forms.EvenniaForm attribute)

field_classes (evennia.web.website.forms.AccountForm.Meta attribute)

declared_fields (evennia.web.website.forms.AccountForm attribute)

declared_fields (evennia.web.website.forms.ObjectForm attribute)

declared_fields (evennia.web.website.forms.CharacterForm attribute)

base_fields (evennia.web.website.forms.CharacterUpdateForm attribute)

declared_fields (evennia.web.website.forms.CharacterUpdateForm attribute)

media() (evennia.web.website.forms.CharacterUpdateForm property)

account_typeclass (evennia.web.website.tests.EvenniaWebTest attribute)

object_typeclass (evennia.web.website.tests.EvenniaWebTest attribute)

character_typeclass (evennia.web.website.tests.EvenniaWebTest attribute)

exit_typeclass (evennia.web.website.tests.EvenniaWebTest attribute)

room_typeclass (evennia.web.website.tests.EvenniaWebTest attribute)

script_typeclass (evennia.web.website.tests.EvenniaWebTest attribute)

channel_typeclass (evennia.web.website.tests.EvenniaWebTest attribute)

unauthenticated_response (evennia.web.website.tests.EvenniaWebTest attribute)

authenticated_response (evennia.web.website.tests.EvenniaWebTest attribute)

test_valid_chars() (evennia.web.website.tests.EvenniaWebTest method)

test_get_authenticated() (evennia.web.website.tests.EvenniaWebTest method)

unauthenticated_response (evennia.web.website.tests.AdminTest attribute)

url_name (evennia.web.website.tests.PasswordResetTest attribute)

unauthenticated_response (evennia.web.website.tests.PasswordResetTest attribute)

test_get_disabled() (evennia.web.website.tests.WebclientTest method)

url_name (evennia.web.website.tests.ChannelDetailTest attribute)

get_kwargs() (evennia.web.website.tests.ChannelDetailTest method)

test_object_cache() (evennia.web.website.tests.HelpDetailTest method)

url_name (evennia.web.website.tests.HelpLockedDetailTest attribute)

get_kwargs() (evennia.web.website.tests.HelpLockedDetailTest method)

test_locked_entry() (evennia.web.website.tests.HelpLockedDetailTest method)

test_lock_with_perm() (evennia.web.website.tests.HelpLockedDetailTest method)

url_name (evennia.web.website.tests.CharacterCreateView attribute)

url_name (evennia.web.website.tests.CharacterPuppetView attribute)

get_kwargs() (evennia.web.website.tests.CharacterPuppetView method)

test_invalid_access() (evennia.web.website.tests.CharacterPuppetView method)

url_name (evennia.web.website.tests.CharacterListView attribute)

unauthenticated_response (evennia.web.website.tests.CharacterListView attribute)

url_name (evennia.web.website.tests.CharacterManageView attribute)

url_name (evennia.web.website.tests.CharacterUpdateView attribute)

get_kwargs() (evennia.web.website.tests.CharacterUpdateView method)

test_valid_access() (evennia.web.website.tests.CharacterUpdateView method)

test_invalid_access() (evennia.web.website.tests.CharacterUpdateView method)

url_name (evennia.web.website.tests.CharacterDeleteView attribute)

get_kwargs() (evennia.web.website.tests.CharacterDeleteView method)

test_valid_access() (evennia.web.website.tests.CharacterDeleteView method)

test_invalid_access() (evennia.web.website.tests.CharacterDeleteView method)

model (evennia.web.website.views.accounts.AccountMixin attribute)

form_class (evennia.web.website.views.accounts.AccountMixin attribute)

template_name (evennia.web.website.views.accounts.AccountCreateView attribute)

success_url (evennia.web.website.views.accounts.AccountCreateView attribute)

form_valid() (evennia.web.website.views.accounts.AccountCreateView method)

model (evennia.web.website.views.channels.ChannelMixin attribute)

page_title (evennia.web.website.views.channels.ChannelMixin attribute)

access_type (evennia.web.website.views.channels.ChannelMixin attribute)

get_queryset() (evennia.web.website.views.channels.ChannelMixin method)

paginate_by (evennia.web.website.views.channels.ChannelListView attribute)

template_name (evennia.web.website.views.channels.ChannelListView attribute)

page_title (evennia.web.website.views.channels.ChannelListView attribute)

max_popular (evennia.web.website.views.channels.ChannelListView attribute)

get_context_data() (evennia.web.website.views.channels.ChannelListView method)

template_name (evennia.web.website.views.channels.ChannelDetailView attribute)

attributes (evennia.web.website.views.channels.ChannelDetailView attribute)

max_num_lines (evennia.web.website.views.channels.ChannelDetailView attribute)

get_context_data() (evennia.web.website.views.channels.ChannelDetailView method)

get_object() (evennia.web.website.views.channels.ChannelDetailView method)

model (evennia.web.website.views.characters.CharacterMixin attribute)

form_class (evennia.web.website.views.characters.CharacterMixin attribute)

success_url (evennia.web.website.views.characters.CharacterMixin attribute)

get_queryset() (evennia.web.website.views.characters.CharacterMixin method)

CharacterListView (class in evennia.web.website.views.characters)

template_name (evennia.web.website.views.characters.CharacterListView attribute)

paginate_by (evennia.web.website.views.characters.CharacterListView attribute)

page_title (evennia.web.website.views.characters.CharacterListView attribute)

access_type (evennia.web.website.views.characters.CharacterListView attribute)

get_queryset() (evennia.web.website.views.characters.CharacterListView method)

CharacterPuppetView (class in evennia.web.website.views.characters)

CharacterManageView (class in evennia.web.website.views.characters)

paginate_by (evennia.web.website.views.characters.CharacterManageView attribute)

page_title (evennia.web.website.views.characters.CharacterManageView attribute)

CharacterUpdateView (class in evennia.web.website.views.characters)

form_class (evennia.web.website.views.characters.CharacterUpdateView attribute)

CharacterDetailView (class in evennia.web.website.views.characters)

attributes (evennia.web.website.views.characters.CharacterDetailView attribute)

access_type (evennia.web.website.views.characters.CharacterDetailView attribute)

get_queryset() (evennia.web.website.views.characters.CharacterDetailView method)

CharacterDeleteView (class in evennia.web.website.views.characters)

form_class (evennia.web.website.views.characters.CharacterDeleteView attribute)

CharacterCreateView (class in evennia.web.website.views.characters)

form_valid() (evennia.web.website.views.characters.CharacterCreateView method)

to_be_implemented() (in module evennia.web.website.views.errors)

get_queryset() (evennia.web.website.views.help.HelpMixin method)

paginate_by (evennia.web.website.views.help.HelpListView attribute)

template_name (evennia.web.website.views.help.HelpListView attribute)

page_title (evennia.web.website.views.help.HelpListView attribute)

template_name (evennia.web.website.views.help.HelpDetailView attribute)

page_title() (evennia.web.website.views.help.HelpDetailView property)

get_context_data() (evennia.web.website.views.help.HelpDetailView method)

get_object() (evennia.web.website.views.help.HelpDetailView method)

template_name (evennia.web.website.views.index.EvenniaIndexView attribute)

get_context_data() (evennia.web.website.views.index.EvenniaIndexView method)

typeclass() (evennia.web.website.views.mixins.TypeclassMixin property)

page_title() (evennia.web.website.views.mixins.EvenniaCreateView property)

page_title() (evennia.web.website.views.mixins.EvenniaDetailView property)

page_title() (evennia.web.website.views.mixins.EvenniaUpdateView property)

page_title() (evennia.web.website.views.mixins.EvenniaDeleteView property)

model (evennia.web.website.views.objects.ObjectDetailView attribute)

template_name (evennia.web.website.views.objects.ObjectDetailView attribute)

access_type (evennia.web.website.views.objects.ObjectDetailView attribute)

attributes (evennia.web.website.views.objects.ObjectDetailView attribute)

get_context_data() (evennia.web.website.views.objects.ObjectDetailView method)

get_object() (evennia.web.website.views.objects.ObjectDetailView method)

model (evennia.web.website.views.objects.ObjectCreateView attribute)

model (evennia.web.website.views.objects.ObjectDeleteView attribute)

template_name (evennia.web.website.views.objects.ObjectDeleteView attribute)

access_type (evennia.web.website.views.objects.ObjectDeleteView attribute)

model (evennia.web.website.views.objects.ObjectUpdateView attribute)

access_type (evennia.web.website.views.objects.ObjectUpdateView attribute)

get_success_url() (evennia.web.website.views.objects.ObjectUpdateView method)

get_initial() (evennia.web.website.views.objects.ObjectUpdateView method)

form_valid() (evennia.web.website.views.objects.ObjectUpdateView method)

coding/changelog:new golden-layout based webclient ui (@friarzen)

coding/setting-up-pycharm:alternative config - utilizing logfiles as source of data

coding/version-control:putting your game dir under version control

components/attributes:what types of data can i save in an attribute?

components/batch-code-processor:no communication between code blocks

components/batch-code-processor:don't treat a batchcode file like any python file

components/batch-code-processor:don't let code rely on the batch-file's real file path

components/batch-command-processor:editor highlighting for .ev files

components/commands:properties assigned to the command instance at run-time

components/locks:a complete example of setting locks on an object

components/tags:properties of tags (and aliases and permissions)

components/typeclasses:difference between typeclasses and classes

components/webclient:a side note on html messages vs text2html messages

concepts/change-message-per-receiver:messages varying per receiver

concepts/change-message-per-receiver:recog systems and roleplaying

concepts/connection-styles:character creation and auto-puppeting

contribs/contrib-building-menu:full sub-menu as separate classes

contribs/contrib-containers:`at_pre_get_from` and `at_pre_put_in` methods

contribs/contrib-extended-room:state-dependent description slots

contribs/contrib-extended-room:changing parts of description based on state

contribs/contrib-ingame-map-display:ascii map (and evennia supports utf-8 characters and even emojis)

contribs/contrib-ingame-python:changing parent classes of typeclasses

contribs/contrib-ingame-python-tutorial-dialogue:dialogues in events

contribs/contrib-ingame-python-tutorial-dialogue:a first example with a first character

contribs/contrib-ingame-python-tutorial-dialogue:the same callback for several keywords

contribs/contrib-ingame-python-tutorial-dialogue:a longer callback

contribs/contrib-ingame-python-tutorial-elevator:a voice operated elevator using events

contribs/contrib-ingame-python-tutorial-elevator:creating the rooms and exits we need

contribs/contrib-ingame-python-tutorial-elevator:our first callback in the elevator

contribs/contrib-ingame-python-tutorial-elevator:our entire callback in the elevator

contribs/contrib-ingame-python-tutorial-elevator:adding a pause in our callback

contribs/contrib-ingame-python-tutorial-elevator:changing exit messages

contribs/contrib-llm:large language model ("chat-bot ai") integration

contribs/contrib-mapbuilder-tutorial:creating rooms from an ascii map

contribs/contrib-rpsystem:language and whisper obfuscation system

contribs/contrib-random-string-generator:pseudo-random generator and registry

evennia-introduction:what do i need to know to work with evennia?

evennia-introduction:i don't know (or don't want to do) any programming - i just want to run a game!

evennia-introduction:i know basic python, or i am willing to learn

evennia-introduction:i know my python stuff and i am willing to use it!

howtos/beginner-tutorial/beginner-tutorial-overview:things you need

howtos/beginner-tutorial/beginner-tutorial-overview:a command line interface

howtos/beginner-tutorial/beginner-tutorial-overview:a fresh game-dir?

howtos/beginner-tutorial/beginner-tutorial-overview:a mud client

howtos/beginner-tutorial/beginner-tutorial-overview:a text editor or ide

howtos/beginner-tutorial/part1/beginner-tutorial-adding-commands:creating a custom command

howtos/beginner-tutorial/part1/beginner-tutorial-adding-commands:making our cmdset persistent

howtos/beginner-tutorial/part1/beginner-tutorial-adding-commands:add the echo command to the default cmdset

howtos/beginner-tutorial/part1/beginner-tutorial-adding-commands:figuring out who to hit

howtos/beginner-tutorial/part1/beginner-tutorial-building-quickstart:using commands and building stuff

howtos/beginner-tutorial/part1/beginner-tutorial-building-quickstart:stepping down from godhood

howtos/beginner-tutorial/part1/beginner-tutorial-building-quickstart:making yourself a house

howtos/beginner-tutorial/part1/beginner-tutorial-building-quickstart:adding a help entry

howtos/beginner-tutorial/part1/beginner-tutorial-creating-things:creating rooms, characters and exits

howtos/beginner-tutorial/part1/beginner-tutorial-creating-things:linking exits and rooms in code

howtos/beginner-tutorial/part1/beginner-tutorial-django-queries:advanced searching - django database queries

howtos/beginner-tutorial/part1/beginner-tutorial-django-queries:let's get that werewolf ...

howtos/beginner-tutorial/part1/beginner-tutorial-django-queries:queries with or or not

howtos/beginner-tutorial/part1/beginner-tutorial-django-queries:grouping and returning only certain properties

howtos/beginner-tutorial/part1/beginner-tutorial-evennia-library-overview:overview of the evennia library

howtos/beginner-tutorial/part1/beginner-tutorial-evennia-library-overview:an example of exploring the library

howtos/beginner-tutorial/part1/beginner-tutorial-gamedir-overview:overview of your new game dir

howtos/beginner-tutorial/part1/beginner-tutorial-learning-typeclasses:our first persistent object

howtos/beginner-tutorial/part1/beginner-tutorial-learning-typeclasses:making a new object by calling the class

howtos/beginner-tutorial/part1/beginner-tutorial-learning-typeclasses:a method on ourselves

howtos/beginner-tutorial/part1/beginner-tutorial-learning-typeclasses:setting things on new characters

howtos/beginner-tutorial/part1/beginner-tutorial-learning-typeclasses:updating all characters in a loop

howtos/beginner-tutorial/part1/beginner-tutorial-making-a-sittable-object:building a chair you can sit on

howtos/beginner-tutorial/part1/beginner-tutorial-making-a-sittable-object:make us not able to move while sitting

howtos/beginner-tutorial/part1/beginner-tutorial-making-a-sittable-object:making the chair itself

howtos/beginner-tutorial/part1/beginner-tutorial-making-a-sittable-object:sitting on or in?

howtos/beginner-tutorial/part1/beginner-tutorial-making-a-sittable-object:command variant 1: commands on the chair

howtos/beginner-tutorial/part1/beginner-tutorial-making-a-sittable-object:command variant 2: command on character

howtos/beginner-tutorial/part1/beginner-tutorial-more-on-commands:adding a command to an object

howtos/beginner-tutorial/part1/beginner-tutorial-more-on-commands:you need to hold the sword!

howtos/beginner-tutorial/part1/beginner-tutorial-more-on-commands:adding the command to a default cmdset

howtos/beginner-tutorial/part1/beginner-tutorial-more-on-commands:replace a default command

howtos/beginner-tutorial/part1/beginner-tutorial-part1-overview:part 1: what we have

howtos/beginner-tutorial/part1/beginner-tutorial-python-basic-introduction:intro to using python with evennia

howtos/beginner-tutorial/part1/beginner-tutorial-python-basic-introduction:making some text 'graphics'

howtos/beginner-tutorial/part1/beginner-tutorial-python-basic-introduction:importing code from other modules

howtos/beginner-tutorial/part1/beginner-tutorial-python-basic-introduction:our first own function

howtos/beginner-tutorial/part1/beginner-tutorial-python-basic-introduction:sending text to others

howtos/beginner-tutorial/part1/beginner-tutorial-python-basic-introduction:passing arguments to functions

howtos/beginner-tutorial/part1/beginner-tutorial-python-basic-introduction:finding others to send to

howtos/beginner-tutorial/part1/beginner-tutorial-python-basic-introduction:other ways to test python code

howtos/beginner-tutorial/part1/beginner-tutorial-python-classes-and-objects:introduction to python classes and objects

howtos/beginner-tutorial/part1/beginner-tutorial-python-classes-and-objects:on classes and objects

howtos/beginner-tutorial/part1/beginner-tutorial-python-classes-and-objects:what's so good about objects?

howtos/beginner-tutorial/part1/beginner-tutorial-python-classes-and-objects:classes can have children

howtos/beginner-tutorial/part1/beginner-tutorial-python-classes-and-objects:a look at multiple inheritance

howtos/beginner-tutorial/part1/beginner-tutorial-searching-things:what can be searched for

howtos/beginner-tutorial/part1/beginner-tutorial-searching-things:search by key

howtos/beginner-tutorial/part1/beginner-tutorial-searching-things:search by tags

howtos/beginner-tutorial/part1/beginner-tutorial-searching-things:finding objects relative each other

howtos/beginner-tutorial/part1/beginner-tutorial-tutorial-world:once you are done (or had enough)

howtos/beginner-tutorial/part1/beginner-tutorial-tutorial-world:uninstall the tutorial world

howtos/beginner-tutorial/part2/beginner-tutorial-game-planning:on planning a game

howtos/beginner-tutorial/part2/beginner-tutorial-game-planning:objects / items

howtos/beginner-tutorial/part2/beginner-tutorial-game-planning:coding and tech demo

howtos/beginner-tutorial/part2/beginner-tutorial-game-planning:planning our tutorial game

howtos/beginner-tutorial/part2/beginner-tutorial-part2-overview:part 2: what we want

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:planning our tutorial game

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:should your game rules be enforced by coded systems by human game masters?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:what is the staff hierarchy in your game? is vanilla evennia roles enough or do you need something else?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:should players be able to post out-of-characters on channels and via other means like bulletin-boards?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:how will the world be built?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:can only privileged builders create things or should regular players also have limited build-capability?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:do you base your game off an existing rpg system or make up your own?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:what are the game mechanics? how do you decide if an action succeeds or fails?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:does the flow of time matter in your game - does night and day change? what about seasons?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:do you want changing, global weather or should weather just be set manually in roleplay?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:do you want a coded world-economy or just a simple barter system? or no formal economy at all?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:do you have concepts like reputation and influence?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:will your characters be known by their name or only by their physical appearance?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:is a simple room description enough or should the description be able to change?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:should the room have different statuses?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:can objects be hidden in the room? can a person hide in the room?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:how numerous are your objects? do you want large loot-lists or are objects just role playing props?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:is each coin a separate object or do you just store a bank account value?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:do multiple similar objects form stack and how are those stacks handled in that case?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:does an object have weight or volume (so you cannot carry an infinite amount of them)?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:can objects be broken? can they be repaired?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:can you fight with a chair or a flower or must you use a special 'weapon' kind of thing?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:will characters be able to craft new objects?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:should mobs/npcs have some sort of ai?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:are npcs and mobs different entities? how do they differ?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:_should there be npcs giving quests? if so, how do you track quest status?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:can players have more than one character active at a time or are they allowed to multi-play?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:how does the character-generation work?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:how do you implement different "classes" or "races"?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:if a character can hide in a room, what skill will decide if they are detected?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:what does the skill tree look like? can a character gain experience to improve? by killing enemies? solving quests? by roleplaying?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:may player-characters attack each other (pvp)?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-the-tutorial-game:what are the penalties of defeat? permanent death? quick respawn? time in prison?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-where-do-i-begin:where do i begin?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-where-do-i-begin:what is your motivation for doing this?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-where-do-i-begin:what are your skills?

howtos/beginner-tutorial/part2/beginner-tutorial-planning-where-do-i-begin:so, where do i begin, then?

howtos/beginner-tutorial/part3/beginner-tutorial-ai:npc and monster ai

howtos/beginner-tutorial/part3/beginner-tutorial-characters:living mixin class

howtos/beginner-tutorial/part3/beginner-tutorial-characters:connecting the character with evennia

howtos/beginner-tutorial/part3/beginner-tutorial-characters:about races and classes

howtos/beginner-tutorial/part3/beginner-tutorial-chargen:how it will work

howtos/beginner-tutorial/part3/beginner-tutorial-chargen:storing state of the menu

howtos/beginner-tutorial/part3/beginner-tutorial-chargen:showing the sheet

howtos/beginner-tutorial/part3/beginner-tutorial-chargen:main node: choosing what to do

howtos/beginner-tutorial/part3/beginner-tutorial-chargen:node: changing your name

howtos/beginner-tutorial/part3/beginner-tutorial-chargen:node: swapping abilities around

howtos/beginner-tutorial/part3/beginner-tutorial-chargen:node: creating the character

howtos/beginner-tutorial/part3/beginner-tutorial-chargen:tying the nodes together

howtos/beginner-tutorial/part3/beginner-tutorial-combat-base:use item action

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:getting the sides of combat

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:adding and removing combatants

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:execute an action and tick the round

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:check and stop combat

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:using evmenu for the combat menu

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:menu for turnbased combat

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:the node index

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:getting or setting the combathandler

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:rerun a node

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:stepping through the wizard

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:choosing targets and recipients

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:choose an item to use or wield

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:the main menu node

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:making sure the menu stops

howtos/beginner-tutorial/part3/beginner-tutorial-combat-turnbased:a small combat test

howtos/beginner-tutorial/part3/beginner-tutorial-combat-twitch:getting the sides of combat

howtos/beginner-tutorial/part3/beginner-tutorial-combat-twitch:tracking advantage / disadvantage

howtos/beginner-tutorial/part3/beginner-tutorial-combat-twitch:execute an action

howtos/beginner-tutorial/part3/beginner-tutorial-combat-twitch:checking and stopping combat

howtos/beginner-tutorial/part3/beginner-tutorial-combat-twitch:base combat class

howtos/beginner-tutorial/part3/beginner-tutorial-combat-twitch:wielding new weapons and equipment

howtos/beginner-tutorial/part3/beginner-tutorial-combat-twitch:grouping commands for use

howtos/beginner-tutorial/part3/beginner-tutorial-combat-twitch:combat startup and cleanup

howtos/beginner-tutorial/part3/beginner-tutorial-combat-twitch:a small combat test

howtos/beginner-tutorial/part3/beginner-tutorial-equipment:`.add` and `.remove`

howtos/beginner-tutorial/part3/beginner-tutorial-equipment:moving things around

howtos/beginner-tutorial/part3/beginner-tutorial-equipment:weapon and armor

howtos/beginner-tutorial/part3/beginner-tutorial-equipment:fixing the character class

howtos/beginner-tutorial/part3/beginner-tutorial-npcs:the npc base class

howtos/beginner-tutorial/part3/beginner-tutorial-objects:in-game objects and items

howtos/beginner-tutorial/part3/beginner-tutorial-objects:the base object

howtos/beginner-tutorial/part3/beginner-tutorial-objects:using attributes or not

howtos/beginner-tutorial/part3/beginner-tutorial-objects:creating tags in `at_object_creation`

howtos/beginner-tutorial/part3/beginner-tutorial-objects:other object types

howtos/beginner-tutorial/part3/beginner-tutorial-objects:your bare hands

howtos/beginner-tutorial/part3/beginner-tutorial-objects:testing and extra credits

howtos/beginner-tutorial/part3/beginner-tutorial-part3-overview:part 3: how we get there (example game)

howtos/beginner-tutorial/part3/beginner-tutorial-rooms:the base room

howtos/beginner-tutorial/part3/beginner-tutorial-rooms:adding a room map

howtos/beginner-tutorial/part3/beginner-tutorial-rooms:adding life to a room

howtos/beginner-tutorial/part3/beginner-tutorial-rules:rules and dice rolling

howtos/beginner-tutorial/part3/beginner-tutorial-rules:summary of _knave_ rules

howtos/beginner-tutorial/part3/beginner-tutorial-rules:making a rule module

howtos/beginner-tutorial/part3/beginner-tutorial-rules:generic dice roller

howtos/beginner-tutorial/part3/beginner-tutorial-rules:rolling with advantage

howtos/beginner-tutorial/part3/beginner-tutorial-rules:opposed saving throw

howtos/beginner-tutorial/part3/beginner-tutorial-rules:roll for healing

howtos/beginner-tutorial/part3/beginner-tutorial-rules:rolling on a table

howtos/beginner-tutorial/part3/beginner-tutorial-rules:roll for death

howtos/beginner-tutorial/part3/beginner-tutorial-rules:mocking and patching

howtos/beginner-tutorial/part3/beginner-tutorial-utilities:code structure and utilities

howtos/beginner-tutorial/part3/beginner-tutorial-utilities:running your test

howtos/beginner-tutorial/part4/beginner-tutorial-part4-overview:part 4: using what we created

howtos/beginner-tutorial/part5/add-a-simple-new-web-page:add a simple new web page

howtos/beginner-tutorial/part5/add-a-simple-new-web-page:create the view

howtos/beginner-tutorial/part5/add-a-simple-new-web-page:the html page

howtos/beginner-tutorial/part5/beginner-tutorial-part5-overview:part 5: showing the world

howtos/evennia-for-diku-users:emulating evennia to look and feel like a diku/rom

howtos/evennia-for-mush-users:collaborating on a game - python vs softcode

howtos/evennia-for-mush-users:`@parent` vs `@typeclass` and `@spawn`

howtos/evennia-for-mush-users:a first step making things more familiar

howtos/evennia-for-mush-users:customizing the multidescer syntax

howtos/evennia-for-roleplaying-sessions:evennia for roleplaying sessions

howtos/evennia-for-roleplaying-sessions:the permission hierarchy

howtos/evennia-for-roleplaying-sessions:how to grant permissions

howtos/evennia-for-roleplaying-sessions:optional: making a gm-granting command

howtos/evennia-for-roleplaying-sessions:building a character sheet

howtos/evennia-for-roleplaying-sessions:making a sheet with evtable

howtos/evennia-for-roleplaying-sessions:making a sheet with evform

howtos/evennia-for-roleplaying-sessions:tie a character sheet to a character

howtos/evennia-for-roleplaying-sessions:command for account to change character sheet

howtos/evennia-for-roleplaying-sessions:commands for gm to change character sheet

howtos/howto-add-object-weight:limit inventory by weight carried

howtos/howto-command-cooldown:make a cooldown-aware command parent

howtos/howto-default-exit-errors:return custom errors on missing exits

howtos/howto-game-time:setting up game time for a standard calendar

howtos/howto-game-time:a command to display the current game time

howtos/implementing-a-game-rule-system:implementing a game rule system

howtos/implementing-a-game-rule-system:overall system infrastructure

howtos/turn-based-combat-system:overview of combat system concepts

howtos/tutorial-coordinates:adding room coordinates to your game

howtos/tutorial-parsing-commands:parsing command arguments, theory and best practices

howtos/tutorial-persistent-handler:making a persistent object handler

howtos/tutorial-persistent-handler:persistent storage of data in handler

howtos/tutorial-persistent-handler:handler with save/load capability

howtos/tutorial-understanding-color-tags:understanding color tags

howtos/tutorial-understanding-color-tags:evennia, ansi and xterm256

howtos/tutorial-understanding-color-tags:ansi color tags in evennia

howtos/tutorial-using-arxcode:setting up the database and starting

howtos/tutorial-weather-effects:adding weather messages to a room

howtos/tutorial-for-basic-mush-like-game:tutorial for basic mush like game

howtos/tutorial-for-basic-mush-like-game:attacking with the +attack command

howtos/tutorial-for-basic-mush-like-game:have "look" show combat scores

howtos/tutorial-for-basic-mush-like-game:creating an npc with +createnpc

howtos/tutorial-for-basic-mush-like-game:editing the npc with +editnpc

howtos/tutorial-for-basic-mush-like-game:making the npc do stuff - the +npc command

howtos/web-character-generation:activating your new character generation

howtos/web-character-generation:adding a no capcha recapcha on your character generation

howtos/web-character-generation:step 1: obtain a sitekey and secret from google

howtos/web-character-generation:step 2: installing and configuring the dedicated django app

howtos/web-character-generation:step 3: adding the capcha to our form

setup/channels-to-discord:connecting an evennia channel to a discord channel

setup/channels-to-grapevine:connect evennia channels to grapevine

setup/choosing-a-database:install and initial setup of postgresql

setup/choosing-a-database:advanced postgresql usage (remote server)

setup/choosing-a-database:installing and initial setup of mysql/mariadb

setup/choosing-a-database:add mysql/mariadb configuration to evennia

setup/client-support-grid:issue: telnet nop displays as spurious character.

setup/config-apache-proxy:running apache as a proxy in front of evennia

setup/config-apache-proxy:evennia secure websocket configuration

setup/config-apache-proxy:run apache instead of the evennia webserver

setup/installation:register with the evennia game index (optional)

setup/installation-docker:run container from your game image for development

setup/installation-troubleshooting:confusion of location (git installation)

setup/security-practices:disable the web interface (if you only want telnet)

api/evennia.contrib.game_systems.crafting.crafting:crafting in game

api/evennia.contrib.game_systems.crafting.crafting:crafting in code

api/evennia.contrib.game_systems.crafting.example_recipes:sword crafting tree

api/evennia.contrib.game_systems.crafting.example_recipes:recipes used for spell casting

api/evennia.contrib.grid.xyzgrid.xymap_legend:map legend components

api/evennia.contrib.rpg.llm.llm_client:optional evennia settings (if not given, these defaults are used)

api/evennia.contrib.rpg.traits.traits:expanding with your own traits

api/evennia.utils.batchprocessors:batch-command processor file syntax

api/evennia.utils.batchprocessors:batch-code processor file syntax

"""
Evennia MUD/MUX/MU* creation system

This is the main top-level API for Evennia. You can explore the evennia library
by accessing evennia.<subpackage> directly. From inside the game you can read
docs of all object by viewing its `__doc__` string, such as through

    py evennia.ObjectDB.__doc__

For full functionality you should explore this module via a django-
aware shell. Go to your game directory and use the command

   evennia shell

to launch such a shell (using python or ipython depending on your install).
See www.evennia.com for full documentation.

"""
import evennia

# docstring header

DOCSTRING = """
Evennia MU* creation system.

Online manual and API docs are found at http://www.evennia.com.

Flat-API shortcut names:
{}
"""

# Delayed loading of properties

# Typeclasses

DefaultAccount = None
DefaultGuest = None
DefaultObject = None
DefaultCharacter = None
DefaultRoom = None
DefaultExit = None
DefaultChannel = None
DefaultScript = None

# Database models
ObjectDB = None
AccountDB = None
ScriptDB = None
ChannelDB = None
Msg = None
ServerConfig = None

# Properties
AttributeProperty = None
TagProperty = None
TagCategoryProperty = None

# commands
Command = None
CmdSet = None
default_cmds = None
syscmdkeys = None
InterruptCommand = None

# search functions
search_object = None
search_script = None
search_account = None
search_channel = None
search_message = None
search_help = None
search_tag = None

# create functions
create_object = None
create_script = None
create_account = None
create_channel = None
create_message = None
create_help_entry = None

# utilities
settings = None
lockfuncs = None
inputhandler = None
logger = None
gametime = None
ansi = None
spawn = None
managers = None
contrib = None
EvMenu = None
EvTable = None
EvForm = None
EvEditor = None
EvMore = None
ANSIString = None
signals = None
FuncParser = None

# Handlers
SESSION_HANDLER = None
PORTAL_SESSION_HANDLER = None
SERVER_SESSION_HANDLER = None
TASK_HANDLER = None
TICKER_HANDLER = None
MONITOR_HANDLER = None

# Containers
GLOBAL_SCRIPTS = None
OPTION_CLASSES = None

PROCESS_ID = None

TWISTED_APPLICATION = None
EVENNIA_PORTAL_SERVICE = None
EVENNIA_SERVER_SERVICE = None

def _create_version():
    """
    Helper function for building the version string
    """
    import os
    from subprocess import STDOUT, CalledProcessError, check_output

    version = "Unknown"
    root = os.path.dirname(os.path.abspath(__file__))
    try:
        with open(os.path.join(root, "VERSION.txt"), "r") as f:
            version = f.read().strip()
    except IOError as err:
        print(err)
    try:
        rev = (
            check_output("git rev-parse --short HEAD", shell=True, cwd=root, stderr=STDOUT)
            .strip()
            .decode()
        )
        version = "%s (rev %s)" % (version, rev)
    except (IOError, CalledProcessError, OSError):
        # ignore if we cannot get to git
        pass
    return version

__version__ = _create_version()
del _create_version

_LOADED = False

PORTAL_MODE = False

def _init(portal_mode=False):
    """
    This function is called automatically by the launcher only after
    Evennia has fully initialized all its models. It sets up the API
    in a safe environment where all models are available already.
    """
    global _LOADED
    if _LOADED:
        return
    _LOADED = True
    global DefaultAccount, DefaultObject, DefaultGuest, DefaultCharacter
    global DefaultRoom, DefaultExit, DefaultChannel, DefaultScript
    global ObjectDB, AccountDB, ScriptDB, ChannelDB, Msg
    global Command, CmdSet, default_cmds, syscmdkeys, InterruptCommand
    global search_object, search_script, search_account, search_channel
    global search_help, search_tag, search_message
    global create_object, create_script, create_account, create_channel
    global create_message, create_help_entry
    global signals
    global settings, lockfuncs, logger, utils, gametime, ansi, spawn, managers
    global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, PROCESS_ID
    global TASK_HANDLER, PORTAL_SESSION_HANDLER, SERVER_SESSION_HANDLER
    global GLOBAL_SCRIPTS, OPTION_CLASSES, EVENNIA_PORTAL_SERVICE, EVENNIA_SERVER_SERVICE, TWISTED_APPLICATION
    global EvMenu, EvTable, EvForm, EvMore, EvEditor
    global ANSIString, FuncParser
    global AttributeProperty, TagProperty, TagCategoryProperty, ServerConfig
    global PORTAL_MODE
    PORTAL_MODE = portal_mode

    # Parent typeclasses
    # utilities
    import os

    from django.conf import settings

    from . import contrib
    from .accounts.accounts import DefaultAccount, DefaultGuest
    from .accounts.models import AccountDB
    from .commands.cmdset import CmdSet
    from .commands.command import Command, InterruptCommand
    from .comms.comms import DefaultChannel
    from .comms.models import ChannelDB, Msg
    from .locks import lockfuncs
    from .objects.models import ObjectDB
    from .objects.objects import (
        DefaultCharacter,
        DefaultExit,
        DefaultObject,
        DefaultRoom,
    )
    from .prototypes.spawner import spawn
    from .scripts.models import ScriptDB
    from .scripts.monitorhandler import MONITOR_HANDLER
    from .scripts.scripts import DefaultScript
    from .scripts.taskhandler import TASK_HANDLER
    from .scripts.tickerhandler import TICKER_HANDLER
    from .server import signals
    from .server.models import ServerConfig
    from .typeclasses.attributes import AttributeProperty
    from .typeclasses.tags import TagCategoryProperty, TagProperty
    from .utils import ansi, class_from_module, gametime, logger
    from .utils.ansi import ANSIString

    if not PORTAL_MODE:
        # containers
        from .utils.containers import GLOBAL_SCRIPTS, OPTION_CLASSES

    # create functions
    from .utils.create import (
        create_account,
        create_channel,
        create_help_entry,
        create_message,
        create_object,
        create_script,
    )
    from .utils.eveditor import EvEditor
    from .utils.evform import EvForm
    from .utils.evmenu import EvMenu
    from .utils.evmore import EvMore
    from .utils.evtable import EvTable
    from .utils.funcparser import FuncParser

    # search functions
    from .utils.search import (
        search_account,
        search_channel,
        search_help,
        search_message,
        search_object,
        search_script,
        search_tag,
    )
    from .utils.utils import class_from_module

    PROCESS_ID = os.getpid()

    from twisted.application.service import Application

    TWISTED_APPLICATION = Application("Evennia")

    _evennia_service_class = None

    if portal_mode:
        # Set up the PortalSessionHandler
        from evennia.server.portal import portalsessionhandler

        portal_sess_handler_class = class_from_module(settings.PORTAL_SESSION_HANDLER_CLASS)
        portalsessionhandler.PORTAL_SESSIONS = portal_sess_handler_class()
        SESSION_HANDLER = portalsessionhandler.PORTAL_SESSIONS
        evennia.PORTAL_SESSION_HANDLER = evennia.SESSION_HANDLER
        _evennia_service_class = class_from_module(settings.EVENNIA_PORTAL_SERVICE_CLASS)
        EVENNIA_PORTAL_SERVICE = _evennia_service_class()
        EVENNIA_PORTAL_SERVICE.setServiceParent(TWISTED_APPLICATION)

        from django.db import connection

        # we don't need a connection to the database so close it right away
        try:
            connection.close()
        except Exception:
            pass

    else:
        # Create the ServerSesssionHandler
        from evennia.server import sessionhandler

        sess_handler_class = class_from_module(settings.SERVER_SESSION_HANDLER_CLASS)
        sessionhandler.SESSIONS = sess_handler_class()
        sessionhandler.SESSION_HANDLER = sessionhandler.SESSIONS
        SESSION_HANDLER = sessionhandler.SESSIONS
        SERVER_SESSION_HANDLER = SESSION_HANDLER
        _evennia_service_class = class_from_module(settings.EVENNIA_SERVER_SERVICE_CLASS)
        EVENNIA_SERVER_SERVICE = _evennia_service_class()
        EVENNIA_SERVER_SERVICE.setServiceParent(TWISTED_APPLICATION)

    # API containers

    class _EvContainer(object):
        """
        Parent for other containers

        """

        def _help(self):
            "Returns list of contents"
            names = [name for name in self.__class__.__dict__ if not name.startswith("_")]
            names += [name for name in self.__dict__ if not name.startswith("_")]
            print(self.__doc__ + "-" * 60 + "\n" + ", ".join(names))

        help = property(_help)

    class DBmanagers(_EvContainer):
        """
        Links to instantiated Django database managers. These are used
        to perform more advanced custom database queries than the standard
        search functions allow.

        helpentries - HelpEntry.objects
        accounts - AccountDB.objects
        scripts - ScriptDB.objects
        msgs    - Msg.objects
        channels - Channel.objects
        objects - ObjectDB.objects
        serverconfigs - ServerConfig.objects
        tags - Tags.objects
        attributes - Attributes.objects

        """

        from .accounts.models import AccountDB
        from .comms.models import ChannelDB, Msg
        from .help.models import HelpEntry
        from .objects.models import ObjectDB
        from .scripts.models import ScriptDB
        from .server.models import ServerConfig
        from .typeclasses.attributes import Attribute
        from .typeclasses.tags import Tag

        # create container's properties
        helpentries = HelpEntry.objects
        accounts = AccountDB.objects
        scripts = ScriptDB.objects
        msgs = Msg.objects
        channels = ChannelDB.objects
        objects = ObjectDB.objects
        serverconfigs = ServerConfig.objects
        attributes = Attribute.objects
        tags = Tag.objects
        # remove these so they are not visible as properties
        del HelpEntry, AccountDB, ScriptDB, Msg, ChannelDB
        # del ExternalChannelConnection
        del ObjectDB, ServerConfig, Tag, Attribute

    managers = DBmanagers()
    del DBmanagers

    class DefaultCmds(_EvContainer):
        """
        This container holds direct shortcuts to all default commands in Evennia.

        To access in code, do 'from evennia import default_cmds' then
        access the properties on the imported default_cmds object.

        """

        from .commands.default.cmdset_account import AccountCmdSet
        from .commands.default.cmdset_character import CharacterCmdSet
        from .commands.default.cmdset_session import SessionCmdSet
        from .commands.default.cmdset_unloggedin import UnloggedinCmdSet
        from .commands.default.muxcommand import MuxAccountCommand, MuxCommand

        def __init__(self):
            "populate the object with commands"

            def add_cmds(module):
                "helper method for populating this object with cmds"
                from evennia.utils import utils

                cmdlist = utils.variable_from_module(module, module.__all__)
                self.__dict__.update(dict([(c.__name__, c) for c in cmdlist]))

            from .commands.default import (
                account,
                admin,
                batchprocess,
                building,
                comms,
                general,
                help,
                system,
                unloggedin,
            )

            add_cmds(admin)
            add_cmds(building)
            add_cmds(batchprocess)
            add_cmds(comms)
            add_cmds(general)
            add_cmds(account)
            add_cmds(help)
            add_cmds(system)
            add_cmds(unloggedin)

    default_cmds = DefaultCmds()
    del DefaultCmds

    class SystemCmds(_EvContainer):
        """
        Creating commands with keys set to these constants will make
        them system commands called as a replacement by the parser when
        special situations occur. If not defined, the hard-coded
        responses in the server are used.

        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_LOGINSTART - this command will be called as the very
                         first command when an account connects to
                         the server.

        To access in code, do 'from evennia import syscmdkeys' then
        access the properties on the imported syscmdkeys object.

        """

        from .commands import cmdhandler

        CMD_NOINPUT = cmdhandler.CMD_NOINPUT
        CMD_NOMATCH = cmdhandler.CMD_NOMATCH
        CMD_MULTIMATCH = cmdhandler.CMD_MULTIMATCH
        CMD_LOGINSTART = cmdhandler.CMD_LOGINSTART
        del cmdhandler

    syscmdkeys = SystemCmds()
    del SystemCmds
    del _EvContainer

def set_trace(term_size=(140, 80), debugger="auto"):
    """
    Helper function for running a debugger inside the Evennia event loop.

    Args:
        term_size (tuple, optional): Only used for Pudb and defines the size of the terminal
            (width, height) in number of characters.
        debugger (str, optional): One of 'auto', 'pdb' or 'pudb'. Pdb is the standard debugger. Pudb
            is an external package with a different, more 'graphical', ncurses-based UI. With
            'auto', will use pudb if possible, otherwise fall back to pdb. Pudb is available through
            `pip install pudb`.

    Notes:
        To use:

        1) add this to a line to act as a breakpoint for entering the debugger:

            from evennia import set_trace; set_trace()

        2) restart evennia in interactive mode

            evennia istart

        3) debugger will appear in the interactive terminal when breakpoint is reached. Exit
           with 'q', remove the break line and restart server when finished.

    """
    import sys

    dbg = None

    if debugger in ("auto", "pudb"):
        try:
            from pudb import debugger

            dbg = debugger.Debugger(stdout=sys.__stdout__, term_size=term_size)
        except ImportError:
            if debugger == "pudb":
                raise
            pass

    if not dbg:
        import pdb

        dbg = pdb.Pdb(stdout=sys.__stdout__)

    try:
        # Start debugger, forcing it up one stack frame (otherwise `set_trace`
        # will start debugger this point, not the actual code location)
        dbg.set_trace(sys._getframe().f_back)
    except Exception:
        # Stopped at breakpoint. Press 'n' to continue into the code.
        dbg.set_trace()

# initialize the doc string
global __doc__
__doc__ = DOCSTRING.format(
    "\n- "
    + "\n- ".join(
        f"evennia.{key}"
        for key in sorted(globals())
        if not key.startswith("_") and key not in ("DOCSTRING",)
    )
)

"""
Typeclass for Account objects.

Note that this object is primarily intended to
store OOC information, not game info! This
object represents the actual user (not their
character) and has NO actual presence in the
game world (this is handled by the associated
character object, so you should customize that
instead for most things).

"""
import re
import time
import typing
from random import getrandbits

from django.conf import settings
from django.contrib.auth import authenticate, password_validation
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.utils import timezone
from django.utils.module_loading import import_string
from django.utils.translation import gettext as _

from evennia.accounts.manager import AccountManager
from evennia.accounts.models import AccountDB
from evennia.commands.cmdsethandler import CmdSetHandler
from evennia.comms.models import ChannelDB
from evennia.objects.models import ObjectDB
from evennia.scripts.scripthandler import ScriptHandler
from evennia.server.models import ServerConfig
from evennia.server.signals import (
    SIGNAL_ACCOUNT_POST_CREATE,
    SIGNAL_ACCOUNT_POST_LOGIN_FAIL,
    SIGNAL_OBJECT_POST_PUPPET,
    SIGNAL_OBJECT_POST_UNPUPPET,
)
from evennia.server.throttle import Throttle
from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler
from evennia.typeclasses.models import TypeclassBase
from evennia.utils import class_from_module, create, logger
from evennia.utils.optionhandler import OptionHandler
from evennia.utils.utils import (
    is_iter,
    lazy_property,
    make_iter,
    to_str,
    variable_from_module,
)

__all__ = ("DefaultAccount", "DefaultGuest")

_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
_MULTISESSION_MODE = settings.MULTISESSION_MODE
_AUTO_CREATE_CHARACTER_WITH_ACCOUNT = settings.AUTO_CREATE_CHARACTER_WITH_ACCOUNT
_AUTO_PUPPET_ON_LOGIN = settings.AUTO_PUPPET_ON_LOGIN
_MAX_NR_SIMULTANEOUS_PUPPETS = settings.MAX_NR_SIMULTANEOUS_PUPPETS
_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
CREATION_THROTTLE = Throttle(
    name="creation",
    limit=settings.CREATION_THROTTLE_LIMIT,
    timeout=settings.CREATION_THROTTLE_TIMEOUT,
)
LOGIN_THROTTLE = Throttle(
    name="login", limit=settings.LOGIN_THROTTLE_LIMIT, timeout=settings.LOGIN_THROTTLE_TIMEOUT
)

class AccountSessionHandler(object):
    """
    Manages the session(s) attached to an account.

    """

    def __init__(self, account):
        """
        Initializes the handler.

        Args:
            account (Account): The Account on which this handler is defined.

        """
        self.account = account

    def get(self, sessid=None):
        """
        Get the sessions linked to this object.

        Args:
            sessid (int, optional): Specify a given session by
                session id.

        Returns:
            sessions (list): A list of Session objects. If `sessid`
                is given, this is a list with one (or zero) elements.

        """
        if sessid:
            return make_iter(evennia.SESSION_HANDLER.session_from_account(self.account, sessid))
        else:
            return evennia.SESSION_HANDLER.sessions_from_account(self.account)

    def all(self):
        """
        Alias to get(), returning all sessions.

        Returns:
            sessions (list): All sessions.

        """
        return self.get()

    def count(self):
        """
        Get amount of sessions connected.

        Returns:
            sesslen (int): Number of sessions handled.

        """
        return len(self.get())

class CharactersHandler:
    """
    A simple Handler that lives on DefaultAccount as .characters via @lazy_property used to
    wrap access to .db._playable_characters.
    """

    def __init__(self, owner: "DefaultAccount"):
        """
        Create the CharactersHandler.

        Args:
            owner: The Account that owns this handler.
        """
        self.owner = owner
        self._ensure_playable_characters()
        self._clean()

    def _ensure_playable_characters(self):
        if self.owner.db._playable_characters is None:
            self.owner.db._playable_characters = []

    def _clean(self):
        # Remove all instances of None from the list.
        self.owner.db._playable_characters = [x for x in self.owner.db._playable_characters if x]

    def add(self, character: "DefaultCharacter"):
        """
        Add a character to this account's list of playable characters.

        Args:
            character (DefaultCharacter): The character to add.
        """
        if character not in self.owner.db._playable_characters:
            self.owner.db._playable_characters.append(character)
            self.owner.at_post_add_character(character)

    def remove(self, character: "DefaultCharacter"):
        """
        Remove a character from this account's list of playable characters.

        Args:
            character (DefaultCharacter): The character to remove.
        """
        if character in self.owner.db._playable_characters:
            self.owner.db._playable_characters.remove(character)
            self.owner.at_post_remove_character(character)

    def all(self) -> list["DefaultCharacter"]:
        """
        Get all playable characters.

        Returns:
            list[DefaultCharacter]: All playable characters.
        """
        return list(self.owner.db._playable_characters)

    def count(self) -> int:
        """
        Get the number of playable characters.

        Returns:
            int: The number of playable characters.
        """
        return len(self.all())

    __len__ = count

    def __iter__(self):
        return iter(self.all())

class DefaultAccount(AccountDB, metaclass=TypeclassBase):
    """
    This is the base Typeclass for all Accounts. Accounts represent
    the person playing the game and tracks account info, password
    etc. They are OOC entities without presence in-game. An Account
    can connect to a Character Object in order to "enter" the
    game.

    Account Typeclass API:

    * Available properties (only available on initiated typeclass objects)

     - key (string) - name of account
     - name (string)- wrapper for user.username
     - aliases (list of strings) - aliases to the object. Will be saved to
            database as AliasDB entries but returned as strings.
     - dbref (int, read-only) - unique #id-number. Also "id" can be used.
     - date_created (string) - time stamp of object creation
     - permissions (list of strings) - list of permission strings
     - user (User, read-only) - django User authorization object
     - obj (Object) - game object controlled by account. 'character' can also
                     be used.
     - sessions (list of Sessions) - sessions connected to this account
     - is_superuser (bool, read-only) - if the connected user is a superuser

    * Handlers

     - locks - lock-handler: use locks.add() to add new lock strings
     - db - attribute-handler: store/retrieve database attributes on this
                              self.db.myattr=val, val=self.db.myattr
     - ndb - non-persistent attribute handler: same as db but does not
                                  create a database entry when storing data
     - scripts - script-handler. Add new scripts to object with scripts.add()
     - cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
     - nicks - nick-handler. New nicks with nicks.add().

    * Helper methods

     - msg(text=None, from_obj=None, session=None, options=None, **kwargs)
     - execute_cmd(raw_string)
     - search(ostring, global_search=False, attribute_name=None,
                      use_nicks=False, location=None,
                      ignore_errors=False, account=False)
     - is_typeclass(typeclass, exact=False)
     - swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
     - access(accessing_obj, access_type='read', default=False, no_superuser_bypass=False)
     - check_permstring(permstring)

    * Hook methods

     basetype_setup()
     at_account_creation()

     > note that the following hooks are also found on Objects and are
       usually handled on the character level:

     - at_init()
     - at_access()
     - at_cmdset_get(**kwargs)
     - at_first_login()
     - at_post_login(session=None)
     - at_disconnect()
     - at_message_receive()
     - at_message_send()
     - at_server_reload()
     - at_server_shutdown()

    """

    objects = AccountManager()

    # Used by account.create_character() to choose default typeclass for characters.
    default_character_typeclass = settings.BASE_CHARACTER_TYPECLASS

    # properties
    @lazy_property
    def cmdset(self):
        return CmdSetHandler(self, True)

    def scripts(self):
        return ScriptHandler(self)

    def nicks(self):
        return NickHandler(self, ModelAttributeBackend)

    def sessions(self):
        return AccountSessionHandler(self)

    def options(self):
        return OptionHandler(
            self,
            options_dict=settings.OPTIONS_ACCOUNT_DEFAULT,
            savefunc=self.attributes.add,
            loadfunc=self.attributes.get,
            save_kwargs={"category": "option"},
            load_kwargs={"category": "option"},
        )

    def characters(self):
        return CharactersHandler(self)

    def at_post_add_character(self, character: "DefaultCharacter"):
        """
        Called after a character is added to this account's list of playable characters.

        Use it to easily implement custom logic when a character is added to an account.

        Args:
            character (DefaultCharacter): The character that was added.
        """
        pass

    def at_post_remove_character(self, character: "DefaultCharacter"):
        """
        Called after a character is removed from this account's list of playable characters.

        Use it to easily implement custom logic when a character is removed from an account.

        Args:
            character (DefaultCharacter): The character that was removed.
        """
        pass

    def uses_screenreader(self, session=None):
        """
        Shortcut to determine if a session uses a screenreader. If no session given,
        will return true if any of the sessions use a screenreader.

        Args:
            session (Session, optional): The session to check for screen reader.

        """
        if session:
            return bool(session.protocol_flags.get("SCREENREADER", False))
        else:
            return any(
                session.protocol_flags.get("SCREENREADER") for session in self.sessions.all()
            )

    def get_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.

        """
        return f"|c{self.key}|n"

    # session-related methods

    def disconnect_session_from_account(self, session, reason=None):
        """
        Access method for disconnecting a given session from the
        account (connection happens automatically in the
        sessionhandler)

        Args:
            session (Session): Session to disconnect.
            reason (str, optional): Eventual reason for the disconnect.

        """
        evennia.SESSION_HANDLER.disconnect(session, reason)

    # puppeting operations

    def puppet_object(self, session, obj):
        """
        Use the given session to control (puppet) the given object (usually
        a Character type).

        Args:
            session (Session): session to use for puppeting
            obj (Object): the object to start puppeting

        Raises:
            RuntimeError: If puppeting is not possible, the
                `exception.msg` will contain the reason.

        """
        # safety checks
        if not obj:
            raise RuntimeError("Object not found")
        if not session:
            raise RuntimeError("Session not found")
        if self.get_puppet(session) == obj:
            # already puppeting this object
            self.msg("You are already puppeting this object.")
            return
        if not obj.access(self, "puppet"):
            # no access
            self.msg(f"You don't have permission to puppet '{obj.key}'.")
            return
        if obj.account:
            # object already puppeted
            if obj.account == self:
                if obj.sessions.count():
                    # we may take over another of our sessions
                    # output messages to the affected sessions
                    if _MULTISESSION_MODE in (1, 3):
                        txt1 = f"Sharing |c{obj.name}|n with another of your sessions."
                        txt2 = f"|c{obj.name}|n|G is now shared from another of your sessions.|n"
                        self.msg(txt1, session=session)
                        self.msg(txt2, session=obj.sessions.all())
                    else:
                        txt1 = f"Taking over |c{obj.name}|n from another of your sessions."
                        txt2 = f"|c{obj.name}|n|R is now acted from another of your sessions.|n"
                        self.unpuppet_object(obj.sessions.get())
            elif obj.account.is_connected:
                # controlled by another account
                self.msg(_("|c{key}|R is already puppeted by another Account.").format(key=obj.key))
                return

        if session.puppet:
            # cleanly unpuppet eventual previous object puppeted by this session
            self.unpuppet_object(session)
        # if we get to this point the character is ready to puppet or it
        # was left with a lingering account/session reference from an unclean
        # server kill or similar

        # check so we are not puppeting too much already
        if _MAX_NR_SIMULTANEOUS_PUPPETS is not None:
            already_puppeted = self.get_all_puppets()
            if (
                not self.is_superuser
                and not self.check_permstring("Developer")
                and obj not in already_puppeted
                and len(self.get_all_puppets()) >= _MAX_NR_SIMULTANEOUS_PUPPETS
            ):
                self.msg(
                    _(f"You cannot control any more puppets (max {_MAX_NR_SIMULTANEOUS_PUPPETS})")
                )
                return

        # do the puppeting
        obj.at_pre_puppet(self, session=session)
        # used to track in case of crash so we can clean up later
        obj.tags.add("puppeted", category="account")

        # do the connection
        obj.sessions.add(session)
        obj.account = self
        session.puid = obj.id
        session.puppet = obj

        # re-cache locks to make sure superuser bypass is updated
        obj.locks.cache_lock_bypass(obj)
        # final hook
        obj.at_post_puppet()
        SIGNAL_OBJECT_POST_PUPPET.send(sender=obj, account=self, session=session)

    def unpuppet_object(self, session):
        """
        Disengage control over an object.

        Args:
            session (Session or list): The session or a list of
                sessions to disengage from their puppets.

        Raises:
            RuntimeError With message about error.

        """
        for session in make_iter(session):
            obj = session.puppet
            if obj:
                # do the disconnect, but only if we are the last session to puppet
                obj.at_pre_unpuppet()
                obj.sessions.remove(session)
                if not obj.sessions.count():
                    del obj.account
                obj.at_post_unpuppet(self, session=session)
                obj.tags.remove("puppeted", category="account")
                SIGNAL_OBJECT_POST_UNPUPPET.send(sender=obj, session=session, account=self)
            # Just to be sure we're always clear.
            session.puppet = None
            session.puid = None

    def unpuppet_all(self):
        """
        Disconnect all puppets. This is called by server before a
        reset/shutdown.
        """
        self.unpuppet_object(self.sessions.all())

    def get_puppet(self, session):
        """
        Get an object puppeted by this session through this account. This is
        the main method for retrieving the puppeted object from the
        account's end.

        Args:
            session (Session): Find puppeted object based on this session

        Returns:
            puppet (Object): The matching puppeted object, if any.

        """
        return session.puppet if session else None

    def get_all_puppets(self):
        """
        Get all currently puppeted objects.

        Returns:
            puppets (list): All puppeted objects currently controlled
                by this Account.

        """
        return list(set(session.puppet for session in self.sessions.all() if session.puppet))

    def __get_single_puppet(self):
        """
        This is a legacy convenience link for use with `MULTISESSION_MODE`.

        Returns:
            puppets (Object or list): Users of `MULTISESSION_MODE` 0 or 1 will
                always get the first puppet back. Users of higher `MULTISESSION_MODE`s will
                get a list of all puppeted objects.

        """
        puppets = self.get_all_puppets()
        if _MULTISESSION_MODE in (0, 1):
            return puppets and puppets[0] or None
        return puppets

    character = property(__get_single_puppet)
    puppet = property(__get_single_puppet)

    # utility methods
    @classmethod
    def is_banned(cls, **kwargs):
        """
        Checks if a given username or IP is banned.

        Keyword Args:
            ip (str, optional): IP address.
            username (str, optional): Username.

        Returns:
            is_banned (bool): Whether either is banned or not.

        """

        ip = kwargs.get("ip", "")
        if isinstance(ip, (tuple, list)):
            ip = ip[0]
        ip = ip.strip()
        username = kwargs.get("username", "").lower().strip()

        # Check IP and/or name bans
        bans = ServerConfig.objects.conf("server_bans")
        if bans and (
            any(tup[0] == username for tup in bans if username)
            or any(tup[2].match(ip) for tup in bans if ip and tup[2])
        ):
            return True

        return False

    def get_username_validators(
        cls, validator_config=getattr(settings, "AUTH_USERNAME_VALIDATORS", [])
    ):
        """
        Retrieves and instantiates validators for usernames.

        Args:
            validator_config (list): List of dicts comprising the battery of
                validators to apply to a username.

        Returns:
            validators (list): List of instantiated Validator objects.
        """
        objs = []
        for validator in validator_config:
            try:
                klass = import_string(validator["NAME"])
            except ImportError:
                msg = (
                    f"The module in NAME could not be imported: {validator['NAME']}. "
                    "Check your AUTH_USERNAME_VALIDATORS setting."
                )
                raise ImproperlyConfigured(msg)
            objs.append(klass(**validator.get("OPTIONS", {})))
        return objs

    def authenticate(cls, username, password, ip="", **kwargs):
        """
        Checks the given username/password against the database to see if the
        credentials are valid.

        Note that this simply checks credentials and returns a valid reference
        to the user-- it does not log them in!

        To finish the job:
        After calling this from a Command, associate the account with a Session:
        - session.sessionhandler.login(session, account)

        ...or after calling this from a View, associate it with an HttpRequest:
        - django.contrib.auth.login(account, request)

        Args:
            username (str): Username of account
            password (str): Password of account
            ip (str, optional): IP address of client

            session (Session, optional): Session requesting authentication

        Returns:
            account (DefaultAccount, None): Account whose credentials were
                provided if not banned.
            errors (list): Error messages of any failures.

        """
        errors = []
        if ip:
            ip = str(ip)

        # See if authentication is currently being throttled
        if ip and LOGIN_THROTTLE.check(ip):
            errors.append(_("Too many login failures; please try again in a few minutes."))

            # With throttle active, do not log continued hits-- it is a
            # waste of storage and can be abused to make your logs harder to
            # read and/or fill up your disk.
            return None, errors

        banned = cls.is_banned(username=username, ip=ip)
        if banned:
            # this is a banned IP or name!
            errors.append(
                _(
                    "|rYou have been banned and cannot continue from here."
                    "\nIf you feel this ban is in error, please email an admin.|x"
                )
            )
            logger.log_sec(f"Authentication Denied (Banned): {username} (IP: {ip}).")
            LOGIN_THROTTLE.update(ip, "Too many sightings of banned artifact.")

        # Authenticate and get Account object
        account = authenticate(username=username, password=password)
        if not account:
            # User-facing message
            errors.append(_("Username and/or password is incorrect."))

            # Log auth failures while throttle is inactive
            logger.log_sec(f"Authentication Failure: {username} (IP: {ip}).")

            # Update throttle
            if ip:
                LOGIN_THROTTLE.update(ip, _("Too many authentication failures."))

            # Try to call post-failure hook
            session = kwargs.get("session", None)
            if session:
                account = AccountDB.objects.get_account_from_name(username)
                if account:
                    SIGNAL_ACCOUNT_POST_LOGIN_FAIL.send(sender=account, session=session)
                    account.at_failed_login(session)

        # Account successfully authenticated
        logger.log_sec(f"Authentication Success: {account} (IP: {ip}).")
        return account, errors

    def normalize_username(cls, username):
        """
        Django: Applies NFKC Unicode normalization to usernames so that visually
        identical characters with different Unicode code points are considered
        identical.

        (This deals with the Turkish "i" problem and similar
        annoyances. Only relevant if you go out of your way to allow Unicode
        usernames though-- Evennia accepts ASCII by default.)

        In this case we're simply piggybacking on this feature to apply
        additional normalization per Evennia's standards.
        """
        username = super(DefaultAccount, cls).normalize_username(username)

        # strip excessive spaces in accountname
        username = re.sub(r"\s+", " ", username).strip()

        return username

    def validate_username(cls, username):
        """
        Checks the given username against the username validator associated with
        Account objects, and also checks the database to make sure it is unique.

        Args:
            username (str): Username to validate

        Returns:
            valid (bool): Whether or not the password passed validation
            errors (list): Error messages of any failures

        """
        valid = []
        errors = []

        # Make sure we're at least using the default validator
        validators = cls.get_username_validators()
        if not validators:
            validators = [cls.username_validator]

        # Try username against all enabled validators
        for validator in validators:
            try:
                valid.append(not validator(username))
            except ValidationError as e:
                valid.append(False)
                errors.extend(e.messages)

        # Disqualify if any check failed
        if False in valid:
            valid = False
        else:
            valid = True

        return valid, errors

    def validate_password(cls, password, account=None):
        """
        Checks the given password against the list of Django validators enabled
        in the server.conf file.

        Args:
            password (str): Password to validate

            account (DefaultAccount, optional): Account object to validate the
                password for. Optional, but Django includes some validators to
                do things like making sure users aren't setting passwords to the
                same value as their username. If left blank, these user-specific
                checks are skipped.

        Returns:
            error (ValidationError, None): Any validation error(s) raised. Multiple
                errors can be nested within a single object.

        """
        valid = False
        error = None

        # Validation returns None on success; invert it and return a more sensible bool
        try:
            valid = not password_validation.validate_password(password, user=account)
        except ValidationError as e:
            error = e

        return valid, error

    def set_password(self, password, **kwargs):
        """
        Applies the given password to the account. Logs and triggers the `at_password_change` hook.

        Args:
            password (str): Password to set.

        Notes:
            This is called by Django also when logging in; it should not be mixed up with
            validation, since that would mean old passwords in the database (pre validation checks)
            could get invalidated.

        """
        super().set_password(password)
        logger.log_sec(f"Password successfully changed for {self}.")
        self.at_password_change()

    def get_character_slots(self) -> typing.Optional[int]:
        """
        Returns the number of character slots this account has, or
        None if there are no limits.

        By default, that's settings.MAX_NR_CHARACTERS but this makes it easy to override.
        Maybe for your game, players can be rewarded with more slots, somehow.

        Returns:
            int (optional): The number of character slots this account has, or None
                if there are no limits.
        """
        return settings.MAX_NR_CHARACTERS

    def get_available_character_slots(self) -> typing.Optional[int]:
        """
        Returns the number of character slots this account has available, or None if
        there are no limits.

        Returns:
            int (optional): The number of open character slots this account has, or None
        """
        if (slots := self.get_character_slots()) is None:
            return None
        return max(0, slots - len(self.characters))

    def check_available_slots(self, **kwargs) -> typing.Optional[str]:
        """
        Helper method used to determine if an account can create additional characters using
        the character slot system.

        Returns:
            str (optional): An error message regarding the status of slots. If present, this
               will halt character creation. If not, character creation can proceed.
        """
        if (slots := self.get_available_character_slots()) is not None:
            if slots <= 0:
                if not (self.is_superuser or self.check_permstring("Developer")):
                    plural = "" if (max_slots := self.get_character_slots()) == 1 else "s"
                    return f"You may only have a maximum of {max_slots} character{plural}."

    def create_character(self, *args, **kwargs):
        """
        Create a character linked to this account.

        Args:
            key (str, optional): If not given, use the same name as the account.
            typeclass (str, optional): Typeclass to use for this character. If
                not given, use self.default_character_class.
            permissions (list, optional): If not given, use the account's permissions.
            ip (str, optional): The client IP creating this character. Will fall back to the
                one stored for the account if not given.
            kwargs (any): Other kwargs will be used in the create_call.
        Returns:
            Object: A new character of the `character_typeclass` type. None on an error.
            list or None: A list of errors, or None.

        """
        # check character slot usage.
        if slot_check := self.check_available_slots():
            return None, [slot_check]

        # parse inputs
        character_key = kwargs.pop("key", self.key)
        character_ip = kwargs.pop("ip", self.db.creator_ip)
        character_permissions = kwargs.pop("permissions", self.permissions)

        # Load the appropriate Character class
        character_typeclass = kwargs.pop("typeclass", self.default_character_typeclass)
        Character = class_from_module(character_typeclass)

        if "location" not in kwargs:
            kwargs["location"] = ObjectDB.objects.get_id(settings.START_LOCATION)

        # Create the character
        character, errs = Character.create(
            character_key,
            self,
            ip=character_ip,
            typeclass=character_typeclass,
            permissions=character_permissions,
            **kwargs,
        )
        if character:
            self.at_post_create_character(character, ip=character_ip)

        return character, errs

    def at_post_create_character(self, character, **kwargs):
        """
        An overloadable hook method that allows for further customization of newly created characters.
        """
        if character not in self.characters:
            self.characters.add(character)

        # We need to set this to have @ic auto-connect to this character
        if len(self.characters) == 1:
            self.db._last_puppet = character

        character.locks.add(
            f"puppet:id({character.id}) or pid({self.id}) or perm(Developer) or pperm(Developer);delete:id({self.id}) or"
            " perm(Admin)"
        )

        logger.log_sec(
            f"Character Created: {character} (Caller: {self}, IP: {kwargs.get('ip', None)})."
        )

    def create(cls, *args, **kwargs):
        """
        Creates an Account (or Account/Character pair for MULTISESSION_MODE<2)
        with default (or overridden) permissions and having joined them to the
        appropriate default channels.

            username (str): Username of Account owner
            password (str): Password of Account owner
            email (str, optional): Email address of Account owner
            ip (str, optional): IP address of requesting connection
            guest (bool, optional): Whether or not this is to be a Guest account

            permissions (str, optional): Default permissions for the Account
            typeclass (str, optional): Typeclass to use for new Account
            character_typeclass (str, optional): Typeclass to use for new char
                when applicable.

        Returns:
            account (Account): Account if successfully created; None if not
            errors (list): List of error messages in string form

        """

        account = None
        errors = []

        username = kwargs.get("username")
        password = kwargs.get("password")
        email = kwargs.get("email", "").strip()
        guest = kwargs.get("guest", False)

        permissions = kwargs.get("permissions", settings.PERMISSION_ACCOUNT_DEFAULT)
        typeclass = kwargs.get("typeclass", cls)

            ip = ip[0]

        if ip and CREATION_THROTTLE.check(ip):
                _("You are creating too many accounts. Please log into an existing account.")
            )

        # Normalize username
        username = cls.normalize_username(username)

        # Validate username
        if not guest:
            valid, errs = cls.validate_username(username)
            if not valid:
                # this echoes the restrictions made by django's auth
                # module (except not allowing spaces, for convenience of
                # logging in).
                errors.extend(errs)
                return None, errors

        # Validate password
        # Have to create a dummy Account object to check username similarity
        valid, errs = cls.validate_password(password, account=cls(username=username))
        if not valid:
            errors.extend(errs)

        if banned:
            string = _(
                "|rYou have been banned and cannot continue from here."
                "\nIf you feel this ban is in error, please email an admin.|x"
            )
            errors.append(string)

        # everything's ok. Create the new account.
        try:
            try:
                account = create.create_account(
                    username, email, password, permissions=permissions, typeclass=typeclass
                )
                logger.log_sec(f"Account Created: {account} (IP: {ip}).")

            except Exception:
                errors.append(
                    _(
                        "There was an error creating the Account. "
                        "If this problem persists, contact an admin."
                    )
                )
                logger.log_trace()

            # This needs to be set so the engine knows this account is
            # logging in for the first time. (so it knows to call the right
            # hooks during login later)
            account.db.FIRST_LOGIN = True

            # Record IP address of creation, if available
            if ip:
                account.db.creator_ip = ip

            # join the new account to the public channels
            for chan_info in settings.DEFAULT_CHANNELS:
                if chankey := chan_info.get("key"):
                    channel = ChannelDB.objects.get_channel(chankey)
                    if not channel or not (
                        channel.access(account, "listen") and channel.connect(account)
                    ):
                        string = (
                            f"New account '{account.key}' could not connect to default channel"
                            f" '{chankey}'!"
                        )
                        logger.log_err(string)
                else:
                    logger.log_err(f"Default channel '{chan_info}' is missing a 'key' field!")

            if account and _AUTO_CREATE_CHARACTER_WITH_ACCOUNT:
                # Auto-create a character to go with this account

                character, errs = account.create_character(
                    typeclass=kwargs.get("character_typeclass", account.default_character_typeclass)
                )
                if errs:
                    errors.extend(errs)

            # We are in the middle between logged in and -not, so we have
            # to handle tracebacks ourselves at this point. If we don't,
            # we won't see any errors at all.
            errors.append(_("An error occurred. Please e-mail an admin if the problem persists."))
            logger.log_trace()

        # Update the throttle to indicate a new account was created from this IP
        if ip and not guest:
            CREATION_THROTTLE.update(ip, "Too many accounts being created.")
        SIGNAL_ACCOUNT_POST_CREATE.send(sender=account, ip=ip)

    def delete(self, *args, **kwargs):
        """
        Deletes the account persistently.

        Notes:
            `*args` and `**kwargs` are passed on to the base delete
             mechanism (these are usually not used).

        Return:
            bool: If deletion was successful. Only time it fails would be
                if the Account was already deleted. Note that even on a failure,
                connected resources (nicks/aliases etc) will still have been
                deleted.

        """
        for session in self.sessions.all():
            # unpuppeting all objects and disconnecting the user, if any
            # sessions remain (should usually be handled from the
            # deleting command)
            try:
                self.unpuppet_object(session)
            except RuntimeError:
                # no puppet to disconnect from
                pass
            session.sessionhandler.disconnect(session, reason=_("Account being deleted."))
        self.scripts.stop()
        self.attributes.clear()
        self.nicks.clear()
        self.aliases.clear()
        if not self.pk:
            return False
        super().delete(*args, **kwargs)
        return True

    # methods inherited from database model

    def msg(self, text=None, from_obj=None, session=None, options=None, **kwargs):
        """
        Evennia -> User
        This is the main route for sending data back to the user from the
        server.

        Args:
            text (str or tuple, optional): The message to send. This
                is treated internally like any send-command, so its
                value can be a tuple if sending multiple arguments to
                the `text` oob command.
            from_obj (Object or Account or list, optional): Object sending. If given, its
                at_msg_send() hook will be called. If iterable, call on all entities.
            session (Session or list, optional): Session object or a list of
                Sessions to receive this send. If given, overrules the
                default send behavior for the current
                MULTISESSION_MODE.
            options (list): Protocol-specific options. Passed on to the protocol.
            any (dict): All other keywords are passed on to the protocol.

        """
        if from_obj:
            # call hook
            for obj in make_iter(from_obj):
                try:
                    obj.at_msg_send(text=text, to_obj=self, **kwargs)
                except Exception:
                    # this may not be assigned.
                    logger.log_trace()
        try:
            if not self.at_msg_receive(text=text, **kwargs):
                # abort message to this account
                return
            # this may not be assigned.
            pass

        kwargs["options"] = options

        if text is not None:
            if not (isinstance(text, str) or isinstance(text, tuple)):
                # sanitize text before sending across the wire
                try:
                    text = to_str(text)
                    text = repr(text)
            kwargs["text"] = text

        # session relay
        sessions = make_iter(session) if session else self.sessions.all()
        for session in sessions:
            session.data_out(**kwargs)

    def execute_cmd(self, raw_string, session=None, **kwargs):
        """
        Do something as this account. This method is never called normally,
        but only when the account object itself is supposed to execute the
        command. It takes account nicks into account, but not nicks of
        eventual puppets.

        Args:
            raw_string (str): Raw command input coming from the command line.
            session (Session, optional): The session to be responsible
                for the command-send

            kwargs (any): Other keyword arguments will be added to the
                found command object instance as variables before it
                executes. This is unused by default Evennia but may be
                used to set flags and change operating parameters for
                commands at run-time.

        """
        # break circular import issues
        global _CMDHANDLER
        if not _CMDHANDLER:
            from evennia.commands.cmdhandler import cmdhandler as _CMDHANDLER
        raw_string = self.nicks.nickreplace(
            raw_string, categories=("inputline", "channel"), include_account=False
        )
        if not session and _MULTISESSION_MODE in (0, 1):
            # for these modes we use the first/only session
            sessions = self.sessions.get()
            session = sessions[0] if sessions else None

        return _CMDHANDLER(self, raw_string, callertype="account", session=session, **kwargs)

    # channel receive hooks

    def at_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 ;.

        """
        if senders:
            sender_string = ", ".join(sender.get_display_name(self) for sender in senders)
            message_lstrip = message.lstrip()
            if message_lstrip.startswith((":", ";")):
                # this is a pose, should show as e.g. "User1 smiles to channel"
                spacing = "" if message_lstrip[1:].startswith((":", "'", ",")) else " "
                message = f"{sender_string}{spacing}{message_lstrip[1:]}"
            else:
                # normal message
                message = f"{sender_string}: {message}"

        if not kwargs.get("no_prefix") and not kwargs.get("emit"):
            message = channel.channel_prefix() + message

        return message

    def channel_msg(self, message, channel, senders=None, **kwargs):
        """
        This performs the actions of receiving a message to an un-muted
        channel.

        Args:
                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},
        )

    def at_post_channel_msg(self, message, channel, senders=None, **kwargs):
        """
        Called by `self.channel_msg` after message was received.

        Args:

        """
        pass

    # search method

    def search(
        self,
        searchdata,
        return_puppet=False,
        search_object=False,
        typeclass=None,
        nofound_string=None,
        multimatch_string=None,
        use_nicks=True,
        quiet=False,
        **kwargs,
    ):
        """
        This is similar to `DefaultObject.search` but defaults to searching
        for Accounts only.

        Args:
            searchdata (str or int): Search criterion, the Account's
                key or dbref to search for.
            return_puppet (bool, optional): Instructs the method to
                return matches as the object the Account controls rather
                than the Account itself (or None) if nothing is puppeted).
            search_object (bool, optional): Search for Objects instead of
                Accounts. This is used by e.g. the @examine command when
                wanting to examine Objects while OOC.
            typeclass (Account typeclass, optional): Limit the search
                only to this particular typeclass. This can be used to
                limit to specific account typeclasses or to limit the search
                to a particular Object typeclass if `search_object` is True.
            nofound_string (str, optional): A one-time error message
                to echo if `searchdata` leads to no matches. If not given,
                will fall back to the default handler.
            multimatch_string (str, optional): A one-time error
                message to echo if `searchdata` leads to multiple matches.
                If not given, will fall back to the default handler.
            use_nicks (bool, optional): Use account-level nick replacement.
            quiet (bool, optional): If set, will not show any error to the user,
                and will also lead to returning a list of matches.

        Return:
            match (Account, Object or None): A single Account or Object match.
            list: If `quiet=True` this is a list of 0, 1 or more Account or Object matches.

        Notes:
            Extra keywords are ignored, but are allowed in call in
            order to make API more consistent with
            objects.objects.DefaultObject.search.

        """
        # handle me, self and *me, *self
        if isinstance(searchdata, str):
            # handle wrapping of common terms
            if searchdata.lower() in ("me", "*me", "self", "*self"):
                return self
        searchdata = self.nicks.nickreplace(
            searchdata, categories=("account",), include_account=False
        )
        if search_object:
            matches = ObjectDB.objects.object_search(searchdata, typeclass=typeclass)
        else:
            matches = AccountDB.objects.account_search(searchdata, typeclass=typeclass)

        if quiet:
            matches = list(matches)
            if return_puppet:
                matches = [match.puppet for match in matches]
        else:
            matches = _AT_SEARCH_RESULT(
                matches,
                self,
                query=searchdata,
                nofound_string=nofound_string,
                multimatch_string=multimatch_string,
            )
            if matches and return_puppet:
                try:
                    matches = matches.puppet
                except AttributeError:
                    return None
        return matches

    def access(
        self, accessing_obj, access_type="read", default=False, no_superuser_bypass=False, **kwargs
    ):
        """
        Determines if another object has permission to access this
        object in whatever way.

        Args:
          accessing_obj (Object): Object trying to access this one.
          access_type (str, optional): Type of access sought.
          default (bool, optional): What to return if no lock of
            access_type was found
          no_superuser_bypass (bool, optional): Turn off superuser
            lock bypassing. Be careful with this one.

          kwargs (any): Passed to the at_access hook along with the result.

        Returns:
            result (bool): Result of access check.

        """
        result = super().access(
            accessing_obj,
            access_type=access_type,
            default=default,
            no_superuser_bypass=no_superuser_bypass,
        )
        self.at_access(result, accessing_obj, access_type, **kwargs)
        return result

    @property
    def idle_time(self):
        """
        Returns the idle time of the least idle session in seconds. If
        no sessions are connected it returns nothing.
        """
        idle = [session.cmd_last_visible for session in self.sessions.all()]
        if idle:
            return time.time() - float(max(idle))
        return None

    @property
    def connection_time(self):
        """
        Returns the maximum connection time of all connected sessions
        in seconds. Returns nothing if there are no sessions.
        """
        conn = [session.conn_time for session in self.sessions.all()]
        if conn:
            return time.time() - float(min(conn))

    # account hooks

    def basetype_setup(self):
        """
        This sets up the basic properties for an account. Overload this
        with at_account_creation rather than changing this method.

        """
        # A basic security setup
        lockstring = (
            "examine:perm(Admin);edit:perm(Admin);"
            "delete:perm(Admin);boot:perm(Admin);msg:all();"
            "noidletimeout:perm(Builder) or perm(noidletimeout)"
        )
        self.locks.add(lockstring)

        # The ooc account cmdset
        self.cmdset.add_default(_CMDSET_ACCOUNT, persistent=True)

    def at_account_creation(self):
        """
        This is called once, the very first time the account is created
        (i.e. first time they register with the game). It's a good
        place to store attributes all accounts should have, like
        configuration values etc.

        """
        # set an (empty) attribute holding the characters this account has
        lockstring = "attrread:perm(Admins);attredit:perm(Admins);attrcreate:perm(Admins);"
        self.attributes.add("_playable_characters", [], lockstring=lockstring)
        self.attributes.add("_saved_protocol_flags", {}, lockstring=lockstring)

    def at_init(self):
        """
        This is always called whenever this object is initiated --
        that is, whenever it its typeclass is cached from memory. This
        happens on-demand first time the object is used or activated
        in some way after being created but also after each server
        restart or reload. In the case of account objects, this usually
        happens the moment the account logs in or reconnects after a
        reload.

        """
        pass

    # Note that the hooks below also exist in the character object's
    # typeclass. You can often ignore these and rely on the character
    # ones instead, unless you are implementing a multi-character game
    # and have some things that should be done regardless of which
    # character is currently connected to this account.

    def at_first_save(self):
        """
        This is a generic hook called by Evennia when this object is
        saved to the database the very first time.  You generally
        don't override this method but the hooks called by it.

        """
        self.basetype_setup()
        self.at_account_creation()
        # initialize Attribute/TagProperties
        self.init_evennia_properties()

        permissions = [settings.PERMISSION_ACCOUNT_DEFAULT]
        if hasattr(self, "_createdict"):
            # this will only be set if the utils.create_account
            # function was used to create the object.
            cdict = self._createdict
            updates = []
            if not cdict.get("key"):
                if not self.db_key:
                    self.db_key = f"#{self.dbid}"
                    updates.append("db_key")
            elif self.key != cdict.get("key"):
                updates.append("db_key")
                self.db_key = cdict["key"]
            if updates:
                self.save(update_fields=updates)

            if cdict.get("locks"):
                self.locks.add(cdict["locks"])
            if cdict.get("permissions"):
                permissions = cdict["permissions"]
            if cdict.get("tags"):
                # this should be a list of tags, tuples (key, category) or (key, category, data)
                self.tags.batch_add(*cdict["tags"])
            if cdict.get("attributes"):
                # this should be tuples (key, val, ...)
                self.attributes.batch_add(*cdict["attributes"])
            if cdict.get("nattributes"):
                # this should be a dict of nattrname:value
                for key, value in cdict["nattributes"]:
                    self.nattributes.add(key, value)
            del self._createdict

        self.permissions.batch_add(*permissions)

    def at_access(self, result, accessing_obj, access_type, **kwargs):
        """
        This is triggered after an access-call on this Account has
            completed.

        Args:
            result (bool): The result of the access check.
            accessing_obj (any): The object requesting the access
                check.
            access_type (str): The type of access checked.

            kwargs (any): These are passed on from the access check
                and can be used to relay custom instructions from the
                check mechanism.

        Notes:
            This method cannot affect the result of the lock check and
            its return value is not used in any way. It can be used
            e.g.  to customize error messages in a central location or
            create other effects based on the access result.

        """
        pass

    def at_cmdset_get(self, **kwargs):
        """
        Called just *before* cmdsets on this account are requested by
        the command handler. The cmdsets are available as
        `self.cmdset`. If changes need to be done on the fly to the
        cmdset before passing them on to the cmdhandler, this is the
        place to do it.  This is called also if the account currently
        have no cmdsets. kwargs are usually not used unless the
        cmdset is generated dynamically.

        """
        pass

    def at_first_login(self, **kwargs):
        """
        Called the very first time this account logs into the game.
        Note that this is called *before* at_pre_login, so no session
        is established and usually no character is yet assigned at
        this point. This hook is intended for account-specific setup
        like configurations.

        Args:
            **kwargs (dict): Arbitrary, optional arguments for users
                overriding the call (unused by default).

        """
        pass

    def at_password_change(self, **kwargs):
        """
        Called after a successful password set/modify.

        Args:

        """
        pass

    def at_pre_login(self, **kwargs):
        """
        Called every time the user logs in, just before the actual
        login-state is set.

        Args:

        """
        pass

    def _send_to_connect_channel(self, message):
        """
        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, _CONNECT_CHANNEL
        if _MUDINFO_CHANNEL is None:
            if settings.CHANNEL_MUDINFO:
                try:
                    _MUDINFO_CHANNEL = ChannelDB.objects.get(db_key=settings.CHANNEL_MUDINFO["key"])
                except ChannelDB.DoesNotExist:
            else:
                _MUDINFO = False
        if _CONNECT_CHANNEL is None:
            if settings.CHANNEL_CONNECTINFO:
                try:
                    _CONNECT_CHANNEL = ChannelDB.objects.get(
                        db_key=settings.CHANNEL_CONNECTINFO["key"]
                    )
            else:
                _CONNECT_CHANNEL = False

        if settings.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.msg(f"[{now}]: {message}")
        if _CONNECT_CHANNEL:
            _CONNECT_CHANNEL.msg(f"[{now}]: {message}")

    def at_post_login(self, session=None, **kwargs):
        """
        Called at the end of the login process, just before letting
        the account loose.

        Args:
            session (Session, optional): Session logging in, if any.

        Notes:
            This is called *before* an eventual Character's
            `at_post_login` hook. By default it is used to set up
            auto-puppeting based on `MULTISESSION_MODE`

        """
        # if we have saved protocol flags on ourselves, load them here.
        protocol_flags = self.attributes.get("_saved_protocol_flags", {})
        if session and protocol_flags:
            session.update_flags(**protocol_flags)

        # inform the client that we logged in through an OOB message
            session.msg(logged_in={})

        self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key))
        if _AUTO_PUPPET_ON_LOGIN:
            # in this mode we try to auto-connect to our last connected object, if any
            try:
                self.puppet_object(session, self.db._last_puppet)
                self.msg(_("The Character does not exist."))
                return
        else:
            # In this mode we don't auto-connect but by default end up at a character selection
            # screen. We execute look on the account.
            self.msg(self.at_look(target=self.characters, session=session), session=session)

    def at_failed_login(self, session, **kwargs):
        """
        Called by the login process if a user account is targeted correctly
        but provided with an invalid password. By default it does nothing,
        but exists to be overridden.

        Args:
            session (session): Session logging in.
        """
        pass

    def at_disconnect(self, reason=None, **kwargs):
        """
        Called just before user is disconnected.

        Args:
            reason (str, optional): The reason given for the disconnect,
                (echoed to the connection channel by default).

        """
        reason = f" ({reason if reason else ''})"
        self._send_to_connect_channel(
            _("|R{key} disconnected{reason}|n").format(key=self.key, reason=reason)
        )

    def at_post_disconnect(self, **kwargs):
        """
        This is called *after* disconnection is complete. No messages
        can be relayed to the account from here. After this call, the
        account should not be accessed any more, making this a good
        spot for deleting it (in the case of a guest account account,
        for example).

        Args:

        """
        pass

    def at_msg_receive(self, text=None, from_obj=None, **kwargs):
        """
        This hook is called whenever someone sends a message to this
        object using the `msg` method.

        Note that from_obj may be None if the sender did not include
        itself as an argument to the obj.msg() call - so you have to
        check for this. .

        Consider this a pre-processing method before msg is passed on
        to the user session. If this method returns False, the msg
        will not be passed on.

        Args:
            text (str, optional): The message received.
            from_obj (any, optional): The object sending the message.

            This includes any keywords sent to the `msg` method.

        Returns:
            receive (bool): If this message should be received.

        Notes:
            If this method returns False, the `msg` operation
            will abort without sending the message.

        """

    def at_msg_send(self, text=None, to_obj=None, **kwargs):
        """
        This is a hook that is called when *this* object sends a
        message to another object with `obj.msg(text, to_obj=obj)`.

        Args:
            text (str, optional): Text to send.
            to_obj (any, optional): The object to send to.

            Keywords passed from msg()

        Notes:
            Since this method is executed by `from_obj`, if no `from_obj`
            was passed to `DefaultCharacter.msg` this hook will never
            get called.

        """
        pass

    def at_server_reload(self):
        """
        This hook is called whenever the server is shutting down for
        restart/reboot. If you want to, for example, save
        non-persistent properties across a restart, this is the place
        to do it.
        """
        pass

    def at_server_shutdown(self):
        """
        This hook is called whenever the server is shutting down fully
        (i.e. not for a restart).
        """
        pass

    ooc_appearance_template = """
--------------------------------------------------------------------
{header}

{sessions}

  |whelp|n - more commands
  |wpublic <text>|n - talk on public channel
  |wcharcreate <name> [=description]|n - create new character
  |wchardelete <name>|n - delete a character
  |wic <name>|n - enter the game as character (|wooc|n to get back here)
  |wic|n - enter the game as latest character controlled.

{characters}
{footer}
""".strip()

    def at_look(self, target=None, session=None, **kwargs):
        """
        Called when this object executes a look. It allows to customize
        just what this means.

        Args:
            target (Object or list, optional): An object or a list
                objects to inspect. This is normally a list of characters.
            session (Session, optional): The session doing this look.

        Returns:
            look_string (str): A prepared look string, ready to send
                off to any recipient (usually to ourselves)

        """

        if target and not is_iter(target):
            # single target - just show it
            if hasattr(target, "return_appearance"):
                return target.return_appearance(self)
            else:
                return f"{target} has no in-game appearance."

        # multiple targets - this is a list of characters
        characters = list(tar for tar in target if tar) if target else []
        ncars = len(characters)
        sessions = self.sessions.all()
        nsess = len(sessions)

        if not nsess:
            # no sessions, nothing to report
            return ""

        # header text
        txt_header = f"Account |g{self.name}|n (you are Out-of-Character)"

        # sessions
        sess_strings = []
        for isess, sess in enumerate(sessions):
            ip_addr = sess.address[0] if isinstance(sess.address, tuple) else sess.address
            addr = f"{sess.protocol_key} ({ip_addr})"
            sess_str = (
                f"|w* {isess + 1}|n"
                if session and session.sessid == sess.sessid
                else f"  {isess + 1}"
            )

            sess_strings.append(f"{sess_str} {addr}")

        txt_sessions = "|wConnected session(s):|n\n" + "\n".join(sess_strings)

        if not characters:
            txt_characters = "You don't have a character yet. Use |wcharcreate|n."
        else:
            max_chars = (
                "unlimited"
                if self.is_superuser or _MAX_NR_CHARACTERS is None
                else _MAX_NR_CHARACTERS
            )

            char_strings = []
            for char in characters:
                csessions = char.sessions.all()
                if csessions:
                    for sess in csessions:
                        # character is already puppeted
                        sid = sess in sessions and sessions.index(sess) + 1
                        if sess and sid:
                            char_strings.append(
                                f" - |G{char.name}|n [{', '.join(char.permissions.all())}] "
                                f"(played by you in session {sid})"
                            )
                        else:
                                f" - |R{char.name}|n [{', '.join(char.permissions.all())}] "
                                "(played by someone else)"
                            )
                else:
                    # character is "free to puppet"
                    char_strings.append(f" - {char.name} [{', '.join(char.permissions.all())}]")

            txt_characters = (
                f"Available character(s) ({ncars}/{max_chars}, |wic <name>|n to play):|n\n"
                + "\n".join(char_strings)
            )
        return self.ooc_appearance_template.format(
            header=txt_header,
            sessions=txt_sessions,
            characters=txt_characters,
            footer="",
        )

class DefaultGuest(DefaultAccount):
    """
    This class is used for guest logins. Unlike Accounts, Guests and
    their characters are deleted after disconnection.

    """

    def create(cls, **kwargs):
        """
        Forwards request to cls.authenticate(); returns a DefaultGuest object
        if one is available for use.

        """
        return cls.authenticate(**kwargs)

    def authenticate(cls, **kwargs):
        """
        Gets or creates a Guest account object.

            ip (str, optional): IP address of requester; used for ban checking,
                throttling and logging

        Returns:
            account (Object): Guest account object, if available
            errors (list): List of error messages accrued during this request.

        """
        errors = []
        username = None
        ip = kwargs.get("ip", "").strip()

        # check if guests are enabled.
        if not settings.GUEST_ENABLED:
            errors.append(_("Guest accounts are not enabled on this server."))

        try:
            # Find an available guest name.
            for name in settings.GUEST_LIST:
                if not AccountDB.objects.filter(username__iexact=name).exists():
                    username = name
                    break
            if not username:
                errors.append(_("All guest accounts are in use. Please try again later."))
                if ip:
                    LOGIN_THROTTLE.update(ip, "Too many requests for Guest access.")
            else:
                # build a new account with the found guest username
                password = "%016x" % getrandbits(64)
                home = settings.GUEST_HOME
                permissions = settings.PERMISSION_GUEST_DEFAULT
                typeclass = settings.BASE_GUEST_TYPECLASS

                # Call parent class creator
                account, errs = super(DefaultGuest, cls).create(
                    guest=True,
                    username=username,
                    password=password,
                    permissions=permissions,
                    typeclass=typeclass,
                    home=home,
                    ip=ip,
                )

                if not account.characters:
                    # this can happen for multisession_mode > 1. For guests we
                    # always auto-create a character, regardless of multi-session-mode.
                    character, errs = account.create_character()

                if errs:

                return account, errors

        """
        By default, Guests only have one character regardless of which
        MAX_NR_CHARACTERS we use. They also always auto-puppet a matching
        character and don't get a choice.

        Args:
            session (Session, optional): Session connecting.

        """
        self.puppet_object(session, self.db._last_puppet)

        """
        We repeat the functionality of `at_disconnect()` here just to
        be on the safe side.
        """
        super().at_server_shutdown()
        for character in self.characters:
            character.delete()

        """
        Once having disconnected, destroy the guest's characters and

        Args:

        """
        super().at_post_disconnect()
        self.delete()

"""
The managers for the custom Account object and permissions.
"""

import datetime

from django.contrib.auth.models import UserManager

from evennia.server import signals
from evennia.typeclasses.managers import TypeclassManager, TypedObjectManager
from evennia.utils.utils import class_from_module, dbid_to_obj, make_iter

__all__ = ("AccountManager", "AccountDBManager")

#
# Account Manager
#

class AccountDBManager(TypedObjectManager, UserManager):
    """
    This AccountManager implements methods for searching
    and manipulating Accounts directly from the database.

    Evennia-specific search methods (will return Characters if
    possible or a Typeclass/list of Typeclassed objects, whereas
    Django-general methods will return Querysets or database objects):

    dbref (converter)
    dbref_search
    get_dbref_range
    object_totals
    typeclass_search
    num_total_accounts
    get_connected_accounts
    get_recently_created_accounts
    get_recently_connected_accounts
    get_account_from_email
    get_account_from_uid
    get_account_from_name
    account_search (equivalent to evennia.search_account)

    """

    def num_total_accounts(self):
        """
        Get total number of accounts.

        Returns:
            count (int): The total number of registered accounts.

        """
        return self.count()

    def get_connected_accounts(self):
        """
        Get all currently connected accounts.

        Returns:
            count (list): Account objects with currently
                connected sessions.

        """
        return self.filter(db_is_connected=True)

    def get_recently_created_accounts(self, days=7):
        """
        Get accounts recently created.

        Args:
            days (int, optional): How many days in the past "recently" means.

        Returns:
            accounts (list): The Accounts created the last `days` interval.

        """
        end_date = timezone.now()
        tdelta = datetime.timedelta(days)
        start_date = end_date - tdelta
        return self.filter(date_joined__range=(start_date, end_date))

    def get_recently_connected_accounts(self, days=7):
        """
        Get accounts recently connected to the game.

        Args:
            days (int, optional): Number of days backwards to check

        Returns:
            accounts (list): The Accounts connected to the game in the
                last `days` interval.

        """
        return self.filter(last_login__range=(start_date, end_date)).order_by("-last_login")

    def get_account_from_email(self, uemail):
        """
        Search account by
        Returns an account object based on email address.

        Args:
            uemail (str): An email address to search for.

        Returns:
            account (Account): A found account, if found.

        """
        return self.filter(email__iexact=uemail)

    def get_account_from_uid(self, uid):
        """
        Get an account by id.

        Args:
            uid (int): Account database id.

        Returns:
            account (Account): The result.

        """
        try:
            return self.get(id=uid)
        except self.model.DoesNotExist:

    def get_account_from_name(self, uname):
        """
        Get account object based on name.

        Args:
            uname (str): The Account name to search for.

        Returns:
            account (Account): The found account.

        """
        try:
            return self.get(username__iexact=uname)

    def search_account(self, ostring, exact=True, typeclass=None):
        """
        Searches for a particular account by name or
        database id.

        Args:
            ostring (str or int): A key string or database id.
            exact (bool, optional): Only valid for string matches. If
                `True`, requires exact (non-case-sensitive) match,
                otherwise also match also keys containing the `ostring`
                (non-case-sensitive fuzzy match).
            typeclass (str or Typeclass, optional): Limit the search only to
                accounts of this typeclass.
        Returns:
            Queryset: A queryset (an iterable) with 0, 1 or more matches.

        """
        dbref = self.dbref(ostring)
        if dbref or dbref == 0:
            # dbref search is always exact
            dbref_match = self.search_dbref(dbref)
            if dbref_match:
                return dbref_match

        query = {"username__iexact" if exact else "username__icontains": ostring}
        if typeclass:
            # we accept both strings and actual typeclasses
            if callable(typeclass):
                typeclass = f"{typeclass.__module__}.{typeclass.__name__}"
            else:
                typeclass = str(typeclass)
            query["db_typeclass_path"] = typeclass
        if exact:
            matches = self.filter(**query)
        else:
        if not matches:
            # try alias match
            matches = self.filter(
                db_tags__db_tagtype__iexact="alias",
                **{"db_tags__db_key__iexact" if exact else "db_tags__db_key__icontains": ostring},
            )

    def create_account(
        self,
        key,
        email,
        password,
        is_superuser=False,
        locks=None,
        permissions=None,
        tags=None,
        attributes=None,
        report_to=None,
    ):
        """
        This creates a new account.

        Args:
            key (str): The account's name. This should be unique.
            email (str or None): Email on valid addr@addr.domain form. If
                the empty string, will be set to None.
            password (str): Password in cleartext.

            typeclass (str): The typeclass to use for the account.
            is_superuser (bool): Whether or not this account is to be a superuser
            locks (str): Lockstring.
            permission (list): List of permission strings.
            tags (list): List of Tags on form `(key, category[, data])`
            attributes (list): List of Attributes on form
                 `(key, value [, category, [,lockstring [, default_pass]]])`
            report_to (Object): An object with a msg() method to report
                errors to. If not given, errors will be logged.

        Returns:
            Account: The newly created Account.
        Raises:
            ValueError: If `key` already exists in database.

        Notes:
            Usually only the server admin should need to be superuser, all
            other access levels can be handled with more fine-grained
            permissions or groups. A superuser bypasses all lock checking
            operations and is thus not suitable for play-testing the game.

        """
        typeclass = typeclass if typeclass else settings.BASE_ACCOUNT_TYPECLASS
        locks = make_iter(locks) if locks is not None else None
        permissions = make_iter(permissions) if permissions is not None else None
        tags = make_iter(tags) if tags is not None else None
        attributes = make_iter(attributes) if attributes is not None else None

        if isinstance(typeclass, str):
            # a path is given. Load the actual typeclass.
            typeclass = class_from_module(typeclass, settings.TYPECLASS_PATHS)

        # setup input for the create command. We use AccountDB as baseclass
        # here to give us maximum freedom (the typeclasses will load
        # correctly when each object is recovered).

        if not email:
            email = None
        if self.model.objects.filter(username__iexact=key):
            raise ValueError("An Account with the name '%s' already exists." % key)

        # this handles a given dbref-relocate to an account.
        report_to = dbid_to_obj(report_to, self.model)

        # create the correct account entity, using the setup from
        # base django auth.
        now = timezone.now()
        email = typeclass.objects.normalize_email(email)
        new_account = typeclass(
            username=key,
            email=email,
            is_staff=is_superuser,
            is_superuser=is_superuser,
            last_login=now,
            date_joined=now,
        )
        if password is not None:
            # the password may be None for 'fake' accounts, like bots
            valid, error = new_account.validate_password(password, new_account)
                raise error

            new_account.set_password(password)

        new_account._createdict = dict(
            locks=locks,
            permissions=permissions,
            report_to=report_to,
            tags=tags,
            attributes=attributes,
        )
        # saving will trigger the signal that calls the
        # at_first_save hook on the typeclass, where the _createdict
        # can be used.
        new_account.save()

        # note that we don't send a signal here, that is sent from the Account.create helper method
        # instead.

        return new_account

    # back-compatibility alias
    account_search = search_account

class AccountManager(AccountDBManager, TypeclassManager):
    pass

"""
Bots are a special child typeclasses of
Account that are  controlled by the server.

"""

from evennia.accounts.accounts import DefaultAccount
from evennia.scripts.scripts import DefaultScript
from evennia.utils import logger, search, utils
from evennia.utils.ansi import strip_ansi

_IDLE_TIMEOUT = settings.IDLE_TIMEOUT

_IRC_ENABLED = settings.IRC_ENABLED
_RSS_ENABLED = settings.RSS_ENABLED
_GRAPEVINE_ENABLED = settings.GRAPEVINE_ENABLED
_DISCORD_ENABLED = settings.DISCORD_ENABLED and hasattr(settings, "DISCORD_BOT_TOKEN")

class BotStarter(DefaultScript):
    """
    This non-repeating script has the
    sole purpose of kicking its bot
    into gear when it is initialized.

    """

    def at_script_creation(self):
        """
        Called once, when script is created.

        """
        self.key = "botstarter"
        self.desc = "bot start/keepalive"
        self.persistent = True

    def at_server_start(self):
        self.at_start()

    def at_start(self):
        """
        Kick bot into gear.

        """
        if not self.account.sessions.all():
            self.account.start()

    def at_repeat(self):
        """
        Called self.interval seconds to keep connection. We cannot use
        the IDLE command from inside the game since the system will
        not catch it (commands executed from the server side usually
        has no sessions). So we update the idle counter manually here
        instead. This keeps the bot getting hit by IDLE_TIMEOUT.

        """
        for session in evennia.SESSION_HANDLER.sessions_from_account(self.account):
            session.update_session_counters(idle=True)

#
# Bot base class

class Bot(DefaultAccount):
    """
    A Bot will start itself when the server starts (it will generally
    not do so on a reload - that will be handled by the normal Portal
    session resync)

    """

        """
        This sets up the basic properties for the bot.

        """
        # the text encoding to use.
        self.db.encoding = "utf-8"
        # A basic security setup (also avoid idle disconnects)
            "examine:perm(Admin);edit:perm(Admin);delete:perm(Admin);"
            "boot:perm(Admin);msg:false();noidletimeout:true()"
        )
        # set the basics of being a bot
        self.scripts.add(BotStarter, key="bot_starter")
        self.is_bot = True

    def start(self, **kwargs):
        """
        This starts the bot, whatever that may mean.

        """
        pass

        """
        Evennia -> outgoing protocol

        """
        super().msg(text=text, from_obj=from_obj, session=session, options=options, **kwargs)

    def execute_cmd(self, raw_string, session=None):
        """
        Incoming protocol -> Evennia

        """
        super().msg(raw_string, session=session)

        """
        We need to handle this case manually since the shutdown may be
        a reset.

        """
            session.sessionhandler.disconnect(session)

# Bot implementations

# IRC

class IRCBot(Bot):
    """
    Bot for handling IRC connections.

    """

    # override this on a child class to use custom factory
    factory_path = "evennia.server.portal.irc.IRCBotFactory"

    def start(
        self,
        ev_channel=None,
        irc_botname=None,
        irc_channel=None,
        irc_network=None,
        irc_port=None,
        irc_ssl=None,
    ):
        """
        Start by telling the portal to start a new session.

        Args:
            ev_channel (str): Key of the Evennia channel to connect to.
            irc_botname (str): Name of bot to connect to irc channel. If
                not set, use `self.key`.
            irc_channel (str): Name of channel on the form `#channelname`.
            irc_network (str): URL of the IRC network, like `irc.freenode.net`.
            irc_port (str): Port number of the irc network, like `6667`.
            irc_ssl (bool): Indicates whether to use SSL connection.

        """
        if not _IRC_ENABLED:
            # the bot was created, then IRC was turned off. We delete
            # ourselves (this will also kill the start script)
            self.delete()
            return

        # if keywords are given, store (the BotStarter script
        # will not give any keywords, so this should normally only
        # happen at initialization)
        if irc_botname:
            self.db.irc_botname = irc_botname
        elif not self.db.irc_botname:
            self.db.irc_botname = self.key
        if ev_channel:
            # connect to Evennia channel
            channel = search.channel_search(ev_channel)
            if not channel:
                raise RuntimeError(f"Evennia Channel '{ev_channel}' not found.")
            channel = channel[0]
            channel.connect(self)
            self.db.ev_channel = channel
        if irc_channel:
            self.db.irc_channel = irc_channel
        if irc_network:
            self.db.irc_network = irc_network
        if irc_port:
            self.db.irc_port = irc_port
        if irc_ssl:
            self.db.irc_ssl = irc_ssl

        # instruct the server and portal to create a new session with
        # the stored configuration
        configdict = {
            "uid": self.dbid,
            "botname": self.db.irc_botname,
            "channel": self.db.irc_channel,
            "network": self.db.irc_network,
            "port": self.db.irc_port,
            "ssl": self.db.irc_ssl,
        }
        evennia.SESSION_HANDLER.start_bot_session(self.factory_path, configdict)

    def at_msg_send(self, **kwargs):
        "Shortcut here or we can end up in infinite loop"
        pass

    def get_nicklist(self, caller):
        """
        Retrive the nick list from the connected channel.

        Args:
            caller (Object or Account): The requester of the list. This will
                be stored and echoed to when the irc network replies with the
                requested info.

        Notes: Since the return is asynchronous, the caller is stored internally
            in a list; all callers in this list will get the nick info once it
            returns (it is a custom OOB inputfunc option). The callback will not
            survive a reload (which should be fine, it's very quick).
        """
        if not hasattr(self, "_nicklist_callers"):
            self._nicklist_callers = []
        self._nicklist_callers.append(caller)
        super().msg(request_nicklist="")
        return

    def ping(self, caller):
        """
        Fire a ping to the IRC server.

        Args:
            caller (Object or Account): The requester of the ping.

        """
        if not hasattr(self, "_ping_callers"):
            self._ping_callers = []
        self._ping_callers.append(caller)
        super().msg(ping="")

    def reconnect(self):
        """
        Force a protocol-side reconnect of the client without
        having to destroy/recreate the bot "account".

        """
        super().msg(reconnect="")

    def msg(self, text=None, **kwargs):
        """
        Takes text from connected channel (only).

        Args:
            text (str, optional): Incoming text from channel.

            options (dict): Options dict with the following allowed keys:
                - from_channel (str): dbid of a channel this text originated from.
                - from_obj (list): list of objects sending this text.

        """
        from_obj = kwargs.get("from_obj", None)
        options = kwargs.get("options", None) or {}

        if not self.ndb.ev_channel and self.db.ev_channel:
            # cache channel lookup
            self.ndb.ev_channel = self.db.ev_channel

        if (
            "from_channel" in options
            and text
            and self.ndb.ev_channel.dbid == options["from_channel"]
        ):
            if not from_obj or from_obj != [self]:
                super().msg(channel=text)

    def execute_cmd(self, session=None, txt=None, **kwargs):
        """
        Take incoming data and send it to connected channel. This is
        triggered by the bot_data_in Inputfunc.

        Args:
            session (Session, optional): Session responsible for this
                command. Note that this is the bot.
            txt (str, optional):  Command string.
            user (str): The name of the user who sent the message.
            channel (str): The name of channel the message was sent to.
            type (str): Nature of message. Either 'msg', 'action', 'nicklist'
                or 'ping'.
            nicklist (list, optional): Set if `type='nicklist'`. This is a list
                of nicks returned by calling the `self.get_nicklist`. It must look
                for a list `self._nicklist_callers` which will contain all callers
                waiting for the nicklist.
            timings (float, optional): Set if `type='ping'`. This is the return
                (in seconds) of a ping request triggered with `self.ping`. The
                return must look for a list `self._ping_callers` which will contain
                all callers waiting for the ping return.

        """
        if kwargs["type"] == "nicklist":
            # the return of a nicklist request
            if hasattr(self, "_nicklist_callers") and self._nicklist_callers:
                chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})"
                nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower()))
                for obj in self._nicklist_callers:
                    obj.msg("Nicks at {chstr}:\n {nicklist}".format(chstr=chstr, nicklist=nicklist))
                self._nicklist_callers = []
            return

        elif kwargs["type"] == "ping":
            # the return of a ping
            if hasattr(self, "_ping_callers") and self._ping_callers:
                for obj in self._ping_callers:
                    obj.msg(
                        "IRC ping return from {chstr} took {time}s.".format(
                            chstr=chstr, time=kwargs["timing"]
                        )
                    )
                self._ping_callers = []
            return

        elif kwargs["type"] == "privmsg":
            # A private message to the bot - a command.
            user = kwargs["user"]

            if txt.lower().startswith("who"):
                # return server WHO list (abbreviated for IRC)
                whos = []
                t0 = time.time()
                for sess in evennia.SESSION_HANDLER.get_sessions():
                    delta_cmd = t0 - sess.cmd_last_visible
                    delta_conn = t0 - session.conn_time
                    account = sess.get_account()
                    whos.append(
                        "%s (%s/%s)"
                        % (
                            utils.crop("|w%s|n" % account.name, width=25),
                            utils.time_format(delta_conn, 0),
                            utils.time_format(delta_cmd, 1),
                        )
                    )
                text = f"Who list (online/idle): {', '.join(sorted(whos, key=lambda w: w.lower()))}"
            elif txt.lower().startswith("about"):
                # some bot info
                text = f"This is an Evennia IRC bot connecting from '{settings.SERVERNAME}'."
            else:
                text = "I understand 'who' and 'about'."
            super().msg(privmsg=((text,), {"user": user}))
        else:
            # something to send to the main channel
            if kwargs["type"] == "action":
                # An action (irc pose)
                text = f"{kwargs['user']}@{kwargs['channel']} {txt}"
            else:
                # msg - A normal channel message
                text = f"{kwargs['user']}@{kwargs['channel']}: {txt}"

            if not self.ndb.ev_channel and self.db.ev_channel:
                # cache channel lookup
                self.ndb.ev_channel = self.db.ev_channel

            if self.ndb.ev_channel:
                self.ndb.ev_channel.msg(text, senders=self)

#
# RSS
#

class RSSBot(Bot):
    """
    An RSS relayer. The RSS protocol itself runs a ticker to update
    its feed at regular intervals.

    """

    def start(self, ev_channel=None, rss_url=None, rss_rate=None):
        """
        Start by telling the portal to start a new RSS session

        Args:
            rss_url (str): Full URL to the RSS feed to subscribe to.
            rss_rate (int): How often for the feedreader to update.

        Raises:
            RuntimeError: If `ev_channel` does not exist.

        """
        if not _RSS_ENABLED:
            # The bot was created, then RSS was turned off. Delete ourselves.
            return

        if rss_url:
            self.db.rss_url = rss_url
        if rss_rate:
            self.db.rss_rate = rss_rate
        configdict = {"uid": self.dbid, "url": self.db.rss_url, "rate": self.db.rss_rate}
        evennia.SESSION_HANDLER.start_bot_session(
            "evennia.server.portal.rss.RSSBotFactory", configdict
        )

    def execute_cmd(self, txt=None, session=None, **kwargs):
        """

        Args:
                command.
            kwargs (dict, optional): Additional Information passed from bot.
                Not used by the RSSbot by default.

        """
        if self.ndb.ev_channel:
            self.ndb.ev_channel.msg(txt, senders=self.id)

# Grapevine bot

class GrapevineBot(Bot):
    """
    g Grapevine (https://grapevine.haus) relayer. The channel to connect to is the first
    name in the settings.GRAPEVINE_CHANNELS list.

    """

    factory_path = "evennia.server.portal.grapevine.RestartingWebsocketServerFactory"

    def start(self, ev_channel=None, grapevine_channel=None):
        """
        Start by telling the portal to connect to the grapevine network.

        """
        if not _GRAPEVINE_ENABLED:
            return

        # connect to Evennia channel

        if grapevine_channel:
            self.db.grapevine_channel = grapevine_channel

        # these will be made available as properties on the protocol factory
        configdict = {"uid": self.dbid, "grapevine_channel": self.db.grapevine_channel}

        pass

        """

        Args:

        """

        if (
            and text
        ):
                # send outputfunc channel(msg, chan, sender)

                text = text[0] if isinstance(text, (tuple, list)) else text

                prefix, text = text.split(":", 1)

                super().msg(
                    channel=(
                        text.strip(),
                        self.db.grapevine_channel,
                        ", ".join(obj.key for obj in from_obj),
                        {},
                    )
                )

    def execute_cmd(
        self,
        txt=None,
        session=None,
        event=None,
        grapevine_channel=None,
        sender=None,
        game=None,
        **kwargs,
    ):
        """
        Take incoming data from protocol and send it to connected channel. This is
        """
        if event == "channels/broadcast":

            text = f"{sender}@{game}: {txt}"

                # simple cache of channel lookup

# Discord

class DiscordBot(Bot):
    """
    Discord bot relay. You will need to set up your own bot
    (https://discord.com/developers/applications) and add the bot token as `DISCORD_BOT_TOKEN` to
    `secret_settings.py` to use
    """

    factory_path = "evennia.server.portal.discord.DiscordWebsocketServerFactory"

        """
        Load required channels back into memory

        """
        if channel_links := self.db.channels:
            # this attribute contains a list of evennia<->discord links in the form
            # of ("evennia_channel", "discord_chan_id")
            # grab Evennia channels, cache and connect
            channel_set = {evchan for evchan, dcid in channel_links}
            self.ndb.ev_channels = {}
            for channel_name in list(channel_set):
                channel = search.search_channel(channel_name)
                if not channel:
                    raise RuntimeError(f"Evennia Channel {channel_name} not found.")
                channel = channel[0]
                self.ndb.ev_channels[channel_name] = channel

    def start(self):
        """
        Tell the Discord protocol to connect.

        """
        if not _DISCORD_ENABLED:
            return

        if self.ndb.ev_channels:
            for channel in self.ndb.ev_channels.values():
                channel.connect(self)

        elif channel_links := self.db.channels:

        # connect
        configdict = {"uid": self.dbid}

        """

        We overload this to set the channel tag prefix.

        """
        kwargs["no_prefix"] = not self.db.tag_channel
        return super().at_pre_channel_msg(message, channel, senders=senders, **kwargs)

    def channel_msg(self, message, channel, senders=None, relayed=False, **kwargs):
        """
        Passes channel messages received on to discord

        Args:
            message (str) - Incoming text from channel.
            channel (Channel) - The channel the message is being received from

            senders (list or None) - Object(s) sending the message
            relayed (bool) - A flag identifying whether the message was relayed by the bot.

        """
        if relayed:
            # don't relay our own relayed messages
            return
        if channel_list := self.db.channels:
            # get all the discord channels connected to this evennia channel
            channel_name = channel.name
            for dc_chan in [dcid for evchan, dcid in channel_list if evchan == channel_name]:
                # send outputfunc channel(msg, discord channel)
                super().msg(channel=(strip_ansi(message.strip()), dc_chan))

    def change_nickname(self, new_nickname, guild_id, user_id, **kwargs):
        """
        Changes a given user's nickname on the given guild the bot is in.

        Args:
            new_nickname (str) - The user's new nickname.
            guild_id (int) - The guild the nickname will be changed in.
            user_id (int) - The Discord ID of the user who's nickname will be changed.

        """
        super().msg(nickname=(new_nickname, guild_id, user_id))

    def assign_role(self, role_id, guild_id, user_id, **kwargs):
        """
        Assigns a user the role on the given guild the bot is in.

        Args:
            role_id (int) - The Discord role's ID.
            guild_id (int) - The guild the role will be assigned in.
            user_id (int) - The user the given role will be assigned to.
        """

        super().msg(role=(role_id, guild_id, user_id))

    def direct_msg(self, message, sender, **kwargs):
        """
        Called when the Discord bot receives a direct message on Discord.

        Args:
            message (str)  - Incoming text from Discord.
            sender (tuple) - The Discord info for the sender in the form (id, nickname)

        Keyword args:
            **kwargs (optional) - Unused by default, but can carry additional data from the protocol.

        """
        pass

    def relay_to_channel(
        self, message, to_channel, sender=None, from_channel=None, from_server=None, **kwargs
    ):
        """
        Formats and sends a Discord -> Evennia message. Called when the Discord bot receives a
        channel message on Discord.

        Args:
            to_channel (Channel) - The Evennia channel receiving the message

            sender (tuple) - The Discord info for the sender in the form `(id, nickname)`
            from_channel (str) - The Discord channel name
            from_server (str) - The Discord server name
            kwargs - Any additional keywords. Unused by default, but available for adding additional
                flags or parameters.

        """

        tag_str = ""
        if from_channel and self.db.tag_channel:
            tag_str = f"#{from_channel}"
        if from_server and self.db.tag_guild:
            if tag_str:
                tag_str += f"@{from_server}"
            else:
                tag_str = from_server

        if tag_str:
            tag_str = f"[{tag_str}] "

        if sender:
            sender_name = f"|c{sender[1]}|n: "

        message = f"{tag_str}{sender_name}{message}"
        to_channel.msg(message, senders=None, relayed=True)

        self,
        txt=None,
        type=None,
        **kwargs,
    ):
        """

            txt (str) - The content of the message from Discord.
            session (Session) - The protocol session this command came from.
            type (str, optional) - Indicates the type of activity from Discord, if
                the protocol pre-processed it.
            sender (tuple) - Identifies the author of the Discord activity in a tuple of two
                strings, in the form of (id, nickname)

            kwargs - Any additional data specific to a particular type of actions. The data for
                any Discord actions not pre-processed by the protocol will also be passed via kwargs.

        """
        # normal channel message
        if type == "channel":
            channel_id = kwargs.get("channel_id")
            channel_name = self.db.discord_channels.get(channel_id, {}).get("name", channel_id)
            guild_id = kwargs.get("guild_id")
            guild = self.db.guilds.get(guild_id)

            if channel_links := self.db.channels:
                for ev_channel in [
                    ev_chan for ev_chan, dc_id in channel_links if dc_id == channel_id
                ]:
                    channel = search.channel_search(ev_channel)
                    if not channel:
                        continue
                    channel = channel[0]
                    self.relay_to_channel(txt, channel, sender, channel_name, guild)

        # direct message
        elif type == "direct":
            # pass on to the DM hook
            self.direct_msg(txt, sender, **kwargs)

        # guild info update
        elif type == "guild":
            if guild_id := kwargs.get("guild_id"):
                if not self.db.guilds:
                    self.db.guilds = {}
                self.db.guilds[guild_id] = kwargs.get("guild_name", "Unidentified")
                if not self.db.discord_channels:
                    self.db.discord_channels = {}
                self.db.discord_channels.update(kwargs.get("channels", {}))

"""
Account

The account class is an extension of the default Django user class,
and is customized for the needs of Evennia.

We use the Account to store a more mud-friendly style of permission
system as well as to allow the admin more flexibility by storing
attributes on the Account.  Within the game we should normally use the
Account manager's methods to create users so that permissions are set
correctly.

To make the Account model more flexible for your own game, it can also
persistently store attributes of its own. This is ideal for extra
account info and OOC account configuration variables etc.

"""
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.encoding import smart_str

from evennia.accounts.manager import AccountDBManager
from evennia.server.signals import SIGNAL_ACCOUNT_POST_RENAME
from evennia.typeclasses.models import TypedObject
from evennia.utils.utils import make_iter

__all__ = ("AccountDB",)

_GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__

_TYPECLASS = None

# ------------------------------------------------------------
#
# AccountDB
#

class AccountDB(TypedObject, AbstractUser):
    """
    This is a special model using Django's 'profile' functionality
    and extends the default Django User model. It is defined as such
    by use of the variable AUTH_PROFILE_MODULE in the settings.
    One accesses the fields/methods. We try use this model as much
    as possible rather than User, since we can customize this to
    our liking.

    The TypedObject supplies the following (inherited) properties:

      - key - main name
      - typeclass_path - the path to the decorating typeclass
      - typeclass - auto-linked typeclass
      - date_created - time stamp of object creation
      - permissions - perm strings
      - dbref - #id of object
      - db - persistent attribute storage
      - ndb - non-persistent attribute storage

    The AccountDB adds the following properties:

      - is_connected - If any Session is currently connected to this Account
      - name - alias for user.username
      - sessions - sessions connected to this account
      - is_superuser - bool if this account is a superuser
      - is_bot - bool if this account is a bot and not a real account

    """

    #
    # AccountDB Database model setup
    #
    # inherited fields (from TypedObject):
    # db_key, db_typeclass_path, db_date_created, db_permissions

    # store a connected flag here too, not just in sessionhandler.
    # This makes it easier to track from various out-of-process locations
    db_is_connected = models.BooleanField(
        default=False,
        verbose_name="is_connected",
        help_text="If player is connected to game or not",
    )
    # database storage of persistant cmdsets.
    db_cmdset_storage = models.CharField(
        "cmdset",
        max_length=255,
        null=True,
        help_text=(
            "optional python path to a cmdset class. If creating a Character, this will "
            "default to settings.CMDSET_CHARACTER."
        ),
    )
    # marks if this is a "virtual" bot account object
    db_is_bot = models.BooleanField(
        default=False, verbose_name="is_bot", help_text="Used to identify irc/rss bots"
    )

    # Database manager
    objects = AccountDBManager()

    # defaults
    __defaultclasspath__ = "evennia.accounts.accounts.DefaultAccount"
    __applabel__ = "accounts"
    __settingsclasspath__ = settings.BASE_SCRIPT_TYPECLASS

    class Meta:
        verbose_name = "Account"

    # cmdset_storage property
    # This seems very sensitive to caching, so leaving it be for now /Griatch
    # @property
    def __cmdset_storage_get(self):
        """
        Getter. Allows for value = self.name. Returns a list of cmdset_storage.
        """
        storage = self.db_cmdset_storage
        # we need to check so storage is not None
        return [path.strip() for path in storage.split(",")] if storage else []

    # @cmdset_storage.setter
    def __cmdset_storage_set(self, value):
        """
        Setter. Allows for self.name = value. Stores as a comma-separated
        string.
        """
        _SA(self, "db_cmdset_storage", ",".join(str(val).strip() for val in make_iter(value)))
        _GA(self, "save")()

    # @cmdset_storage.deleter
    def __cmdset_storage_del(self):
        "Deleter. Allows for del self.name"
        _SA(self, "db_cmdset_storage", None)

    cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set, __cmdset_storage_del)

    #
    # property/field access
    #

    def __str__(self):
        return smart_str(f"{self.name}(account {self.dbid})")

    def __repr__(self):
        return f"{self.name}(account#{self.dbid})"

    def __username_get(self):
        return self.username

    def __username_set(self, value):
        old_name = self.username
        self.username = value
        self.save(update_fields=["username"])
        SIGNAL_ACCOUNT_POST_RENAME.send(self, old_name=old_name, new_name=value)

    def __username_del(self):
        del self.username

    # aliases
    name = property(__username_get, __username_set, __username_del)
    key = property(__username_get, __username_set, __username_del)

    def __uid_get(self):
        "Getter. Retrieves the user id"
        return self.id

    def __uid_set(self, value):
        raise Exception("User id cannot be set!")

    def __uid_del(self):
        raise Exception("User id cannot be deleted!")

    uid = property(__uid_get, __uid_set, __uid_del)

"""
Various data structures used in query construction.

Factored out from django.db.models.query to avoid making the main module very
large and/or so that they can be used by other modules without getting into
circular import difficulties.
"""
import functools
import inspect
import logging
from collections import namedtuple

from django.core.exceptions import FieldError
from django.db import DEFAULT_DB_ALIAS, DatabaseError, connections
from django.db.models.constants import LOOKUP_SEP
from django.utils import tree

logger = logging.getLogger("django.db.models")

# PathInfo is used when converting lookups (fk__somecol). The contents
# describe the relation in Model terms (model Options and Fields for both
# sides of the relation. The join_field is the field backing the relation.
PathInfo = namedtuple(
    "PathInfo",
    "from_opts to_opts target_fields join_field m2m direct filtered_relation",
)

def subclasses(cls):
    yield cls
    for subclass in cls.__subclasses__():
        yield from subclasses(subclass)

class Q(tree.Node):
    """
    Encapsulate filters as objects that can then be combined logically (using
    `&` and `|`).
    """

    # Connection types
    AND = "AND"
    OR = "OR"
    XOR = "XOR"
    default = AND
    conditional = True

    def __init__(self, *args, _connector=None, _negated=False, **kwargs):
        super().__init__(
            children=[*args, *sorted(kwargs.items())],
            connector=_connector,
            negated=_negated,
        )

    def _combine(self, other, conn):
        if getattr(other, "conditional", False) is False:
            raise TypeError(other)
        if not self:
            return other.copy()
        if not other and isinstance(other, Q):
            return self.copy()

        obj = self.create(connector=conn)
        obj.add(self, conn)
        obj.add(other, conn)
        return obj

    def __or__(self, other):
        return self._combine(other, self.OR)

    def __and__(self, other):
        return self._combine(other, self.AND)

    def __xor__(self, other):
        return self._combine(other, self.XOR)

    def __invert__(self):
        obj = self.copy()
        obj.negate()
        return obj

    def resolve_expression(
        self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
    ):
        # We must promote any new joins to left outer joins so that when Q is
        # used as an expression, rows aren't filtered due to joins.
        clause, joins = query._add_q(
            self,
            reuse,
            allow_joins=allow_joins,
            split_subq=False,
            check_filterable=False,
            summarize=summarize,
        )
        query.promote_joins(joins)
        return clause

    def flatten(self):
        """
        Recursively yield this Q object and all subexpressions, in depth-first
        order.
        """
        yield self
        for child in self.children:
            if isinstance(child, tuple):
                # Use the lookup.
                child = child[1]
            if hasattr(child, "flatten"):
                yield from child.flatten()
            else:
                yield child

    def check(self, against, using=DEFAULT_DB_ALIAS):
        """
        Do a database query to check if the expressions of the Q instance
        matches against the expressions.
        """
        # Avoid circular imports.
        from django.db.models import BooleanField, Value
        from django.db.models.functions import Coalesce
        from django.db.models.sql import Query
        from django.db.models.sql.constants import SINGLE

        query = Query(None)
        for name, value in against.items():
            if not hasattr(value, "resolve_expression"):
                value = Value(value)
            query.add_annotation(value, name, select=False)
        query.add_annotation(Value(1), "_check")
        # This will raise a FieldError if a field is missing in "against".
        if connections[using].features.supports_comparing_boolean_expr:
            query.add_q(Q(Coalesce(self, True, output_field=BooleanField())))
        else:
            query.add_q(self)
        compiler = query.get_compiler(using=using)
        try:
            return compiler.execute_sql(SINGLE) is not None
        except DatabaseError as e:
            logger.warning("Got a database error calling check() on %r: %s", self, e)

    def deconstruct(self):
        path = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
        if path.startswith("django.db.models.query_utils"):
            path = path.replace("django.db.models.query_utils", "django.db.models")
        args = tuple(self.children)
        kwargs = {}
        if self.connector != self.default:
            kwargs["_connector"] = self.connector
        if self.negated:
            kwargs["_negated"] = True
        return path, args, kwargs

class DeferredAttribute:
    """
    A wrapper for a deferred-loading field. When the value is read from this
    object the first time, the query is executed.
    """

    def __init__(self, field):
        self.field = field

    def __get__(self, instance, cls=None):
        """
        Retrieve and caches the value from the datastore on the first lookup.
        Return the cached value.
        """
        if instance is None:
            return self
        data = instance.__dict__
        field_name = self.field.attname
        if field_name not in data:
            # Let's see if the field is part of the parent chain. If so we
            # might be able to reuse the already loaded value. Refs #18343.
            val = self._check_parent_chain(instance)
            if val is None:
                instance.refresh_from_db(fields=[field_name])
            else:
                data[field_name] = val
        return data[field_name]

    def _check_parent_chain(self, instance):
        """
        Check if the field value can be fetched from a parent field already
        loaded in the instance. This can be done if the to-be fetched
        field is a primary key field.
        """
        opts = instance._meta
        link_field = opts.get_ancestor_link(self.field.model)
        if self.field.primary_key and self.field != link_field:
            return getattr(instance, link_field.attname)

class class_or_instance_method:
    """
    Hook used in RegisterLookupMixin to return partial functions depending on
    the caller type (instance or class of models.Field).
    """

    def __init__(self, class_method, instance_method):
        self.class_method = class_method
        self.instance_method = instance_method

    def __get__(self, instance, owner):
            return functools.partial(self.class_method, owner)
        return functools.partial(self.instance_method, instance)

class RegisterLookupMixin:
    def _get_lookup(self, lookup_name):
        return self.get_lookups().get(lookup_name, None)

    @functools.lru_cache(maxsize=None)
    def get_class_lookups(cls):
        class_lookups = [
            parent.__dict__.get("class_lookups", {}) for parent in inspect.getmro(cls)
        ]
        return cls.merge_dicts(class_lookups)

    def get_instance_lookups(self):
        class_lookups = self.get_class_lookups()
        if instance_lookups := getattr(self, "instance_lookups", None):
            return {**class_lookups, **instance_lookups}
        return class_lookups

    get_lookups = class_or_instance_method(get_class_lookups, get_instance_lookups)
    get_class_lookups = classmethod(get_class_lookups)

    def get_lookup(self, lookup_name):
        from django.db.models.lookups import Lookup

        found = self._get_lookup(lookup_name)
        if found is None and hasattr(self, "output_field"):
            return self.output_field.get_lookup(lookup_name)
        if found is not None and not issubclass(found, Lookup):
        return found

    def get_transform(self, lookup_name):
        from django.db.models.lookups import Transform

            return self.output_field.get_transform(lookup_name)
        if found is not None and not issubclass(found, Transform):

    @staticmethod
    def merge_dicts(dicts):
        """
        Merge dicts in reverse to preference the order of the original list. e.g.,
        merge_dicts([a, b]) will preference the keys in 'a' over those in 'b'.
        """
        merged = {}
        for d in reversed(dicts):
            merged.update(d)
        return merged

    def _clear_cached_class_lookups(cls):
        for subclass in subclasses(cls):
            subclass.get_class_lookups.cache_clear()

    def register_class_lookup(cls, lookup, lookup_name=None):
        if lookup_name is None:
            lookup_name = lookup.lookup_name
        if "class_lookups" not in cls.__dict__:
            cls.class_lookups = {}
        cls.class_lookups[lookup_name] = lookup
        cls._clear_cached_class_lookups()
        return lookup

    def register_instance_lookup(self, lookup, lookup_name=None):
        if "instance_lookups" not in self.__dict__:
            self.instance_lookups = {}
        self.instance_lookups[lookup_name] = lookup

    register_lookup = class_or_instance_method(
        register_class_lookup, register_instance_lookup
    )
    register_class_lookup = classmethod(register_class_lookup)

    def _unregister_class_lookup(cls, lookup, lookup_name=None):
        """
        Remove given lookup from cls lookups. For use in tests only as it's
        not thread-safe.
        """
        del cls.class_lookups[lookup_name]

    def _unregister_instance_lookup(self, lookup, lookup_name=None):
        """
        Remove given lookup from instance lookups. For use in tests only as
        it's not thread-safe.
        """
        del self.instance_lookups[lookup_name]

    _unregister_lookup = class_or_instance_method(
        _unregister_class_lookup, _unregister_instance_lookup
    )
    _unregister_class_lookup = classmethod(_unregister_class_lookup)

def select_related_descend(field, restricted, requested, select_mask, reverse=False):
    """
    Return True if this field should be used to descend deeper for
    select_related() purposes. Used by both the query construction code
    (compiler.get_related_selections()) and the model instance creation code
    (compiler.klass_info).

    Arguments:
     * field - the field to be checked
     * restricted - a boolean field, indicating if the field list has been
       manually restricted using a requested clause)
     * requested - The select_related() dictionary.
     * select_mask - the dictionary of selected fields.
     * reverse - boolean, True if we are checking a reverse select related
    """
    if not field.remote_field:
    if field.remote_field.parent_link and not reverse:
    if restricted:
        if reverse and field.related_query_name() not in requested:
        if not reverse and field.name not in requested:
    if not restricted and field.null:
    if (
        restricted
        and select_mask
        and field.name in requested
        and field not in select_mask
    ):
        raise FieldError(
            f"Field {field.model._meta.object_name}.{field.name} cannot be both "
            "deferred and traversed using select_related at the same time."
        )
    return True

def refs_expression(lookup_parts, annotations):
    """
    Check if the lookup_parts contains references to the given annotations set.
    Because the LOOKUP_SEP is contained in the default annotation names, check
    each prefix of the lookup_parts for a match.
    """
    for n in range(1, len(lookup_parts) + 1):
        level_n_lookup = LOOKUP_SEP.join(lookup_parts[0:n])
        if annotations.get(level_n_lookup):
            return level_n_lookup, lookup_parts[n:]
    return None, ()

def check_rel_lookup_compatibility(model, target_opts, field):
    """
    Check that self.model is compatible with target_opts. Compatibility
    is OK if:
      1) model and opts match (where proxy inheritance is removed)
      2) model is parent of opts' model or the other way around
    """

    def check(opts):
        return (
            model._meta.concrete_model == opts.concrete_model
            or opts.concrete_model in model._meta.get_parent_list()
            or model in opts.get_parent_list()
        )

    # If the field is a primary key, then doing a query against the field's
    # model is ok, too. Consider the case:
    # class Restaurant(models.Model):
    #     place = OneToOneField(Place, primary_key=True):
    # Restaurant.objects.filter(pk__in=Restaurant.objects.all()).
    # If we didn't have the primary key check, then pk__in (== place__in) would
    # give Place's opts as the target opts, but Restaurant isn't compatible
    # with that. This logic applies only to primary keys, as when doing __in=qs,
    # we are going to turn this into __in=qs.values('pk') later on.
    return check(target_opts) or (
        getattr(field, "primary_key", False) and check(field.model._meta)
    )

class FilteredRelation:
    """Specify custom filtering in the ON clause of SQL joins."""

    def __init__(self, relation_name, *, condition=Q()):
        if not relation_name:
            raise ValueError("relation_name cannot be empty.")
        self.relation_name = relation_name
        self.alias = None
        if not isinstance(condition, Q):
            raise ValueError("condition argument must be a Q() instance.")
        self.condition = condition
        self.path = []

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return (
            self.relation_name == other.relation_name
            and self.alias == other.alias
            and self.condition == other.condition
        )

    def clone(self):
        clone = FilteredRelation(self.relation_name, condition=self.condition)
        clone.alias = self.alias
        clone.path = self.path[:]
        return clone

    def resolve_expression(self, *args, **kwargs):
        """
        QuerySet.annotate() only accepts expression-like arguments
        (with a resolve_expression() method).
        """
        raise NotImplementedError("FilteredRelation.resolve_expression() is unused.")

    def as_sql(self, compiler, connection):
        # Resolve the condition in Join.filtered_relation.
        query = compiler.query
        where = query.build_filtered_relation_q(self.condition, reuse=set(self.path))
        return compiler.compile(where)

"""
Accessors for related objects.

When a field defines a relation between two models, each model class provides
an attribute to access related instances of the other model class (unless the
reverse accessor has been disabled with related_name='+').

Accessors are implemented as descriptors in order to customize access and
assignment. This module defines the descriptor classes.

Forward accessors follow foreign keys. Reverse accessors trace them back. For
example, with the following models::

    class Parent(Model):
        pass

    class Child(Model):
        parent = ForeignKey(Parent, related_name='children')

 ``child.parent`` is a forward many-to-one relation. ``parent.children`` is a
reverse many-to-one relation.

There are three types of relations (many-to-one, one-to-one, and many-to-many)
and two directions (forward and reverse) for a total of six combinations.

1. Related instance on the forward side of a many-to-one relation:
   ``ForwardManyToOneDescriptor``.

   Uniqueness of foreign key values is irrelevant to accessing the related
   instance, making the many-to-one and one-to-one cases identical as far as
   the descriptor is concerned. The constraint is checked upstream (unicity
   validation in forms) or downstream (unique indexes in the database).

2. Related instance on the forward side of a one-to-one
   relation: ``ForwardOneToOneDescriptor``.

   It avoids querying the database when accessing the parent link field in
   a multi-table inheritance scenario.

3. Related instance on the reverse side of a one-to-one relation:
   ``ReverseOneToOneDescriptor``.

   One-to-one relations are asymmetrical, despite the apparent symmetry of the
   name, because they're implemented in the database with a foreign key from
   one table to another. As a consequence ``ReverseOneToOneDescriptor`` is
   slightly different from ``ForwardManyToOneDescriptor``.

4. Related objects manager for related instances on the reverse side of a
   many-to-one relation: ``ReverseManyToOneDescriptor``.

   Unlike the previous two classes, this one provides access to a collection
   of objects. It returns a manager rather than an instance.

5. Related objects manager for related instances on the forward or reverse
   sides of a many-to-many relation: ``ManyToManyDescriptor``.

   Many-to-many relations are symmetrical. The syntax of Django models
   requires declaring them on one side but that's an implementation detail.
   They could be declared on the other side without any change in behavior.
   Therefore the forward and reverse descriptors can be the same.

   If you're looking for ``ForwardManyToManyDescriptor`` or
   ``ReverseManyToManyDescriptor``, use ``ManyToManyDescriptor`` instead.
"""

from asgiref.sync import sync_to_async

from django.db import (
    DEFAULT_DB_ALIAS,
    NotSupportedError,
    connections,
    router,
    transaction,
)
from django.db.models import Q, Window, signals
from django.db.models.functions import RowNumber
from django.db.models.lookups import GreaterThan, LessThanOrEqual
from django.db.models.query import QuerySet
from django.db.models.query_utils import DeferredAttribute
from django.db.models.utils import AltersData, resolve_callables
from django.utils.functional import cached_property

class ForeignKeyDeferredAttribute(DeferredAttribute):
    def __set__(self, instance, value):
        if instance.__dict__.get(self.field.attname) != value and self.field.is_cached(
            instance
        ):
            self.field.delete_cached_value(instance)
        instance.__dict__[self.field.attname] = value

def _filter_prefetch_queryset(queryset, field_name, instances):
    predicate = Q(**{f"{field_name}__in": instances})
    db = queryset._db or DEFAULT_DB_ALIAS
    if queryset.query.is_sliced:
        if not connections[db].features.supports_over_clause:
            raise NotSupportedError(
                "Prefetching from a limited queryset is only supported on backends "
                "that support window functions."
            )
        low_mark, high_mark = queryset.query.low_mark, queryset.query.high_mark
        order_by = [
            expr for expr, _ in queryset.query.get_compiler(using=db).get_order_by()
        ]
        window = Window(RowNumber(), partition_by=field_name, order_by=order_by)
        predicate &= GreaterThan(window, low_mark)
        if high_mark is not None:
            predicate &= LessThanOrEqual(window, high_mark)
        queryset.query.clear_limits()
    return queryset.filter(predicate)

class ForwardManyToOneDescriptor:
    """
    Accessor to the related object on the forward side of a many-to-one or
    one-to-one (via ForwardOneToOneDescriptor subclass) relation.

    In the example::

        class Child(Model):
            parent = ForeignKey(Parent, related_name='children')

    ``Child.parent`` is a ``ForwardManyToOneDescriptor`` instance.
    """

    def __init__(self, field_with_rel):
        self.field = field_with_rel

    @cached_property
    def RelatedObjectDoesNotExist(self):
        # The exception can't be created at initialization time since the
        # related model might not be resolved yet; `self.field.model` might
        # still be a string model reference.
        return type(
            "RelatedObjectDoesNotExist",
            (self.field.remote_field.model.DoesNotExist, AttributeError),
            {
                "__module__": self.field.model.__module__,
                "__qualname__": "%s.%s.RelatedObjectDoesNotExist"
                % (
                    self.field.model.__qualname__,
                    self.field.name,
                ),
            },
        )

    def is_cached(self, instance):
        return self.field.is_cached(instance)

    def get_queryset(self, **hints):
        return self.field.remote_field.model._base_manager.db_manager(hints=hints).all()

    def get_prefetch_queryset(self, instances, queryset=None):
        if queryset is None:
            queryset = self.get_queryset()
        queryset._add_hints(instance=instances[0])

        rel_obj_attr = self.field.get_foreign_related_value
        instance_attr = self.field.get_local_related_value
        instances_dict = {instance_attr(inst): inst for inst in instances}
        related_field = self.field.foreign_related_fields[0]
        remote_field = self.field.remote_field

        # FIXME: This will need to be revisited when we introduce support for
        # composite fields. In the meantime we take this practical approach to
        # solve a regression on 1.6 when the reverse manager in hidden
        # (related_name ends with a '+'). Refs #21410.
        # The check for len(...) == 1 is a special case that allows the query
        # to be join-less and smaller. Refs #21760.
        if remote_field.is_hidden() or len(self.field.foreign_related_fields) == 1:
            query = {
                "%s__in"
                % related_field.name: {instance_attr(inst)[0] for inst in instances}
            }
        else:
            query = {"%s__in" % self.field.related_query_name(): instances}
        queryset = queryset.filter(**query)

        # Since we're going to assign directly in the cache,
        # we must manage the reverse relation cache manually.
        if not remote_field.multiple:
            for rel_obj in queryset:
                instance = instances_dict[rel_obj_attr(rel_obj)]
                remote_field.set_cached_value(rel_obj, instance)
        return (
            queryset,
            rel_obj_attr,
            instance_attr,
            True,
            self.field.get_cache_name(),
            False,
        )

    def get_object(self, instance):
        qs = self.get_queryset(instance=instance)
        # Assuming the database enforces foreign keys, this won't fail.
        return qs.get(self.field.get_reverse_related_filter(instance))

        """
        Get the related instance through the forward relation.

        With the example above, when getting ``child.parent``:

        - ``self`` is the descriptor managing the ``parent`` attribute
        - ``instance`` is the ``child`` instance
        - ``cls`` is the ``Child`` class (we don't need it)
        """

        # The related instance is loaded from the database and then cached
        # by the field on the model instance state. It can also be pre-cached
        # by the reverse accessor (ReverseOneToOneDescriptor).
        try:
            rel_obj = self.field.get_cached_value(instance)
        except KeyError:
            has_value = None not in self.field.get_local_related_value(instance)
            ancestor_link = (
                instance._meta.get_ancestor_link(self.field.model)
                if has_value
                else None
            )
            if ancestor_link and ancestor_link.is_cached(instance):
                # An ancestor link will exist if this field is defined on a
                # multi-table inheritance parent of the instance's class.
                ancestor = ancestor_link.get_cached_value(instance)
                # The value might be cached on an ancestor if the instance
                # originated from walking down the inheritance chain.
                rel_obj = self.field.get_cached_value(ancestor, default=None)
            else:
                rel_obj = None
            if rel_obj is None and has_value:
                rel_obj = self.get_object(instance)
                remote_field = self.field.remote_field
                # If this is a one-to-one relation, set the reverse accessor
                # cache on the related object to the current instance to avoid
                # an extra SQL query if it's accessed later on.
                if not remote_field.multiple:
                    remote_field.set_cached_value(rel_obj, instance)
            self.field.set_cached_value(instance, rel_obj)

        if rel_obj is None and not self.field.null:
            raise self.RelatedObjectDoesNotExist(
                "%s has no %s." % (self.field.model.__name__, self.field.name)
            )
        else:
            return rel_obj

        """
        Set the related instance through the forward relation.

        With the example above, when setting ``child.parent = parent``:

        - ``value`` is the ``parent`` instance on the right of the equal sign
        """
        # An object must be an instance of the related class.
        if value is not None and not isinstance(
            value, self.field.remote_field.model._meta.concrete_model
        ):
            raise ValueError(
                'Cannot assign "%r": "%s.%s" must be a "%s" instance.'
                % (
                    value,
                    instance._meta.object_name,
                    self.field.remote_field.model._meta.object_name,
                )
            )
        elif value is not None:
            if instance._state.db is None:
                instance._state.db = router.db_for_write(
                    instance.__class__, instance=value
                )
            if value._state.db is None:
                value._state.db = router.db_for_write(
                    value.__class__, instance=instance
                )
            if not router.allow_relation(value, instance):
                raise ValueError(
                    'Cannot assign "%r": the current database router prevents this '
                    "relation." % value
                )

        # If we're setting the value of a OneToOneField to None, we need to clear
        # out the cache on any old related object. Otherwise, deleting the
        # previously-related object will also cause this object to be deleted,
        # which is wrong.
        if value is None:
            # Look up the previously-related object, which may still be available
            # since we've not yet cleared out the related field.
            # Use the cache directly, instead of the accessor; if we haven't
            # populated the cache, then we don't care - we're only accessing
            # the object to invalidate the accessor cache, so there's no
            # need to populate the cache just to expire it again.
            related = self.field.get_cached_value(instance, default=None)

            # If we've got an old related object, we need to clear out its
            # cache. This cache also might not exist if the related object
            # hasn't been accessed yet.
            if related is not None:
                remote_field.set_cached_value(related, None)

            for lh_field, rh_field in self.field.related_fields:
                setattr(instance, lh_field.attname, None)

        # Set the values of the related field.
        else:
                setattr(instance, lh_field.attname, getattr(value, rh_field.attname))

        # Set the related instance cache used by __get__ to avoid an SQL query
        # when accessing the attribute we just set.
        self.field.set_cached_value(instance, value)

        # If this is a one-to-one relation, set the reverse accessor cache on
        # the related object to the current instance to avoid an extra SQL
        # query if it's accessed later on.
        if value is not None and not remote_field.multiple:
            remote_field.set_cached_value(value, instance)

    def __reduce__(self):
        """
        Pickling should return the instance attached by self.field on the
        model, not a new copy of that descriptor. Use getattr() to retrieve
        the instance directly from the model.
        """
        return getattr, (self.field.model, self.field.name)

class ForwardOneToOneDescriptor(ForwardManyToOneDescriptor):
    """
    Accessor to the related object on the forward side of a one-to-one relation.

        class Restaurant(Model):
            place = OneToOneField(Place, related_name='restaurant')

    ``Restaurant.place`` is a ``ForwardOneToOneDescriptor`` instance.
    """

        if self.field.remote_field.parent_link:
            deferred = instance.get_deferred_fields()
            # Because it's a parent link, all the data is available in the
            # instance, so populate the parent model with this data.
            rel_model = self.field.remote_field.model
            fields = [field.attname for field in rel_model._meta.concrete_fields]

            # If any of the related model's fields are deferred, fallback to
            # fetching all fields from the related model. This avoids a query
            # on the related model for every deferred field.
            if not any(field in fields for field in deferred):
                kwargs = {field: getattr(instance, field) for field in fields}
                obj = rel_model(**kwargs)
                obj._state.adding = instance._state.adding
                obj._state.db = instance._state.db
                return obj
        return super().get_object(instance)

        super().__set__(instance, value)
        # If the primary key is a link to a parent model and a parent instance
        # is being set, update the value of the inherited pk(s).
        if self.field.primary_key and self.field.remote_field.parent_link:
            opts = instance._meta
            # Inherited primary key fields from this object's base classes.
            inherited_pk_fields = [
                field
                for field in opts.concrete_fields
                if field.primary_key and field.remote_field
            ]
            for field in inherited_pk_fields:
                rel_model_pk_name = field.remote_field.model._meta.pk.attname
                raw_value = (
                    getattr(value, rel_model_pk_name) if value is not None else None
                )
                setattr(instance, rel_model_pk_name, raw_value)

class ReverseOneToOneDescriptor:
    """
    Accessor to the related object on the reverse side of a one-to-one
    relation.

    ``Place.restaurant`` is a ``ReverseOneToOneDescriptor`` instance.
    """

    def __init__(self, related):
        # Following the example above, `related` is an instance of OneToOneRel
        # which represents the reverse restaurant field (place.restaurant).
        self.related = related

        # The exception isn't created at initialization time for the sake of
        # consistency with `ForwardManyToOneDescriptor`.
            (self.related.related_model.DoesNotExist, AttributeError),
            {
                "__module__": self.related.model.__module__,
                % (
                    self.related.model.__qualname__,
                    self.related.name,
                ),
            },
        )

        return self.related.is_cached(instance)

        return self.related.related_model._base_manager.db_manager(hints=hints).all()

        rel_obj_attr = self.related.field.get_local_related_value
        instance_attr = self.related.field.get_foreign_related_value
        query = {"%s__in" % self.related.field.name: instances}

        for rel_obj in queryset:
            instance = instances_dict[rel_obj_attr(rel_obj)]
            self.related.field.set_cached_value(rel_obj, instance)
        return (
            queryset,
            True,
            self.related.get_cache_name(),
            False,
        )

        """
        Get the related instance through the reverse relation.

        With the example above, when getting ``place.restaurant``:

        - ``self`` is the descriptor managing the ``restaurant`` attribute
        - ``instance`` is the ``place`` instance
        - ``cls`` is the ``Place`` class (unused)

        Keep in mind that ``Restaurant`` holds the foreign key to ``Place``.
        """

        # by the forward accessor (ForwardManyToOneDescriptor).
        try:
            rel_obj = self.related.get_cached_value(instance)
            related_pk = instance.pk
            if related_pk is None:
            else:
                filter_args = self.related.field.get_forward_related_filter(instance)
                try:
                    rel_obj = self.get_queryset(instance=instance).get(**filter_args)
                except self.related.related_model.DoesNotExist:
                    rel_obj = None
                else:
                    # Set the forward accessor cache on the related object to
                    # the current instance to avoid an extra SQL query if it's
                    # accessed later on.
                    self.related.field.set_cached_value(rel_obj, instance)
            self.related.set_cached_value(instance, rel_obj)

        if rel_obj is None:
                "%s has no %s."
                % (instance.__class__.__name__, self.related.get_accessor_name())
            )
        else:

        """
        Set the related instance through the reverse relation.

        With the example above, when setting ``place.restaurant = restaurant``:

        - ``value`` is the ``restaurant`` instance on the right of the equal sign

        """
        # The similarity of the code below to the code in
        # ForwardManyToOneDescriptor is annoying, but there's a bunch
        # of small differences that would make a common base class convoluted.

            # Update the cached related instance (if any) & clear the cache.
            # Following the example above, this would be the cached
            # ``restaurant`` instance (if any).
            rel_obj = self.related.get_cached_value(instance, default=None)
            if rel_obj is not None:
                # Remove the ``restaurant`` instance from the ``place``
                # instance cache.
                self.related.delete_cached_value(instance)
                # Set the ``place`` field on the ``restaurant``
                # instance to None.
                setattr(rel_obj, self.related.field.name, None)
        elif not isinstance(value, self.related.related_model):
            # An object must be an instance of the related class.
                % (
                    value,
                    self.related.get_accessor_name(),
                    self.related.related_model._meta.object_name,
                )
            )
        else:
                )
                )
                )

            related_pk = tuple(
                getattr(instance, field.attname)
                for field in self.related.field.foreign_related_fields
            )
            # Set the value of the related field to the value of the related
            # object's related field.
            for index, field in enumerate(self.related.field.local_related_fields):
                setattr(value, field.attname, related_pk[index])

            # Set the related instance cache used by __get__ to avoid an SQL query
            # when accessing the attribute we just set.
            self.related.set_cached_value(instance, value)

            # Set the forward accessor cache on the related object to the current
            # instance to avoid an extra SQL query if it's accessed later on.
            self.related.field.set_cached_value(value, instance)

        # Same purpose as ForwardManyToOneDescriptor.__reduce__().
        return getattr, (self.related.model, self.related.name)

class ReverseManyToOneDescriptor:
    """
    Accessor to the related objects manager on the reverse side of a
    many-to-one relation.

    ``Parent.children`` is a ``ReverseManyToOneDescriptor`` instance.

    Most of the implementation is delegated to a dynamically defined manager
    class built by ``create_forward_many_to_many_manager()`` defined below.
    """

    def __init__(self, rel):
        self.rel = rel
        self.field = rel.field

    def related_manager_cls(self):
        related_model = self.rel.related_model

        return create_reverse_many_to_one_manager(
            related_model._default_manager.__class__,
            self.rel,
        )

        """
        Get the related objects through the reverse relation.

        With the example above, when getting ``parent.children``:

        - ``self`` is the descriptor managing the ``children`` attribute
        - ``instance`` is the ``parent`` instance
        - ``cls`` is the ``Parent`` class (unused)
        """

        return self.related_manager_cls(instance)

    def _get_set_deprecation_msg_params(self):
        return (
            "reverse side of a related set",
            self.rel.get_accessor_name(),
        )

        raise TypeError(
            "Direct assignment to the %s is prohibited. Use %s.set() instead."
            % self._get_set_deprecation_msg_params(),
        )

def create_reverse_many_to_one_manager(superclass, rel):
    """
    Create a manager for the reverse side of a many-to-one relation.

    This manager subclasses another manager, generally the default manager of
    the related model, and adds behaviors specific to many-to-one relations.
    """

    class RelatedManager(superclass, AltersData):
        def __init__(self, instance):
            super().__init__()

            self.instance = instance
            self.model = rel.related_model
            self.field = rel.field

            self.core_filters = {self.field.name: instance}

        def __call__(self, *, manager):
            manager = getattr(self.model, manager)
            manager_class = create_reverse_many_to_one_manager(manager.__class__, rel)
            return manager_class(self.instance)

        do_not_call_in_templates = True

        def _check_fk_val(self):
            for field in self.field.foreign_related_fields:
                if getattr(self.instance, field.attname) is None:
                    raise ValueError(
                        f'"{self.instance!r}" needs to have a value for field '
                        f'"{field.attname}" before this relationship can be used.'
                    )

        def _apply_rel_filters(self, queryset):
            """
            Filter the queryset for the instance this manager is bound to.
            """
            db = self._db or router.db_for_read(self.model, instance=self.instance)
            empty_strings_as_null = connections[
                db
            ].features.interprets_empty_strings_as_nulls
            queryset._add_hints(instance=self.instance)
            if self._db:
                queryset = queryset.using(self._db)
            queryset._defer_next_filter = True
            queryset = queryset.filter(**self.core_filters)
                val = getattr(self.instance, field.attname)
                if val is None or (val == "" and empty_strings_as_null):
                    return queryset.none()
            if self.field.many_to_one:
                # Guard against field-like objects such as GenericRelation
                # that abuse create_reverse_many_to_one_manager() with reverse
                # one-to-many relationships instead and break known related
                # objects assignment.
                try:
                    target_field = self.field.target_field
                except FieldError:
                    # The relationship has multiple target fields. Use a tuple
                    # for related object id.
                    rel_obj_id = tuple(
                        [
                            getattr(self.instance, target_field.attname)
                            for target_field in self.field.path_infos[-1].target_fields
                        ]
                    )
                else:
                    rel_obj_id = getattr(self.instance, target_field.attname)
                queryset._known_related_objects = {
                    self.field: {rel_obj_id: self.instance}
                }
            return queryset

        def _remove_prefetched_objects(self):
            try:
                self.instance._prefetched_objects_cache.pop(
                    self.field.remote_field.get_cache_name()
                )
            except (AttributeError, KeyError):
                pass  # nothing to clear from cache

        def get_queryset(self):
            # Even if this relation is not to pk, we require still pk value.
            # The wish is that the instance has been already saved to DB,
            # although having a pk value isn't a guarantee of that.
            if self.instance.pk is None:
                    f"{self.instance.__class__.__name__!r} instance needs to have a "
                    f"primary key value before this relationship can be used."
                )
            try:
                return self.instance._prefetched_objects_cache[
                ]
                queryset = super().get_queryset()
                return self._apply_rel_filters(queryset)

        def get_prefetch_queryset(self, instances, queryset=None):
            if queryset is None:

            queryset._add_hints(instance=instances[0])
            queryset = queryset.using(queryset._db or self._db)

            rel_obj_attr = self.field.get_local_related_value
            instance_attr = self.field.get_foreign_related_value
            instances_dict = {instance_attr(inst): inst for inst in instances}
            queryset = _filter_prefetch_queryset(queryset, self.field.name, instances)

            # Since we just bypassed this class' get_queryset(), we must manage
            # the reverse relation manually.
                if not self.field.is_cached(rel_obj):
                    instance = instances_dict[rel_obj_attr(rel_obj)]
                    setattr(rel_obj, self.field.name, instance)
            cache_name = self.field.remote_field.get_cache_name()
            return queryset, rel_obj_attr, instance_attr, False, cache_name, False

        def add(self, *objs, bulk=True):
            self._check_fk_val()
            self._remove_prefetched_objects()
            db = router.db_for_write(self.model, instance=self.instance)

            def check_and_update_obj(obj):
                if not isinstance(obj, self.model):
                    raise TypeError(
                        "'%s' instance expected, got %r"
                        % (
                            self.model._meta.object_name,
                            obj,
                        )
                    )
                setattr(obj, self.field.name, self.instance)

            if bulk:
                pks = []
                for obj in objs:
                    check_and_update_obj(obj)
                    if obj._state.adding or obj._state.db != db:
                        raise ValueError(
                            "%r instance isn't saved. Use bulk=False or save "
                            "the object first." % obj
                        )
                    pks.append(obj.pk)
                self.model._base_manager.using(db).filter(pk__in=pks).update(
                    **{
                        self.field.name: self.instance,
                    }
                )
            else:
                with transaction.atomic(using=db, savepoint=False):
                    for obj in objs:
                        check_and_update_obj(obj)
                        obj.save()

        add.alters_data = True

        async def aadd(self, *objs, bulk=True):
            return await sync_to_async(self.add)(*objs, bulk=bulk)

        aadd.alters_data = True

        def create(self, **kwargs):
            kwargs[self.field.name] = self.instance
            return super(RelatedManager, self.db_manager(db)).create(**kwargs)

        create.alters_data = True

        async def acreate(self, **kwargs):
            return await sync_to_async(self.create)(**kwargs)

        acreate.alters_data = True

        def get_or_create(self, **kwargs):
            return super(RelatedManager, self.db_manager(db)).get_or_create(**kwargs)

        get_or_create.alters_data = True

        async def aget_or_create(self, **kwargs):
            return await sync_to_async(self.get_or_create)(**kwargs)

        aget_or_create.alters_data = True

        def update_or_create(self, **kwargs):
            return super(RelatedManager, self.db_manager(db)).update_or_create(**kwargs)

        update_or_create.alters_data = True

        async def aupdate_or_create(self, **kwargs):
            return await sync_to_async(self.update_or_create)(**kwargs)

        aupdate_or_create.alters_data = True

        # remove() and clear() are only provided if the ForeignKey can have a
        # value of null.
        if rel.field.null:

            def remove(self, *objs, bulk=True):
                if not objs:
                    return
                self._check_fk_val()
                val = self.field.get_foreign_related_value(self.instance)
                old_ids = set()
                    if not isinstance(obj, self.model):
                        raise TypeError(
                            "'%s' instance expected, got %r"
                            % (
                                self.model._meta.object_name,
                                obj,
                            )
                        )
                    # Is obj actually part of this descriptor set?
                    if self.field.get_local_related_value(obj) == val:
                        old_ids.add(obj.pk)
                    else:
                        raise self.field.remote_field.model.DoesNotExist(
                            "%r is not related to %r." % (obj, self.instance)
                        )
                self._clear(self.filter(pk__in=old_ids), bulk)

            remove.alters_data = True

            async def aremove(self, *objs, bulk=True):
                return await sync_to_async(self.remove)(*objs, bulk=bulk)

            aremove.alters_data = True

            def clear(self, *, bulk=True):
                self._clear(self, bulk)

            clear.alters_data = True

            async def aclear(self, *, bulk=True):
                return await sync_to_async(self.clear)(bulk=bulk)

            aclear.alters_data = True

            def _clear(self, queryset, bulk):
                self._remove_prefetched_objects()
                db = router.db_for_write(self.model, instance=self.instance)
                queryset = queryset.using(db)
                if bulk:
                    # `QuerySet.update()` is intrinsically atomic.
                    queryset.update(**{self.field.name: None})
                else:
                    with transaction.atomic(using=db, savepoint=False):
                        for obj in queryset:
                            setattr(obj, self.field.name, None)
                            obj.save(update_fields=[self.field.name])

            _clear.alters_data = True

        def set(self, objs, *, bulk=True, clear=False):
            # Force evaluation of `objs` in case it's a queryset whose value
            # could be affected by `manager.clear()`. Refs #19816.
            objs = tuple(objs)

            if self.field.null:
                    if clear:
                        self.clear(bulk=bulk)
                        self.add(*objs, bulk=bulk)
                    else:
                        old_objs = set(self.using(db).all())
                        new_objs = []
                        for obj in objs:
                            if obj in old_objs:
                                old_objs.remove(obj)
                            else:
                                new_objs.append(obj)

                        self.remove(*old_objs, bulk=bulk)
                        self.add(*new_objs, bulk=bulk)
            else:
                self.add(*objs, bulk=bulk)

        set.alters_data = True

        async def aset(self, objs, *, bulk=True, clear=False):
            return await sync_to_async(self.set)(objs=objs, bulk=bulk, clear=clear)

        aset.alters_data = True

    return RelatedManager

class ManyToManyDescriptor(ReverseManyToOneDescriptor):
    """
    Accessor to the related objects manager on the forward and reverse sides of
    a many-to-many relation.

        class Pizza(Model):
            toppings = ManyToManyField(Topping, related_name='pizzas')

    ``Pizza.toppings`` and ``Topping.pizzas`` are ``ManyToManyDescriptor``
    instances.

    """

    def __init__(self, rel, reverse=False):
        super().__init__(rel)

        self.reverse = reverse

    @property
    def through(self):
        # through is provided so that you have easy access to the through
        # model (Book.authors.through) for inlines, etc. This is done as
        # a property to ensure that the fully resolved value is returned.
        return self.rel.through

        related_model = self.rel.related_model if self.reverse else self.rel.model

        return create_forward_many_to_many_manager(
            self.rel,
            reverse=self.reverse,
        )

        return (
            "%s side of a many-to-many set"
            % ("reverse" if self.reverse else "forward"),
            self.rel.get_accessor_name() if self.reverse else self.field.name,
        )

def create_forward_many_to_many_manager(superclass, rel, reverse):
    """
    Create a manager for the either side of a many-to-many relation.

    the related model, and adds behaviors specific to many-to-many relations.
    """

    class ManyRelatedManager(superclass, AltersData):
        def __init__(self, instance=None):

            if not reverse:
                self.model = rel.model
                self.query_field_name = rel.field.related_query_name()
                self.prefetch_cache_name = rel.field.name
                self.source_field_name = rel.field.m2m_field_name()
                self.target_field_name = rel.field.m2m_reverse_field_name()
                self.symmetrical = rel.symmetrical
            else:
                self.model = rel.related_model
                self.query_field_name = rel.field.name
                self.prefetch_cache_name = rel.field.related_query_name()
                self.source_field_name = rel.field.m2m_reverse_field_name()
                self.target_field_name = rel.field.m2m_field_name()
                self.symmetrical = False

            self.through = rel.through
            self.reverse = reverse

            self.source_field = self.through._meta.get_field(self.source_field_name)
            self.target_field = self.through._meta.get_field(self.target_field_name)

            self.core_filters = {}
            self.pk_field_names = {}
            for lh_field, rh_field in self.source_field.related_fields:
                core_filter_key = "%s__%s" % (self.query_field_name, rh_field.name)
                self.core_filters[core_filter_key] = getattr(instance, rh_field.attname)
                self.pk_field_names[lh_field.name] = rh_field.name

            self.related_val = self.source_field.get_foreign_related_value(instance)
            if None in self.related_val:
                    '"%r" needs to have a value for field "%s" before '
                    "this many-to-many relationship can be used."
                    % (instance, self.pk_field_names[self.source_field_name])
                )
            if instance.pk is None:
                    "%r instance needs to have a primary key value before "
                    "a many-to-many relationship can be used."
                    % instance.__class__.__name__
                )

            manager_class = create_forward_many_to_many_manager(
                manager.__class__, rel, reverse
            )
            return manager_class(instance=self.instance)

        def _build_remove_filters(self, removed_vals):
            filters = Q.create([(self.source_field_name, self.related_val)])
            # No need to add a subquery condition if removed_vals is a QuerySet without
            # filters.
            removed_vals_filters = (
                not isinstance(removed_vals, QuerySet) or removed_vals._has_filters()
            )
            if removed_vals_filters:
                filters &= Q.create([(f"{self.target_field_name}__in", removed_vals)])
            if self.symmetrical:
                symmetrical_filters = Q.create(
                    [(self.target_field_name, self.related_val)]
                )
                if removed_vals_filters:
                    symmetrical_filters &= Q.create(
                        [(f"{self.source_field_name}__in", removed_vals)]
                    )
                filters |= symmetrical_filters
            return filters

            """
            """
            return queryset._next_is_sticky().filter(**self.core_filters)

            try:
                self.instance._prefetched_objects_cache.pop(self.prefetch_cache_name)

            try:
                return self.instance._prefetched_objects_cache[self.prefetch_cache_name]

            queryset = _filter_prefetch_queryset(
                queryset._next_is_sticky(), self.query_field_name, instances
            )

            # M2M: need to annotate the query in order to get the primary model
            # that the secondary model was actually related to. We know that
            # there will already be a join on the join table, so we can just add
            # the select.

            # For non-autocreated 'through' models, can't assume we are
            # dealing with PK values.
            fk = self.through._meta.get_field(self.source_field_name)
            join_table = fk.model._meta.db_table
            connection = connections[queryset.db]
            qn = connection.ops.quote_name
            queryset = queryset.extra(
                select={
                    "_prefetch_related_val_%s"
                    % f.attname: "%s.%s"
                    % (qn(join_table), qn(f.column))
                    for f in fk.local_related_fields
                }
            )
            return (
                queryset,
                lambda result: tuple(
                    getattr(result, "_prefetch_related_val_%s" % f.attname)
                ),
                lambda inst: tuple(
                    f.get_db_prep_value(getattr(inst, f.attname), connection)
                    for f in fk.foreign_related_fields
                ),
                False,
                self.prefetch_cache_name,
                False,
            )

        def add(self, *objs, through_defaults=None):
            db = router.db_for_write(self.through, instance=self.instance)
            with transaction.atomic(using=db, savepoint=False):
                self._add_items(
                    self.source_field_name,
                    self.target_field_name,
                    *objs,
                    through_defaults=through_defaults,
                )
                # If this is a symmetrical m2m relation to self, add the mirror
                # entry in the m2m table.
                if self.symmetrical:
                    self._add_items(
                        self.target_field_name,
                        self.source_field_name,
                        *objs,
                        through_defaults=through_defaults,
                    )

        async def aadd(self, *objs, through_defaults=None):
            return await sync_to_async(self.add)(
                *objs, through_defaults=through_defaults
            )

        def remove(self, *objs):
            self._remove_items(self.source_field_name, self.target_field_name, *objs)

        remove.alters_data = True

        async def aremove(self, *objs):
            return await sync_to_async(self.remove)(*objs)

        aremove.alters_data = True

        def clear(self):
                signals.m2m_changed.send(
                    sender=self.through,
                    action="pre_clear",
                    instance=self.instance,
                    reverse=self.reverse,
                    model=self.model,
                    pk_set=None,
                    using=db,
                )
                filters = self._build_remove_filters(super().get_queryset().using(db))
                self.through._default_manager.using(db).filter(filters).delete()

                    action="post_clear",
                    using=db,
                )

        clear.alters_data = True

        async def aclear(self):
            return await sync_to_async(self.clear)()

        aclear.alters_data = True

        def set(self, objs, *, clear=False, through_defaults=None):

                if clear:
                    self.clear()
                    self.add(*objs, through_defaults=through_defaults)
                else:
                    old_ids = set(
                        self.using(db).values_list(
                            self.target_field.target_field.attname, flat=True
                        )
                    )

                    new_objs = []
                        fk_val = (
                            self.target_field.get_foreign_related_value(obj)[0]
                            if isinstance(obj, self.model)
                            else self.target_field.get_prep_value(obj)
                        )
                        if fk_val in old_ids:
                            old_ids.remove(fk_val)
                        else:
                            new_objs.append(obj)

                    self.remove(*old_ids)
                    self.add(*new_objs, through_defaults=through_defaults)

        async def aset(self, objs, *, clear=False, through_defaults=None):
            return await sync_to_async(self.set)(
                objs=objs, clear=clear, through_defaults=through_defaults
            )

        def create(self, *, through_defaults=None, **kwargs):
            db = router.db_for_write(self.instance.__class__, instance=self.instance)
            new_obj = super(ManyRelatedManager, self.db_manager(db)).create(**kwargs)
            self.add(new_obj, through_defaults=through_defaults)
            return new_obj

        async def acreate(self, *, through_defaults=None, **kwargs):
            return await sync_to_async(self.create)(
                through_defaults=through_defaults, **kwargs
            )

        def get_or_create(self, *, through_defaults=None, **kwargs):
            obj, created = super(ManyRelatedManager, self.db_manager(db)).get_or_create(
                **kwargs
            )
            # We only need to add() if created because if we got an object back
            # from get() then the relationship already exists.
            if created:
                self.add(obj, through_defaults=through_defaults)
            return obj, created

        async def aget_or_create(self, *, through_defaults=None, **kwargs):
            return await sync_to_async(self.get_or_create)(
            )

        def update_or_create(self, *, through_defaults=None, **kwargs):
            obj, created = super(
                ManyRelatedManager, self.db_manager(db)
            ).update_or_create(**kwargs)

        async def aupdate_or_create(self, *, through_defaults=None, **kwargs):
            return await sync_to_async(self.update_or_create)(
            )

        def _get_target_ids(self, target_field_name, objs):
            """
            Return the set of ids of `objs` that the target field references.
            """
            from django.db.models import Model

            target_ids = set()
            target_field = self.through._meta.get_field(target_field_name)
            for obj in objs:
                if isinstance(obj, self.model):
                    if not router.allow_relation(obj, self.instance):
                            'Cannot add "%r": instance is on database "%s", '
                            'value is on database "%s"'
                            % (obj, self.instance._state.db, obj._state.db)
                        )
                    target_id = target_field.get_foreign_related_value(obj)[0]
                    if target_id is None:
                            'Cannot add "%r": the value for field "%s" is None'
                            % (obj, target_field_name)
                        )
                    target_ids.add(target_id)
                elif isinstance(obj, Model):
                        % (self.model._meta.object_name, obj)
                    )
                else:
                    target_ids.add(target_field.get_prep_value(obj))
            return target_ids

        def _get_missing_target_ids(
            self, source_field_name, target_field_name, db, target_ids
        ):
            """
            Return the subset of ids of `objs` that aren't already assigned to
            this relationship.
            """
            vals = (
                self.through._default_manager.using(db)
                .values_list(target_field_name, flat=True)
                .filter(
                    **{
                        source_field_name: self.related_val[0],
                        "%s__in" % target_field_name: target_ids,
                    }
                )
            )
            return target_ids.difference(vals)

        def _get_add_plan(self, db, source_field_name):
            """
            Return a boolean triple of the way the add should be performed.

            The first element is whether or not bulk_create(ignore_conflicts)
            can be used, the second whether or not signals must be sent, and
            the third element is whether or not the immediate bulk insertion
            with conflicts ignored can be performed.
            """
            # Conflicts can be ignored when the intermediary model is
            # auto-created as the only possible collision is on the
            # (source_id, target_id) tuple. The same assertion doesn't hold for
            # user-defined intermediary models as they could have other fields
            # causing conflicts which must be surfaced.
            can_ignore_conflicts = (
                self.through._meta.auto_created is not False
                and connections[db].features.supports_ignore_conflicts
            )
            # Don't send the signal when inserting duplicate data row
            # for symmetrical reverse entries.
            must_send_signals = (
                self.reverse or source_field_name == self.source_field_name
            ) and (signals.m2m_changed.has_listeners(self.through))
            # Fast addition through bulk insertion can only be performed
            # if no m2m_changed listeners are connected for self.through
            # as they require the added set of ids to be provided via
            # pk_set.
            return (
                can_ignore_conflicts,
                must_send_signals,
                (can_ignore_conflicts and not must_send_signals),
            )

        def _add_items(
            self, source_field_name, target_field_name, *objs, through_defaults=None
        ):
            # source_field_name: the PK fieldname in join table for the source object
            # target_field_name: the PK fieldname in join table for the target object
            # *objs - objects to add. Either object instances, or primary keys
            # of object instances.
            if not objs:
                return

            through_defaults = dict(resolve_callables(through_defaults or {}))
            target_ids = self._get_target_ids(target_field_name, objs)
            can_ignore_conflicts, must_send_signals, can_fast_add = self._get_add_plan(
                db, source_field_name
            )
            if can_fast_add:
                self.through._default_manager.using(db).bulk_create(
                    [
                        self.through(
                            **{
                                "%s_id" % source_field_name: self.related_val[0],
                                "%s_id" % target_field_name: target_id,
                            }
                        )
                        for target_id in target_ids
                    ],
                    ignore_conflicts=True,
                )
                return

            missing_target_ids = self._get_missing_target_ids(
                source_field_name, target_field_name, db, target_ids
            )
                if must_send_signals:
                    signals.m2m_changed.send(
                        sender=self.through,
                        action="pre_add",
                        instance=self.instance,
                        reverse=self.reverse,
                        model=self.model,
                        pk_set=missing_target_ids,
                        using=db,
                    )
                # Add the ones that aren't there already.
                    [
                            **through_defaults,
                            **{
                            },
                        )
                        for target_id in missing_target_ids
                    ],
                    ignore_conflicts=can_ignore_conflicts,
                )

                        action="post_add",
                        using=db,
                    )

        def _remove_items(self, source_field_name, target_field_name, *objs):
            # source_field_name: the PK colname in join table for the source object
            # target_field_name: the PK colname in join table for the target object
            # *objs - objects to remove. Either object instances, or primary
            # keys of object instances.
                return

            # Check that all the objects are of the right type
            old_ids = set()
                    fk_val = self.target_field.get_foreign_related_value(obj)[0]
                    old_ids.add(fk_val)
                else:
                    old_ids.add(obj)

                # Send a signal to the other end if need be.
                    action="pre_remove",
                    pk_set=old_ids,
                    using=db,
                )
                target_model_qs = super().get_queryset()
                if target_model_qs._has_filters():
                    old_vals = target_model_qs.using(db).filter(
                        **{"%s__in" % self.target_field.target_field.attname: old_ids}
                    )
                else:
                    old_vals = old_ids
                filters = self._build_remove_filters(old_vals)

                    action="post_remove",
                    using=db,
                )

    return ManyRelatedManager

"""functools.py - Tools for working with functions and callable objects
"""
# Python module wrapper for _functools C module
# to allow utilities written in Python to be added
# to the functools module.
# Written by Nick Coghlan <ncoghlan at gmail.com>,
# Raymond Hettinger <python at rcn.com>,
# and Łukasz Langa <lukasz at langa.pl>.
#   Copyright (C) 2006-2013 Python Software Foundation.
# See C source code for _functools credits/copyright

__all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES',
           'total_ordering', 'cache', 'cmp_to_key', 'lru_cache', 'reduce',
           'partial', 'partialmethod', 'singledispatch', 'singledispatchmethod',
           'cached_property']

from abc import get_cache_token
# import types, weakref  # Deferred to single_dispatch()
from reprlib import recursive_repr
from _thread import RLock
from types import GenericAlias

################################################################################
### update_wrapper() and wraps() decorator

# update_wrapper() and wraps() are tools to help write
# wrapper functions that can handle naive introspection

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        try:
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Issue #17482: set __wrapped__ last so we don't inadvertently copy it
    # from the wrapped function when updating __dict__
    wrapper.__wrapped__ = wrapped
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

       Returns a decorator that invokes update_wrapper() with the decorated
       function as the wrapper argument and the arguments to wraps() as the
       remaining arguments. Default arguments are as for update_wrapper().
       This is a convenience function to simplify applying partial() to
       update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

### total_ordering class decorator

# The total ordering functions all invoke the root magic method directly
# rather than using the corresponding operator.  This avoids possible
# infinite recursion that could occur when the operator dispatch logic
# detects a NotImplemented result and then calls a reflected method.

def _gt_from_lt(self, other, NotImplemented=NotImplemented):
    'Return a > b.  Computed by @total_ordering from (not a < b) and (a != b).'
    op_result = type(self).__lt__(self, other)
    if op_result is NotImplemented:
        return op_result
    return not op_result and self != other

def _le_from_lt(self, other, NotImplemented=NotImplemented):
    'Return a <= b.  Computed by @total_ordering from (a < b) or (a == b).'
    return op_result or self == other

def _ge_from_lt(self, other, NotImplemented=NotImplemented):
    'Return a >= b.  Computed by @total_ordering from (not a < b).'
    return not op_result

def _ge_from_le(self, other, NotImplemented=NotImplemented):
    'Return a >= b.  Computed by @total_ordering from (not a <= b) or (a == b).'
    op_result = type(self).__le__(self, other)
    return not op_result or self == other

def _lt_from_le(self, other, NotImplemented=NotImplemented):
    'Return a < b.  Computed by @total_ordering from (a <= b) and (a != b).'
    return op_result and self != other

def _gt_from_le(self, other, NotImplemented=NotImplemented):
    'Return a > b.  Computed by @total_ordering from (not a <= b).'

def _lt_from_gt(self, other, NotImplemented=NotImplemented):
    'Return a < b.  Computed by @total_ordering from (not a > b) and (a != b).'
    op_result = type(self).__gt__(self, other)

def _ge_from_gt(self, other, NotImplemented=NotImplemented):
    'Return a >= b.  Computed by @total_ordering from (a > b) or (a == b).'

def _le_from_gt(self, other, NotImplemented=NotImplemented):
    'Return a <= b.  Computed by @total_ordering from (not a > b).'

def _le_from_ge(self, other, NotImplemented=NotImplemented):
    'Return a <= b.  Computed by @total_ordering from (not a >= b) or (a == b).'
    op_result = type(self).__ge__(self, other)

def _gt_from_ge(self, other, NotImplemented=NotImplemented):
    'Return a > b.  Computed by @total_ordering from (a >= b) and (a != b).'

def _lt_from_ge(self, other, NotImplemented=NotImplemented):
    'Return a < b.  Computed by @total_ordering from (not a >= b).'

_convert = {
    '__lt__': [('__gt__', _gt_from_lt),
               ('__le__', _le_from_lt),
               ('__ge__', _ge_from_lt)],
    '__le__': [('__ge__', _ge_from_le),
               ('__lt__', _lt_from_le),
               ('__gt__', _gt_from_le)],
    '__gt__': [('__lt__', _lt_from_gt),
               ('__ge__', _ge_from_gt),
               ('__le__', _le_from_gt)],
    '__ge__': [('__le__', _le_from_ge),
               ('__gt__', _gt_from_ge),
               ('__lt__', _lt_from_ge)]
}

def total_ordering(cls):
    """Class decorator that fills in missing ordering methods"""
    # Find user-defined comparisons (not those inherited from object).
    roots = {op for op in _convert if getattr(cls, op, None) is not getattr(object, op, None)}
    if not roots:
        raise ValueError('must define at least one ordering operation: < > <= >=')
    root = max(roots)       # prefer __lt__ to __le__ to __gt__ to __ge__
    for opname, opfunc in _convert[root]:
        if opname not in roots:
            opfunc.__name__ = opname
            setattr(cls, opname, opfunc)
    return cls

### cmp_to_key() function converter

def cmp_to_key(mycmp):
    """Convert a cmp= function into a key= function"""
    class K(object):
        __slots__ = ['obj']
        def __init__(self, obj):
            self.obj = obj
        def __lt__(self, other):
            return mycmp(self.obj, other.obj) < 0
        def __gt__(self, other):
            return mycmp(self.obj, other.obj) > 0
        def __eq__(self, other):
            return mycmp(self.obj, other.obj) == 0
        def __le__(self, other):
            return mycmp(self.obj, other.obj) <= 0
        def __ge__(self, other):
            return mycmp(self.obj, other.obj) >= 0
        __hash__ = None
    return K

try:
    from _functools import cmp_to_key
except ImportError:
    pass

### reduce() sequence to a single item

_initial_missing = object()

def reduce(function, sequence, initial=_initial_missing):
    """
    reduce(function, iterable[, initial]) -> value

    Apply a function of two arguments cumulatively to the items of a sequence
    or iterable, from left to right, so as to reduce the iterable to a single
    value.  For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates
    ((((1+2)+3)+4)+5).  If initial is present, it is placed before the items
    of the iterable in the calculation, and serves as a default when the
    iterable is empty.
    """

    it = iter(sequence)

    if initial is _initial_missing:
        try:
            value = next(it)
        except StopIteration:
            raise TypeError(
                "reduce() of empty iterable with no initial value") from None
    else:
        value = initial

    for element in it:
        value = function(value, element)

    return value

try:
    from _functools import reduce
    pass

### partial() argument application

# Purely functional, no descriptor behaviour
class partial:
    """New function with partial application of the given arguments
    and keywords.
    """

    __slots__ = "func", "args", "keywords", "__dict__", "__weakref__"

    def __new__(cls, func, /, *args, **keywords):
        if not callable(func):
            raise TypeError("the first argument must be callable")

        if hasattr(func, "func"):
            args = func.args + args
            keywords = {**func.keywords, **keywords}
            func = func.func

        self = super(partial, cls).__new__(cls)

        self.func = func
        self.args = args
        self.keywords = keywords
        return self

    def __call__(self, /, *args, **keywords):
        keywords = {**self.keywords, **keywords}
        return self.func(*self.args, *args, **keywords)

    @recursive_repr()
        qualname = type(self).__qualname__
        args = [repr(self.func)]
        args.extend(repr(x) for x in self.args)
        args.extend(f"{k}={v!r}" for (k, v) in self.keywords.items())
        if type(self).__module__ == "functools":
            return f"functools.{qualname}({', '.join(args)})"
        return f"{qualname}({', '.join(args)})"

        return type(self), (self.func,), (self.func, self.args,
               self.keywords or None, self.__dict__ or None)

    def __setstate__(self, state):
        if not isinstance(state, tuple):
            raise TypeError("argument to __setstate__ must be a tuple")
        if len(state) != 4:
            raise TypeError(f"expected 4 items in state, got {len(state)}")
        func, args, kwds, namespace = state
        if (not callable(func) or not isinstance(args, tuple) or
           (kwds is not None and not isinstance(kwds, dict)) or
           (namespace is not None and not isinstance(namespace, dict))):
            raise TypeError("invalid partial state")

        args = tuple(args) # just in case it's a subclass
        if kwds is None:
            kwds = {}
        elif type(kwds) is not dict: # XXX does it need to be *exactly* dict?
            kwds = dict(kwds)
        if namespace is None:
            namespace = {}

        self.__dict__ = namespace
        self.keywords = kwds

try:
    from _functools import partial
    pass

# Descriptor version
class partialmethod(object):
    """Method descriptor with partial application of the given arguments

    Supports wrapping existing descriptors and handles non-descriptor
    callables as instance methods.
    """

    def __init__(self, func, /, *args, **keywords):
        if not callable(func) and not hasattr(func, "__get__"):
            raise TypeError("{!r} is not callable or a descriptor"
                                 .format(func))

        # func could be a descriptor like classmethod which isn't callable,
        # so we can't inherit from partial (it verifies func is callable)
        if isinstance(func, partialmethod):
            # flattening is mandatory in order to place cls/self before all
            # other arguments
            # it's also more efficient since only one function will be called
            self.func = func.func
            self.args = func.args + args
            self.keywords = {**func.keywords, **keywords}
        else:
            self.func = func
            self.args = args
            self.keywords = keywords

        args = ", ".join(map(repr, self.args))
        keywords = ", ".join("{}={!r}".format(k, v)
                                 for k, v in self.keywords.items())
        format_string = "{module}.{cls}({func}, {args}, {keywords})"
        return format_string.format(module=self.__class__.__module__,
                                    cls=self.__class__.__qualname__,
                                    func=self.func,
                                    args=args,
                                    keywords=keywords)

    def _make_unbound_method(self):
        def _method(cls_or_self, /, *args, **keywords):
            keywords = {**self.keywords, **keywords}
            return self.func(cls_or_self, *self.args, *args, **keywords)
        _method.__isabstractmethod__ = self.__isabstractmethod__
        _method._partialmethod = self
        return _method

    def __get__(self, obj, cls=None):
        get = getattr(self.func, "__get__", None)
        result = None
        if get is not None:
            new_func = get(obj, cls)
            if new_func is not self.func:
                # Assume __get__ returning something new indicates the
                # creation of an appropriate callable
                result = partial(new_func, *self.args, **self.keywords)
                try:
                    result.__self__ = new_func.__self__
                    pass
        if result is None:
            # If the underlying descriptor didn't do anything, treat this
            # like an instance method
            result = self._make_unbound_method().__get__(obj, cls)

    @property
    def __isabstractmethod__(self):
        return getattr(self.func, "__isabstractmethod__", False)

    __class_getitem__ = classmethod(GenericAlias)

# Helper functions

def _unwrap_partial(func):
    while isinstance(func, partial):
        func = func.func
    return func

### LRU Cache function decorator

_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])

class _HashedSeq(list):
    """ This class guarantees that hash() will be called no more than once
        per element.  This is important because the lru_cache() will hash
        the key multiple times on a cache miss.

    """

    __slots__ = 'hashvalue'

    def __init__(self, tup, hash=hash):
        self[:] = tup
        self.hashvalue = hash(tup)

    def __hash__(self):
        return self.hashvalue

def _make_key(args, kwds, typed,
             kwd_mark = (object(),),
             fasttypes = {int, str},
             tuple=tuple, type=type, len=len):
    """Make a cache key from optionally typed positional and keyword arguments

    The key is constructed in a way that is flat as possible rather than
    as a nested structure that would take more memory.

    If there is only a single argument and its data type is known to cache
    its hash value, then that argument is returned without a wrapper.  This
    saves space and improves lookup speed.

    """
    # All of code below relies on kwds preserving the order input by the user.
    # Formerly, we sorted() the kwds before looping.  The new way is *much*
    # faster; however, it means that f(x=1, y=2) will now be treated as a
    # distinct call from f(y=2, x=1) which will be cached separately.
    key = args
    if kwds:
        key += kwd_mark
        for item in kwds.items():
            key += item
    if typed:
        key += tuple(type(v) for v in args)
        if kwds:
            key += tuple(type(v) for v in kwds.values())
    elif len(key) == 1 and type(key[0]) in fasttypes:
        return key[0]
    return _HashedSeq(key)

def lru_cache(maxsize=128, typed=False):
    """Least-recently-used cache decorator.

    If *maxsize* is set to None, the LRU features are disabled and the cache
    can grow without bound.

    If *typed* is True, arguments of different types will be cached separately.
    For example, f(3.0) and f(3) will be treated as distinct calls with
    distinct results.

    Arguments to the cached function must be hashable.

    View the cache statistics named tuple (hits, misses, maxsize, currsize)
    with f.cache_info().  Clear the cache and statistics with f.cache_clear().
    Access the underlying function with f.__wrapped__.

    See:  https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)

    """

    # Users should only access the lru_cache through its public API:
    #       cache_info, cache_clear, and f.__wrapped__
    # The internals of the lru_cache are encapsulated for thread safety and
    # to allow the implementation to change (including a possible C version).

    if isinstance(maxsize, int):
        # Negative maxsize is treated as 0
        if maxsize < 0:
            maxsize = 0
    elif callable(maxsize) and isinstance(typed, bool):
        # The user_function was passed in directly via the maxsize argument
        user_function, maxsize = maxsize, 128
        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
        wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
        return update_wrapper(wrapper, user_function)
    elif maxsize is not None:
            'Expected first argument to be an integer, a callable, or None')

    def decorating_function(user_function):

    return decorating_function

def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
    # Constants shared by all lru cache instances:
    sentinel = object()          # unique object used to signal cache misses
    make_key = _make_key         # build a key from the function arguments
    PREV, NEXT, KEY, RESULT = 0, 1, 2, 3   # names for the link fields

    cache = {}
    hits = misses = 0
    full = False
    cache_get = cache.get    # bound method to lookup a key or return None
    cache_len = cache.__len__  # get cache size without calling len()
    lock = RLock()           # because linkedlist updates aren't threadsafe
    root = []                # root of the circular doubly linked list
    root[:] = [root, root, None, None]     # initialize by pointing to self

    if maxsize == 0:

        def wrapper(*args, **kwds):
            # No caching -- just a statistics update
            nonlocal misses
            misses += 1
            result = user_function(*args, **kwds)
            return result

    elif maxsize is None:

            # Simple caching without ordering or size limit
            nonlocal hits, misses
            key = make_key(args, kwds, typed)
            result = cache_get(key, sentinel)
            if result is not sentinel:
                hits += 1
                return result
            misses += 1
            cache[key] = result

    else:

            # Size limited caching that tracks accesses by recency
            nonlocal root, hits, misses, full
            with lock:
                link = cache_get(key)
                if link is not None:
                    # Move the link to the front of the circular queue
                    link_prev, link_next, _key, result = link
                    link_prev[NEXT] = link_next
                    link_next[PREV] = link_prev
                    last = root[PREV]
                    last[NEXT] = root[PREV] = link
                    link[PREV] = last
                    link[NEXT] = root
                    hits += 1
                    return result
                misses += 1
            with lock:
                if key in cache:
                    # Getting here means that this same key was added to the
                    # cache while the lock was released.  Since the link
                    # update is already done, we need only return the
                    # computed result and update the count of misses.
                    pass
                elif full:
                    # Use the old root to store the new key and result.
                    oldroot = root
                    oldroot[KEY] = key
                    oldroot[RESULT] = result
                    # Empty the oldest link and make it the new root.
                    # Keep a reference to the old key and old result to
                    # prevent their ref counts from going to zero during the
                    # update. That will prevent potentially arbitrary object
                    # clean-up code (i.e. __del__) from running while we're
                    # still adjusting the links.
                    root = oldroot[NEXT]
                    oldkey = root[KEY]
                    oldresult = root[RESULT]
                    root[KEY] = root[RESULT] = None
                    # Now update the cache dictionary.
                    del cache[oldkey]
                    # Save the potentially reentrant cache[key] assignment
                    # for last, after the root and links have been put in
                    # a consistent state.
                    cache[key] = oldroot
                else:
                    # Put result in a new link at the front of the queue.
                    link = [last, root, key, result]
                    last[NEXT] = root[PREV] = cache[key] = link
                    # Use the cache_len bound method instead of the len() function
                    # which could potentially be wrapped in an lru_cache itself.
                    full = (cache_len() >= maxsize)

    def cache_info():
        """Report cache statistics"""
        with lock:
            return _CacheInfo(hits, misses, maxsize, cache_len())

    def cache_clear():
        """Clear the cache and cache statistics"""
        nonlocal hits, misses, full
        with lock:
            cache.clear()
            root[:] = [root, root, None, None]
            hits = misses = 0
            full = False

    wrapper.cache_info = cache_info
    wrapper.cache_clear = cache_clear

try:
    from _functools import _lru_cache_wrapper
    pass

### cache -- simplified access to the infinity cache

def cache(user_function, /):
    'Simple lightweight unbounded cache.  Sometimes called "memoize".'
    return lru_cache(maxsize=None)(user_function)

### singledispatch() - single-dispatch generic function decorator

def _c3_merge(sequences):
    """Merges MROs in *sequences* to a single MRO using the C3 algorithm.

    Adapted from https://www.python.org/download/releases/2.3/mro/.

    """
    result = []
    while True:
        sequences = [s for s in sequences if s]   # purge empty sequences
        if not sequences:
        for s1 in sequences:   # find merge candidates among seq heads
            candidate = s1[0]
            for s2 in sequences:
                if candidate in s2[1:]:
                    candidate = None
                    break      # reject the current head, it appears later
            else:
                break
        if candidate is None:
            raise RuntimeError("Inconsistent hierarchy")
        result.append(candidate)
        # remove the chosen candidate
        for seq in sequences:
            if seq[0] == candidate:
                del seq[0]

def _c3_mro(cls, abcs=None):
    """Computes the method resolution order using extended C3 linearization.

    If no *abcs* are given, the algorithm works exactly like the built-in C3
    linearization used for method resolution.

    If given, *abcs* is a list of abstract base classes that should be inserted
    into the resulting MRO. Unrelated ABCs are ignored and don't end up in the
    result. The algorithm inserts ABCs where their functionality is introduced,
    i.e. issubclass(cls, abc) returns True for the class itself but returns
    False for all its direct base classes. Implicit ABCs for a given class
    (either registered or inferred from the presence of a special method like
    __len__) are inserted directly after the last ABC explicitly listed in the
    MRO of said class. If two implicit ABCs end up next to each other in the
    resulting MRO, their ordering depends on the order of types in *abcs*.

    """
    for i, base in enumerate(reversed(cls.__bases__)):
        if hasattr(base, '__abstractmethods__'):
            boundary = len(cls.__bases__) - i
            break   # Bases up to the last explicit ABC are considered first.
    else:
        boundary = 0
    abcs = list(abcs) if abcs else []
    explicit_bases = list(cls.__bases__[:boundary])
    abstract_bases = []
    other_bases = list(cls.__bases__[boundary:])
    for base in abcs:
        if issubclass(cls, base) and not any(
                issubclass(b, base) for b in cls.__bases__
            ):
            # If *cls* is the class that introduces behaviour described by
            # an ABC *base*, insert said ABC to its MRO.
            abstract_bases.append(base)
    for base in abstract_bases:
        abcs.remove(base)
    explicit_c3_mros = [_c3_mro(base, abcs=abcs) for base in explicit_bases]
    abstract_c3_mros = [_c3_mro(base, abcs=abcs) for base in abstract_bases]
    other_c3_mros = [_c3_mro(base, abcs=abcs) for base in other_bases]
    return _c3_merge(
        [[cls]] +
        explicit_c3_mros + abstract_c3_mros + other_c3_mros +
        [explicit_bases] + [abstract_bases] + [other_bases]
    )

def _compose_mro(cls, types):
    """Calculates the method resolution order for a given class *cls*.

    Includes relevant abstract base classes (with their respective bases) from
    the *types* iterable. Uses a modified C3 linearization algorithm.

    """
    bases = set(cls.__mro__)
    # Remove entries which are already present in the __mro__ or unrelated.
    def is_related(typ):
        return (typ not in bases and hasattr(typ, '__mro__')
                                 and not isinstance(typ, GenericAlias)
                                 and issubclass(cls, typ))
    types = [n for n in types if is_related(n)]
    # Remove entries which are strict bases of other entries (they will end up
    # in the MRO anyway.
    def is_strict_base(typ):
        for other in types:
            if typ != other and typ in other.__mro__:
                return True
    types = [n for n in types if not is_strict_base(n)]
    # Subclasses of the ABCs in *types* which are also implemented by
    # *cls* can be used to stabilize ABC ordering.
    type_set = set(types)
    mro = []
    for typ in types:
        found = []
        for sub in typ.__subclasses__():
            if sub not in bases and issubclass(cls, sub):
                found.append([s for s in sub.__mro__ if s in type_set])
        if not found:
            mro.append(typ)
            continue
        # Favor subclasses with the biggest number of useful bases
        found.sort(key=len, reverse=True)
        for sub in found:
            for subcls in sub:
                if subcls not in mro:
                    mro.append(subcls)
    return _c3_mro(cls, abcs=mro)

def _find_impl(cls, registry):
    """Returns the best matching implementation from *registry* for type *cls*.

    Where there is no registered implementation for a specific type, its method
    resolution order is used to find a more generic implementation.

    Note: if *registry* does not contain an implementation for the base
    *object* type, this function may return None.

    """
    mro = _compose_mro(cls, registry.keys())
    match = None
    for t in mro:
        if match is not None:
            # If *match* is an implicit ABC but there is another unrelated,
            # equally matching implicit ABC, refuse the temptation to guess.
            if (t in registry and t not in cls.__mro__
                              and match not in cls.__mro__
                              and not issubclass(match, t)):
                raise RuntimeError("Ambiguous dispatch: {} or {}".format(
                    match, t))
            break
        if t in registry:
            match = t
    return registry.get(match)

def singledispatch(func):
    """Single-dispatch generic function decorator.

    Transforms a function into a generic function, which can have different
    behaviours depending upon the type of its first argument. The decorated
    function acts as the default implementation, and additional
    implementations can be registered using the register() attribute of the
    generic function.
    """
    # There are many programs that use functools without singledispatch, so we
    # trade-off making singledispatch marginally slower for the benefit of
    # making start-up of such applications slightly faster.
    import types, weakref

    registry = {}
    dispatch_cache = weakref.WeakKeyDictionary()
    cache_token = None

    def dispatch(cls):
        """generic_func.dispatch(cls) -> <function implementation>

        Runs the dispatch algorithm to return the best available implementation
        for the given *cls* registered on *generic_func*.

        """
        nonlocal cache_token
        if cache_token is not None:
            current_token = get_cache_token()
            if cache_token != current_token:
                dispatch_cache.clear()
                cache_token = current_token
        try:
            impl = dispatch_cache[cls]
            try:
                impl = registry[cls]
            except KeyError:
                impl = _find_impl(cls, registry)
            dispatch_cache[cls] = impl
        return impl

    def _is_valid_dispatch_type(cls):
        return isinstance(cls, type) and not isinstance(cls, GenericAlias)

    def register(cls, func=None):
        """generic_func.register(cls, func) -> func

        Registers a new implementation for the given *cls* on a *generic_func*.

        """
        if _is_valid_dispatch_type(cls):
            if func is None:
                return lambda f: register(cls, f)
        else:
            if func is not None:
                raise TypeError(
                    f"Invalid first argument to `register()`. "
                    f"{cls!r} is not a class."
                )
            ann = getattr(cls, '__annotations__', {})
            if not ann:
                    f"Invalid first argument to `register()`: {cls!r}. "
                    f"Use either `@register(some_class)` or plain `@register` "
                    f"on an annotated function."
                )
            func = cls

            # only import typing if annotation parsing is necessary
            from typing import get_type_hints
            argname, cls = next(iter(get_type_hints(func).items()))
            if not _is_valid_dispatch_type(cls):
                    f"Invalid annotation for {argname!r}. "
                )

        registry[cls] = func
        if cache_token is None and hasattr(cls, '__abstractmethods__'):
            cache_token = get_cache_token()
        dispatch_cache.clear()
        return func

    def wrapper(*args, **kw):
        if not args:
            raise TypeError(f'{funcname} requires at least '
                            '1 positional argument')

        return dispatch(args[0].__class__)(*args, **kw)

    funcname = getattr(func, '__name__', 'singledispatch function')
    registry[object] = func
    wrapper.register = register
    wrapper.dispatch = dispatch
    wrapper.registry = types.MappingProxyType(registry)
    wrapper._clear_cache = dispatch_cache.clear
    update_wrapper(wrapper, func)

class singledispatchmethod:
    """Single-dispatch generic method descriptor.

    """

    def __init__(self, func):
            raise TypeError(f"{func!r} is not callable or a descriptor")

        self.dispatcher = singledispatch(func)

    def register(self, cls, method=None):
        """generic_method.register(cls, func) -> func

        Registers a new implementation for the given *cls* on a *generic_method*.
        """
        return self.dispatcher.register(cls, func=method)

        def _method(*args, **kwargs):
            method = self.dispatcher.dispatch(args[0].__class__)
            return method.__get__(obj, cls)(*args, **kwargs)

        _method.register = self.register
        update_wrapper(_method, self.func)

    @property
        return getattr(self.func, '__isabstractmethod__', False)

### cached_property() - computed once per instance, cached as attribute

_NOT_FOUND = object()

class cached_property:
        self.attrname = None
        self.__doc__ = func.__doc__
        self.lock = RLock()

    def __set_name__(self, owner, name):
        if self.attrname is None:
            self.attrname = name
        elif name != self.attrname:
                "Cannot assign the same cached_property to two different names "
                f"({self.attrname!r} and {name!r})."
            )

    def __get__(self, instance, owner=None):
                "Cannot use cached_property instance without calling __set_name__ on it.")
        try:
            cache = instance.__dict__
        except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
            msg = (
                f"No '__dict__' attribute on {type(instance).__name__!r} "
                f"instance to cache {self.attrname!r} property."
            )
            raise TypeError(msg) from None
        val = cache.get(self.attrname, _NOT_FOUND)
        if val is _NOT_FOUND:
            with self.lock:
                # check if another thread filled cache while we awaited lock
                val = cache.get(self.attrname, _NOT_FOUND)
                if val is _NOT_FOUND:
                    val = self.func(instance)
                    try:
                        cache[self.attrname] = val
                    except TypeError:
                        msg = (
                            f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                            f"does not support item assignment for caching {self.attrname!r} property."
                        )
                        raise TypeError(msg) from None
        return val

"""
Command handler

This module contains the infrastructure for accepting commands on the
command line. The processing of a command works as follows:

1. The calling object (caller) is analyzed based on its callertype.
2. Cmdsets are gathered from different sources:
   - 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.
3. The collected cmdsets are merged together to a combined, current cmdset.
4. If the input string is empty -> check for CMD_NOINPUT command in
   current cmdset or fallback to error message. Exit.
5. The Command Parser is triggered, using the current cmdset to analyze the
   input string for possible command matches.
6. If multiple matches are found -> check for CMD_MULTIMATCH in current
   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. 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.
9. We have a unique cmdobject, primed for use. Call all hooks:
   `at_pre_cmd()`, `cmdobj.parse()`, `cmdobj.func()` and finally `at_post_cmd()`.
10. Return deferred that will fire with the return from `cmdobj.func()` (unused by default).

"""

import types
from collections import defaultdict
from copy import copy
from itertools import chain
from traceback import format_exc
from weakref import WeakValueDictionary

from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.task import deferLater

from evennia.commands.command import InterruptCommand
from evennia.utils import logger, utils
from evennia.utils.utils import string_suggestions

_IN_GAME_ERRORS = settings.IN_GAME_ERRORS

__all__ = ("cmdhandler", "InterruptCommand")
_CMDSET_MERGE_CACHE = WeakValueDictionary()

# tracks recursive calls by each caller
# to avoid infinite loops (commands calling themselves)
_COMMAND_NESTING = defaultdict(lambda: 0)
_COMMAND_RECURSION_LIMIT = 10

# This decides which command parser is to be used.
# You have to restart the server for changes to take effect.
_COMMAND_PARSER = utils.variable_from_module(*settings.COMMAND_PARSER.rsplit(".", 1))

# System command names - import these variables rather than trying to
# remember the actual string constants. If not defined, Evennia
# hard-coded defaults are used instead.

# command to call if user just presses <return> with no input
CMD_NOINPUT = "__noinput_command"
# command to call if no command match was found
CMD_NOMATCH = "__nomatch_command"
# command to call if multiple command matches were found
CMD_MULTIMATCH = "__multimatch_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))

# Output strings. The first is the IN_GAME_ERRORS return, the second
# is the normal "production message to echo to the account.

_ERROR_UNTRAPPED = (
    _(
        """
An untrapped error occurred.
"""
    ),
    _(
        """
An untrapped error occurred. Please file a bug report detailing the steps to reproduce.
"""
    ),
)

_ERROR_CMDSETS = (
    _(
        """
A cmdset merger-error occurred. This is often due to a syntax
error in one of the cmdsets to merge.
"""
    ),
    _(
        """
A cmdset merger-error occurred. Please file a bug report detailing the
steps to reproduce.
"""
    ),
)

_ERROR_NOCMDSETS = (
    _(
        """
No command sets found! This is a critical bug that can have
multiple causes.
"""
    ),
    _(
        """
No command sets found! This is a sign of a critical bug.  If
disconnecting/reconnecting doesn't" solve the problem, try to contact
the server admin through" some other means for assistance.
"""
    ),
)

_ERROR_CMDHANDLER = (
    _(
        """
A command handler bug occurred. If this is not due to a local change,
please file a bug report with the Evennia project, including the
traceback and steps to reproduce.
"""
    ),
    _(
        """
A command handler bug occurred. Please notify staff - they should
likely file a bug report with the Evennia project.
"""
    ),
)

_ERROR_RECURSION_LIMIT = _(
    "Command recursion limit ({recursion_limit}) reached for '{raw_cmdname}' ({cmdclass})."
)

# delayed imports
_GET_INPUT = None

# helper functions

def _msg_err(receiver, stringtuple):
    """
    Helper function for returning an error to the caller.

    Args:
        receiver (Object): object to get the error message.
        stringtuple (tuple): tuple with two strings - one for the
            _IN_GAME_ERRORS mode (with the traceback) and one with the
            production string (with a timestamp) to be shown to the user.

    """
    string = _("{traceback}\n{errmsg}\n(Traceback was logged {timestamp}).")
    timestamp = logger.timeformat()
    tracestring = format_exc()
    logger.log_trace()
    if _IN_GAME_ERRORS:
        receiver.msg(
            string.format(
                traceback=tracestring, errmsg=stringtuple[0].strip(), timestamp=timestamp
            ).strip()
        )
    else:
                traceback=tracestring.splitlines()[-1],
                errmsg=stringtuple[1].strip(),
                timestamp=timestamp,
            ).strip()
        )

def _process_input(caller, prompt, result, cmd, generator):
    """
    Specifically handle the get_input value to send to _progressive_cmd_run as
    part of yielding from a Command's `func`.

    Args:
        caller (Character, Account or Session): the caller.
        prompt (str): The sent prompt.
        result (str): The unprocessed answer.
        cmd (Command): The command itself.
        generator (GeneratorType): The generator.

    Returns:
        result (bool): Always `False` (stop processing).

    """
    # We call it using a Twisted deferLater to make sure the input is properly closed.
    deferLater(reactor, 0, _progressive_cmd_run, cmd, generator, response=result)
    return False

def _progressive_cmd_run(cmd, generator, response=None):
    """
    Progressively call the command that was given in argument. Used
    when `yield` is present in the Command's `func()` method.

    Args:
        cmd (Command): the command itself.
        generator (GeneratorType): the generator describing the processing.
        reponse (str, optional): the response to send to the generator.

    Raises:
        ValueError: If the func call yields something not identifiable as a
            time-delay or a string prompt.

    Note:
        This function is responsible for executing the command, if
        the func() method contains 'yield' instructions.  The yielded
        value will be accessible at each step and will affect the
        process.  If the value is a number, just delay the execution
        of the command.  If it's a string, wait for the user input.

    """
    global _GET_INPUT
    if not _GET_INPUT:
        from evennia.utils.evmenu import get_input as _GET_INPUT

    try:
        if response is None:
            value = next(generator)
        else:
            value = generator.send(response)
    except StopIteration:
        # duplicated from cmdhandler._run_command, to have these
        # run in the right order while staying inside the deferred
        cmd.at_post_cmd()
        if cmd.save_for_next:
            # store a reference to this command, possibly
            # accessible by the next command.
            cmd.caller.ndb.last_cmd = copy(cmd)
        else:
            cmd.caller.ndb.last_cmd = None
    else:
        if isinstance(value, (int, float)):
            utils.delay(value, _progressive_cmd_run, cmd, generator)
        elif isinstance(value, str):
            _GET_INPUT(cmd.caller, value, _process_input, cmd=cmd, generator=generator)
        else:
            raise ValueError("unknown type for a yielded value in command: {}".format(type(value)))

# custom Exceptions

class NoCmdSets(Exception):
    "No cmdsets found. Critical error."
    pass

class ExecSystemCommand(Exception):
    "Run a system command"

    def __init__(self, syscmd, sysarg):
        self.args = (syscmd, sysarg)  # needed by exception error handling
        self.syscmd = syscmd
        self.sysarg = sysarg

class ErrorReported(Exception):
    "Re-raised when a subsructure already reported the error"

    def __init__(self, raw_string):
        self.args = (raw_string,)
        self.raw_string = raw_string

# Helper function

@inlineCallbacks
def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string):
    """
    Gather all relevant cmdsets and merge them.

    Args:
        caller (Session, Account or Object): The entity executing the command. Which
            type of object this is depends on the current game state; for example
            when the user is not logged in, this will be a Session, when being OOC
            it will be an Account and when puppeting an object this will (often) be
            a Character Object. In the end it depends on where the cmdset is stored.
        session (Session or None): The Session associated with caller, if any.
        account (Account or None): The calling Account associated with caller, if any.
        obj (Object or None): The Object associated with caller, if any.
        callertype (str): This identifies caller as either "account", "object" or "session"
            to avoid having to do this check internally.
        raw_string (str): The input string. This is only used for error reporting.

    Returns:
        cmdset (Deferred): This deferred fires with the merged cmdset
        result once merger finishes.

    Notes:
        The cdmsets are merged in order or generality, so that the
        Object's cmdset is merged last (and will thus take precedence
        over same-named and same-prio commands on Account and Session).

    """
    try:

        @inlineCallbacks
        def _get_local_obj_cmdsets(obj):
            """
            Helper-method; Get Object-level cmdsets

            """
            # Gather cmdsets from location, objects in location or carried
            try:
                local_obj_cmdsets = [None]
                try:
                    location = obj.location
                    location = None
                if location:
                    # Gather all cmdsets stored on objects in the room and
                    # also in the caller's inventory and the location itself
                    local_objlist = yield (
                        location.contents_get(exclude=obj) + obj.contents_get() + [location]
                    )
                    local_objlist = [o for o in local_objlist if not o._is_deleted]
                    for lobj in local_objlist:
                        try:
                            # call hook in case we need to do dynamic changing to cmdset
                            _GA(lobj, "at_cmdset_get")(caller=caller)
                        except Exception:
                            logger.log_trace()
                    # the call-type lock is checked here, it makes sure an account
                    # is not seeing e.g. the commands on a fellow account (which is why
                    # the no_superuser_bypass must be True)
                    local_obj_cmdsets = yield list(
                        chain.from_iterable(
                            lobj.cmdset.cmdset_stack
                            for lobj in local_objlist
                            if (
                                lobj.cmdset.current
                                and lobj.access(
                                    caller, access_type="call", no_superuser_bypass=True
                                )
                            )
                        )
                    )
                    for cset in local_obj_cmdsets:
                        # This is necessary for object sets, or we won't be able to
                        # separate the command sets from each other in a busy room. We
                        # only keep the setting if duplicates were set to False/True
                        # explicitly.
                        cset.old_duplicates = cset.duplicates
                        cset.duplicates = True if cset.duplicates is None else cset.duplicates
                returnValue(local_obj_cmdsets)
                _msg_err(caller, _ERROR_CMDSETS)
                raise ErrorReported(raw_string)

        def _get_cmdsets(obj):
            """
            Helper method; Get cmdset while making sure to trigger all
            hooks safely. Returns the stack and the valid options.

            """
            try:
                yield obj.at_cmdset_get()
            try:
                returnValue((obj.cmdset.current, list(obj.cmdset.cmdset_stack)))
            except AttributeError:
                returnValue(((None, None, None), []))

        local_obj_cmdsets = []
        if callertype == "session":
            # we are calling the command from the session level
            report_to = session
            current, cmdsets = yield _get_cmdsets(session)
            if account:  # this automatically implies logged-in
                pcurrent, account_cmdsets = yield _get_cmdsets(account)
                cmdsets += account_cmdsets
                current = current + pcurrent
                if obj:
                    ocurrent, obj_cmdsets = yield _get_cmdsets(obj)
                    current = current + ocurrent
                    cmdsets += obj_cmdsets
                    if not current.no_objs:
                        local_obj_cmdsets = yield _get_local_obj_cmdsets(obj)
                        if current.no_exits:
                            # filter out all exits
                            local_obj_cmdsets = [
                                cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
                            ]
                        cmdsets += local_obj_cmdsets

        elif callertype == "account":
            # we are calling the command from the account level
            report_to = account
            current, cmdsets = yield _get_cmdsets(account)
            if obj:
                ocurrent, obj_cmdsets = yield _get_cmdsets(obj)
                current = current + ocurrent
                cmdsets += obj_cmdsets
                if not current.no_objs:
                    local_obj_cmdsets = yield _get_local_obj_cmdsets(obj)
                    if current.no_exits:
                        # filter out all exits
                        local_obj_cmdsets = [
                            cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
                        ]
                    cmdsets += local_obj_cmdsets

        elif callertype == "object":
            # we are calling the command from the object level
            report_to = obj
            current, cmdsets = yield _get_cmdsets(obj)
            if not current.no_objs:
                local_obj_cmdsets = yield _get_local_obj_cmdsets(obj)
                if current.no_exits:
                    # filter out all exits
                    local_obj_cmdsets = [
                        cmdset for cmdset in local_obj_cmdsets if cmdset.key != "ExitCmdSet"
                    ]
                cmdsets += yield local_obj_cmdsets
        else:
            raise Exception("get_and_merge_cmdsets: callertype %s is not valid." % callertype)

        # weed out all non-found sets
        cmdsets = yield [cmdset for cmdset in cmdsets if cmdset and cmdset.key != "_EMPTY_CMDSET"]
        # report cmdset errors to user (these should already have been logged)
        yield [
            report_to.msg(cmdset.errmessage) for cmdset in cmdsets if cmdset.key == "_CMDSET_ERROR"
        ]

        if cmdsets:
            # faster to do tuple on list than to build tuple directly
            mergehash = tuple([id(cmdset) for cmdset in cmdsets])
            if mergehash in _CMDSET_MERGE_CACHE:
                # cached merge exist; use that
                cmdset = _CMDSET_MERGE_CACHE[mergehash]
            else:
                # we group and merge all same-prio cmdsets separately (this avoids
                # order-dependent clashes in certain cases, such as
                # when duplicates=True)
                tempmergers = {}
                for cmdset in cmdsets:
                    prio = cmdset.priority
                    if prio in tempmergers:
                        # merge same-prio cmdset together separately
                        tempmergers[prio] = yield tempmergers[prio] + cmdset
                    else:
                        tempmergers[prio] = cmdset

                # sort cmdsets after reverse priority (highest prio are merged in last)
                sorted_cmdsets = yield sorted(list(tempmergers.values()), key=lambda x: x.priority)

                # Merge all command sets into one, beginning with the lowest-prio one
                cmdset = sorted_cmdsets[0]
                for merging_cmdset in sorted_cmdsets[1:]:
                    cmdset = yield cmdset + merging_cmdset
                # store the original, ungrouped set for diagnosis
                cmdset.merged_from = cmdsets
                # cache
                _CMDSET_MERGE_CACHE[mergehash] = cmdset
        else:
            cmdset = None
        for cset in (cset for cset in local_obj_cmdsets if cset):
            cset.duplicates = cset.old_duplicates
        # important - this syncs the CmdSetHandler's .current field with the
        # true current cmdset!
        # TODO - removed because this causes cmdset overlaps across sessions/accounts
        # - see https://github.com/evennia/evennia/issues/2855
        # if cmdset:
        #     caller.cmdset.current = cmdset

        returnValue(cmdset)
    except ErrorReported:
        raise
        _msg_err(caller, _ERROR_CMDSETS)
        raise
        # raise ErrorReported

# Main command-handler function

def cmdhandler(
    called_by,
    raw_string,
    _testing=False,
    callertype="session",
    session=None,
    cmdobj=None,
    cmdobj_key=None,
    **kwargs,
):
    """
    This is the main mechanism that handles any string sent to the engine.

    Args:
        called_by (Session, Account or Object): Object from which this
            command was called. which this was called from.  What this is
            depends on the game state.
        raw_string (str): The command string as given on the command line.
        _testing (bool, optional): Used for debug purposes and decides if we
            should actually execute the command or not. If True, the
            command instance will be returned.
        callertype (str, optional): One of "session", "account" or
            "object". These are treated in decending order, so when the
            Session is the caller, it will merge its own cmdset into
            cmdsets from both Account and eventual puppeted Object (and
            cmdsets in its room etc). An Account will only include its own
            cmdset and the Objects and so on. Merge order is the same
            order, so that Object cmdsets are merged in last, giving them
            precendence for same-name and same-prio commands.
        session (Session, optional): Relevant if callertype is "account" - the session will help
            retrieve the correct cmdsets from puppeted objects.
        cmdobj (Command, optional): If given a command instance, this will be executed using
            `called_by` as the caller, `raw_string` representing its arguments and (optionally)
            `cmdobj_key` as its input command name. No cmdset lookup will be performed but
            all other options apply as normal. This allows for running a specific Command
            within the command system mechanism.
        cmdobj_key (string, optional): Used together with `cmdobj` keyword to specify
            which cmdname should be assigned when calling the specified Command instance. This
            is made available as `self.cmdstring` when the Command runs.
            If not given, the command will be assumed to be called as `cmdobj.key`.

    Keyword Args:
        kwargs (any): other keyword arguments will be assigned as named variables on the
            retrieved command object *before* it is executed. This is unused
            in default Evennia but may be used by code to set custom flags or
            special operating conditions for a command as it executes.

    Returns:
        deferred (Deferred): This deferred is fired with the return
        value of the command's `func` method.  This is not used in
        default Evennia.

    """

    @inlineCallbacks
    def _run_command(cmd, cmdname, args, raw_cmdname, cmdset, session, account):
        """
        Helper function: This initializes and runs the Command
        instance once the parser has identified it as either a normal
        command or one of the system commands.

        Args:
            cmd (Command): Command object
            cmdname (str): Name of command
            args (str): extra text entered after the identified command
            raw_cmdname (str): Name of Command, unaffected by eventual
                prefix-stripping (if no prefix-stripping, this is the same
                as cmdname).
            cmdset (CmdSet): Command sert the command belongs to (if any)..
            session (Session): Session of caller (if any).
            account (Account): Account of caller (if any).

        Returns:
            deferred (Deferred): this will fire with the return of the
                command's `func` method.

        Raises:
            RuntimeError: If command recursion limit was reached.

        """
        global _COMMAND_NESTING
        try:
            # Assign useful variables to the instance
            cmd.caller = caller
            cmd.cmdname = cmdname
            cmd.raw_cmdname = raw_cmdname
            cmd.cmdstring = cmdname  # deprecated
            cmd.args = args
            cmd.cmdset = cmdset
            cmd.session = session
            cmd.account = account
            cmd.raw_string = unformatted_raw_string
            # cmd.obj  # set via on-object cmdset handler for each command,
            # since this may be different for every command when
            # merging multiple cmdsets

            if _testing:
                # only return the command instance
                returnValue(cmd)

            # assign custom kwargs to found cmd object
            for key, val in kwargs.items():
                setattr(cmd, key, val)

            _COMMAND_NESTING[called_by] += 1
            if _COMMAND_NESTING[called_by] > _COMMAND_RECURSION_LIMIT:
                err = _ERROR_RECURSION_LIMIT.format(
                    recursion_limit=_COMMAND_RECURSION_LIMIT,
                    raw_cmdname=raw_cmdname,
                    cmdclass=cmd.__class__,
                )
                raise RuntimeError(err)

            # pre-command hook
            abort = yield cmd.at_pre_cmd()
            if abort:
                # abort sequence
                returnValue(abort)

            # Parse and execute
            yield cmd.parse()

            # main command code
            # (return value is normally None)
            ret = cmd.func()
            if isinstance(ret, types.GeneratorType):
                # cmd.func() is a generator, execute progressively
                _progressive_cmd_run(cmd, ret)
                ret = yield ret
                # note that the _progressive_cmd_run will itself run
                # the at_post_cmd etc as it finishes; this is a bit of
                # code duplication but there seems to be no way to
                # catch the StopIteration here (it's not in the same
                # frame since this is in a deferred chain)
            else:
                # post-command hook
                yield cmd.at_post_cmd()

                if cmd.save_for_next:
                    # store a reference to this command, possibly
                    # accessible by the next command.
                    caller.ndb.last_cmd = yield copy(cmd)
                else:
                    caller.ndb.last_cmd = None

            # return result to the deferred
            returnValue(ret)

        except InterruptCommand:
            # Do nothing, clean exit
            pass
            _msg_err(caller, _ERROR_UNTRAPPED)
            raise ErrorReported(raw_string)
        finally:
            _COMMAND_NESTING[called_by] -= 1

    session, account, obj = session, None, None
    if callertype == "session":
        session = called_by
        account = session.account
        obj = session.puppet
    elif callertype == "account":
        account = called_by
            obj = yield session.puppet
    elif callertype == "object":
        obj = called_by
    else:
        raise RuntimeError("cmdhandler: callertype %s is not valid." % callertype)
    # the caller will be the one to receive messages and excert its permissions.
    # we assign the caller with preference 'bottom up'
    caller = obj or account or session
    # The error_to is the default recipient for errors. Tries to make sure an account
    # does not get spammed for errors while preserving character mirroring.
    error_to = obj or session or account

    try:  # catch bugs in cmdhandler itself
        try:  # catch special-type commands
            if cmdobj:
                # the command object is already given
                cmd = cmdobj() if callable(cmdobj) else cmdobj
                cmdname = cmdobj_key if cmdobj_key else cmd.key
                args = raw_string
                unformatted_raw_string = "%s%s" % (cmdname, args)
                cmdset = None
                raw_cmdname = cmdname
                # session = session
                # account = account

            else:
                # no explicit cmdobject given, figure it out
                cmdset = yield get_and_merge_cmdsets(
                    caller, session, account, obj, callertype, raw_string
                )
                if not cmdset:
                    # this is bad and shouldn't happen.
                    raise NoCmdSets
                # store the completely unmodified raw string - including
                # whitespace and eventual prefixes-to-be-stripped.
                unformatted_raw_string = raw_string
                raw_string = raw_string.strip()
                if not raw_string:
                    # Empty input. Test for system command instead.
                    syscmd = yield cmdset.get(CMD_NOINPUT)
                    sysarg = ""
                    raise ExecSystemCommand(syscmd, sysarg)
                # Parse the input string and match to available cmdset.
                # This also checks for permissions, so all commands in match
                # are commands the caller is allowed to call.
                matches = yield _COMMAND_PARSER(raw_string, cmdset, caller)

                # Deal with matches

                if len(matches) > 1:
                    # We have a multiple-match
                    syscmd = yield cmdset.get(CMD_MULTIMATCH)
                    sysarg = _("There were multiple matches.")
                    if syscmd:
                        # use custom CMD_MULTIMATCH
                        syscmd.matches = matches
                    else:
                        # fall back to default error handling
                        sysarg = yield _SEARCH_AT_RESULT(
                            [match[2] for match in matches], caller, query=matches[0][0]
                        )

                cmdname, args, cmd, raw_cmdname = "", "", None, ""
                if len(matches) == 1:
                    # We have a unique command match. But it may still be invalid.
                    match = matches[0]
                    cmdname, args, cmd, raw_cmdname = (match[0], match[1], match[2], match[5])

                if not matches:
                    # No commands match our entered command
                    syscmd = yield cmdset.get(CMD_NOMATCH)
                    if syscmd:
                        # use custom CMD_NOMATCH command
                        sysarg = raw_string
                    else:
                        # fallback to default error text
                        sysarg = _("Command '{command}' is not available.").format(
                            command=raw_string
                        )
                        suggestions = string_suggestions(
                            raw_string,
                            cmdset.get_all_cmd_keys_and_aliases(caller),
                            cutoff=0.7,
                            maxnum=3,
                        )
                        if suggestions:
                            sysarg += _(" Maybe you meant {command}?").format(
                                command=utils.list_to_string(
                                    suggestions, endsep=_("or"), addquote=True
                                )
                            )
                        else:
                            sysarg += _(' Type "help" for help.')

            if not cmd.retain_instance:
                # making a copy allows multiple users to share the command also when yield is used
                cmd = copy(cmd)

            # A normal command.
            ret = yield _run_command(cmd, cmdname, args, raw_cmdname, cmdset, session, account)

        except ErrorReported as exc:
            # this error was already reported, so we
            # catch it here and don't pass it on.
            logger.log_err("User input was: '%s'." % exc.raw_string)

        except ExecSystemCommand as exc:
            # Not a normal command: run a system command, if available,
            # or fall back to a return string.
            syscmd = exc.syscmd
            sysarg = exc.sysarg

            if syscmd:
                ret = yield _run_command(
                    syscmd, syscmd.key, sysarg, unformatted_raw_string, cmdset, session, account
                )
                returnValue(ret)
            elif sysarg:
                # return system arg
                error_to.msg(exc.sysarg)

        except NoCmdSets:
            # Critical error.
            logger.log_err("No cmdsets found: %s" % caller)
            error_to.msg(_ERROR_NOCMDSETS)

            # We should not end up here. If we do, it's a programming bug.
            _msg_err(error_to, _ERROR_UNTRAPPED)

        # This catches exceptions in cmdhandler exceptions themselves
        _msg_err(error_to, _ERROR_CMDHANDLER)

"""
The base Command class.

All commands in Evennia inherit from the 'Command' class in this module.

"""
import math
import re

from django.urls import reverse
from django.utils.text import slugify

from evennia.locks.lockhandler import LockHandler
from evennia.utils.ansi import ANSIString
from evennia.utils.evtable import EvTable
from evennia.utils.utils import fill, is_iter, lazy_property, make_iter

CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES

class InterruptCommand(Exception):

    """Cleanly interrupt a command."""

    pass

def _init_command(cls, **kwargs):
    """
    Helper command.
    Makes sure all data are stored as lowercase and
    do checking on all properties that should be in list form.
    Sets up locks to be more forgiving. This is used both by the metaclass
    and (optionally) at instantiation time.

    If kwargs are given, these are set as instance-specific properties
    on the command - but note that the Command instance is *re-used* on a given
    host object, so a kwarg value set on the instance will *remain* on the instance
    for subsequent uses of that Command on that particular object.

    """
    for i in range(len(kwargs)):
        # used for dynamic creation of commands
        key, value = kwargs.popitem()
        setattr(cls, key, value)

    cls.key = cls.key.lower()
    if cls.aliases and not is_iter(cls.aliases):
        try:
            cls.aliases = [str(alias).strip().lower() for alias in cls.aliases.split(",")]
            cls.aliases = []
    cls.aliases = list(set(alias for alias in cls.aliases if alias and alias != cls.key))

    # optimization - a set is much faster to match against than a list. This is useful
    # for 'does any match' kind of queries
    cls._matchset = set([cls.key] + cls.aliases)
    # optimization for looping over keys+aliases. We sort by longest entry first, since we
    # want to be able to catch commands with spaces in the alias/key (so if you have key 'smile'
    # and alias 'smile at', writing 'smile at' should not risk being interpreted as 'smile'
    # with an argument 'at')
    cls._keyaliases = tuple(sorted([cls.key] + cls.aliases, key=len, reverse=True))

    # by default we don't save the command between runs
    if not hasattr(cls, "save_for_next"):
        cls.save_for_next = False

    # pre-process locks as defined in class definition
    temp = []
    if hasattr(cls, "permissions"):
        cls.locks = cls.permissions
    if not hasattr(cls, "locks"):
        # default if one forgets to define completely
        cls.locks = "cmd:all()"
    if "cmd:" not in cls.locks:
        cls.locks = "cmd:all();" + cls.locks
    for lockstring in cls.locks.split(";"):
        if lockstring and ":" not in lockstring:
            lockstring = "cmd:%s" % lockstring
        temp.append(lockstring)
    cls.lock_storage = ";".join(temp)

    if hasattr(cls, "arg_regex") and isinstance(cls.arg_regex, str):
        cls.arg_regex = re.compile(r"%s" % cls.arg_regex, re.I + re.UNICODE)
    if not hasattr(cls, "auto_help"):
        cls.auto_help = True
    if not hasattr(cls, "is_exit"):
        cls.is_exit = False
    if not hasattr(cls, "help_category"):
        cls.help_category = "general"
    if not hasattr(cls, "retain_instance"):
        cls.retain_instance = False

    # make sure to pick up the parent's docstring if the child class is
    # missing one (important for auto-help)
    if cls.__doc__ is None:
        for parent_class in inspect.getmro(cls):
            if parent_class.__doc__ is not None:
                cls.__doc__ = parent_class.__doc__
                break
    cls.help_category = cls.help_category.lower()

    # pre-prepare a help index entry for quicker lookup
    # strip the @- etc to allow help to be agnostic
    stripped_key = cls.key[1:] if cls.key and cls.key[0] in CMD_IGNORE_PREFIXES else ""
    stripped_aliases = " ".join(
        al[1:] if al and al[0] in CMD_IGNORE_PREFIXES else al for al in cls.aliases
    )
    cls.search_index_entry = {
        "key": cls.key,
        "aliases": " ".join(cls.aliases),
        "no_prefix": f"{stripped_key} {stripped_aliases}",
        "category": cls.help_category,
        "text": cls.__doc__,
        "tags": "",
    }

class CommandMeta(type):
    """
    The metaclass cleans up all properties on the class
    """

    def __init__(cls, *args, **kwargs):
        _init_command(cls, **kwargs)
        super().__init__(*args, **kwargs)

#    The Command class is the basic unit of an Evennia command; when
#    defining new commands, the admin subclass this class and
#    define their own parser method to handle the input. The
#    advantage of this is inheritage; commands that have similar
#    structure can parse the input string the same way, minimizing
#    parsing errors.

class Command(metaclass=CommandMeta):
    """
    ## Base command

    (you may see this if a child command had no help text defined)

    Usage:
      command [args]

    This is the base command class. Inherit from this
    to create new commands.

    The cmdhandler makes the following variables available to the
    command methods (so you can always assume them to be there):

    self.caller - the game object calling the command
    self.cmdstring - the command name used to trigger this command (allows
                     you to know which alias was used, for example)
    self.args - everything supplied to the command following the cmdstring
               (this is usually what is parsed in self.parse())
    self.cmdset - the merged cmdset from which this command was matched (useful only
                  seldomly, notably for help-type commands, to create dynamic
                  help entries and lists)
    self.cmdset_source - the specific cmdset this command was matched from.
    self.obj - the object on which this command is defined. If a default command,
               this is usually the same as caller.
    self.raw_string - the full raw string input, including the command name,
                      any args and no parsing.

    The following class properties can/should be defined on your child class:

    key - identifier for command (e.g. "look")
    aliases - (optional) list of aliases (e.g. ["l", "loo"])
    locks - lock string (default is "cmd:all()")
    help_category - how to organize this help entry in help system
                    (default is "General")
    auto_help - defaults to True. Allows for turning off auto-help generation
    arg_regex - (optional) raw string regex defining how the argument part of
                the command should look in order to match for this command
                (e.g. must it be a space between cmdname and arg?)
    auto_help_display_key - (optional) if given, this replaces the string shown
        in the auto-help listing. This is particularly useful for system-commands
        whose actual key is not really meaningful.

    (Note that if auto_help is on, this initial string is also used by the
    system to create the help entry for the command, so it's a good idea to
    format it similar to this one).  This behavior can be changed by
    overriding the method 'get_help' of a command: by default, this
    method returns cmd.__doc__ (that is, this very docstring, or
    the docstring of your command).  You can, however, extend or
    replace this without disabling auto_help.
    """

    # the main way to call this command (e.g. 'look')
    key = "command"
    # alternative ways to call the command (e.g. 'l', 'glance', 'examine')
    aliases = []
    # a list of lock definitions on the form
    #   cmd:[NOT] func(args) [ AND|OR][ NOT] func2(args)
    locks = settings.COMMAND_DEFAULT_LOCKS
    # used by the help system to group commands in lists.
    help_category = settings.COMMAND_DEFAULT_HELP_CATEGORY
    # This allows to turn off auto-help entry creation for individual commands.
    auto_help = True
    # optimization for quickly separating exit-commands from normal commands
    is_exit = False
    # define the command not only by key but by the regex form of its arguments
    arg_regex = settings.COMMAND_DEFAULT_ARG_REGEX
    # whether self.msg sends to all sessions of a related account/object (default
    # is to only send to the session sending the command).
    msg_all_sessions = settings.COMMAND_DEFAULT_MSG_ALL_SESSIONS
    # whether the exact command instance should be retained between command calls.
    # By default it's False; this allows for retaining state and saves some CPU, but
    # can cause cross-talk between users if multiple users access the same command
    # (especially if the command is using yield)
    retain_instance = False

    # auto-set (by Evennia on command instantiation) are:
    #   obj - which object this command is defined on
    #   session - which session is responsible for triggering this command. Only set
    #             if triggered by an account.

    def __init__(self, **kwargs):
        """
        The lockhandler works the same as for objects.
        optional kwargs will be set as properties on the Command at runtime,
        overloading evential same-named class properties.

        """
        if kwargs:
            _init_command(self, **kwargs)
        self._optimize()

    def lockhandler(self):
        return LockHandler(self)

        """
        Print the command key
        """
        return self.key

    def __eq__(self, cmd):
        """
        Compare two command instances to each other by matching their
        key and aliases.

        Args:
            cmd (Command or str): Allows for equating both Command
                objects and their keys.

        Returns:
            equal (bool): If the commands are equal or not.

        """
        try:
            # first assume input is a command (the most common case)
            return self._matchset.intersection(cmd._matchset)
            # probably got a string
            return cmd in self._matchset

        """
        Python 3 requires that any class which implements __eq__ must also
        implement __hash__ and that the corresponding hashes for equivalent
        instances are themselves equivalent.

        Technically, the following implementation is only valid for comparison
        against other Commands, as our __eq__ supports comparison against
        str, too.

        """
        return hash("command")

    def __ne__(self, cmd):
        """
        The logical negation of __eq__. Since this is one of the most
        called methods in Evennia (along with __eq__) we do some
        code-duplication here rather than issuing a method-lookup to
        __eq__.
        """
        try:
            return self._matchset.isdisjoint(cmd._matchset)
            return cmd not in self._matchset

    def __contains__(self, query):
        """
        This implements searches like 'if query in cmd'. It's a fuzzy
        matching used by the help system, returning True if query can
        be found as a substring of the commands key or its aliases.

        Args:
            query (str): query to match against. Should be lower case.

        Returns:
            result (bool): Fuzzy matching result.

        """
        return any(query in keyalias for keyalias in self._keyaliases)

    def _optimize(self):
        """
        Optimize the key and aliases for lookups.
        """
        # optimization - a set is much faster to match against than a list
        matches = [self.key.lower()]
        matches.extend(x.lower() for x in self.aliases)

        self._matchset = set(matches)
        # optimization for looping over keys+aliases. We sort by longest entry first, since we
        # want to be able to catch commands with spaces in the alias/key (so if you have key 'smile'
        # and alias 'smile at', writing 'smile at' should not risk being interpreted as 'smile'
        # with an argument 'at')
        self._keyaliases = tuple(sorted(matches, key=len, reverse=True))

        self._noprefix_aliases = {x.lstrip(CMD_IGNORE_PREFIXES): x for x in self._keyaliases}

    def set_key(self, new_key):
        """
        Update key.

        Args:
            new_key (str): The new key.

        Notes:
            This is necessary to use to make sure the optimization
            caches are properly updated as well.

        """
        self.key = new_key.lower()

    def set_aliases(self, new_aliases):
        """
        Replace aliases with new ones.

        Args:
            new_aliases (str or list): Either a ;-separated string
                or a list of aliases. These aliases will replace the
                existing ones, if any.

        Notes:

        """
        if isinstance(new_aliases, str):
            new_aliases = new_aliases.split(";")
        aliases = (str(alias).strip().lower() for alias in make_iter(new_aliases))
        self.aliases = list(set(alias for alias in aliases if alias != self.key))

    def match(self, cmdname, include_prefixes=True):
        """
        This is called by the system when searching the available commands,
        in order to determine if this is the one we wanted. cmdname was
        previously extracted from the raw string by the system.

        Args:
            cmdname (str): Always lowercase when reaching this point.

        Kwargs:
            include_prefixes (bool): If false, will compare against the _noprefix
                variants of commandnames.

        Returns:
            result (bool): Match result.

        """
        if include_prefixes:
            for cmd_key in self._keyaliases:
                if cmdname.startswith(cmd_key) and (
                    not self.arg_regex or self.arg_regex.match(cmdname[len(cmd_key) :])
                ):
                    return cmd_key, cmd_key
        else:
            for k, v in self._noprefix_aliases.items():
                if cmdname.startswith(k) and (
                    not self.arg_regex or self.arg_regex.match(cmdname[len(k) :])
                ):
                    return k, v
        return None, None

    def access(self, srcobj, access_type="cmd", default=False):
        """
        This hook is called by the cmdhandler to determine if srcobj
        is allowed to execute this command. It should return a boolean
        value and is not normally something that need to be changed since
        it's using the Evennia permission system directly.

        Args:
            srcobj (Object): Object trying to gain permission
            access_type (str, optional): The lock type to check.
            default (bool, optional): The fallback result if no lock
                of matching `access_type` is found on this Command.

        """
        return self.lockhandler.check(srcobj, access_type, default=default)

    def msg(self, text=None, to_obj=None, from_obj=None, session=None, **kwargs):
        """
        This is a shortcut instead of calling msg() directly on an
        object - it will detect if caller is an Object or an Account and
        also appends self.session automatically if self.msg_all_sessions is False.

        Args:
            text (str, optional): Text string of message to send.
            to_obj (Object, optional): Target object of message. Defaults to self.caller.
            from_obj (Object, optional): Source of message. Defaults to to_obj.
            session (Session, optional): Supply data only to a unique
                session (ignores the value of `self.msg_all_sessions`).

            options (dict): Options to the protocol.
            any (any): All other keywords are interpreted as th
                name of send-instructions.

        """
        from_obj = from_obj or self.caller
        to_obj = to_obj or from_obj
        if not session and not self.msg_all_sessions:
            if to_obj == self.caller:
                session = self.session
            else:
                session = to_obj.sessions.get()
        to_obj.msg(text=text, from_obj=from_obj, session=session, **kwargs)

    def execute_cmd(self, raw_string, session=None, obj=None, **kwargs):
        """
        A shortcut of execute_cmd on the caller. It appends the
        session automatically.

        Args:
            raw_string (str): Execute this string as a command input.
            session (Session, optional): If not given, the current command's Session will be used.
            obj (Object or Account, optional): Object or Account on which to call the execute_cmd.
                If not given, self.caller will be used.

            Other keyword arguments will be added to the found command
            object instace as variables before it executes.  This is
            unused by default Evennia but may be used to set flags and
            change operating paramaters for commands at run-time.

        """
        obj = self.caller if obj is None else obj
        session = self.session if session is None else session
        obj.execute_cmd(raw_string, session=session, **kwargs)

    # Common Command hooks

    def at_pre_cmd(self):
        """
        This hook is called before self.parse() on all commands.  If
        this hook returns anything but False/None, the command
        sequence is aborted.

        """
        pass

    def at_post_cmd(self):
        """
        This hook is called after the command has finished executing
        (after self.func()).

        """
        pass

    def parse(self):
        """
        Once the cmdhandler has identified this as the command we
        want, this function is run. If many of your commands have a
        similar syntax (for example 'cmd arg1 = arg2') you should
        simply define this once and just let other commands of the
        same form inherit from this. See the docstring of this module
        for which object properties are available to use (notably
        self.args).

        """
        pass

    def get_command_info(self):
        """
        This is the default output of func() if no func() overload is done.
        Provided here as a separate method so that it can be called for debugging
        purposes when making commands.

        """
        variables = "\n".join(
            " |w{}|n ({}): {}".format(key, type(val), val) for key, val in self.__dict__.items()
        )
        string = f"""
Command {self} has no defined `func()` - showing on-command variables:
{variables}
        """
        # a simple test command to show the available properties
        string += "-" * 50
        string += "\n|w%s|n - Command variables from evennia:\n" % self.key
        string += "\nname of cmd (self.key): |w%s|n\n" % self.key
        string += "cmd aliases (self.aliases): |w%s|n\n" % self.aliases
        string += "cmd locks (self.locks): |w%s|n\n" % self.locks
        string += "help category (self.help_category): |w%s|n\n" % self.help_category.capitalize()
        string += "object calling (self.caller): |w%s|n\n" % self.caller
        string += "object storing cmdset (self.obj): |w%s|n\n" % self.obj
        string += "command string given (self.cmdstring): |w%s|n\n" % self.cmdstring
        # show cmdset.key instead of cmdset to shorten output
        string += fill(
            "current cmdset (self.cmdset): |w%s|n\n"
            % (self.cmdset.key if self.cmdset.key else self.cmdset.__class__)
        )

        self.msg(string)

    def func(self):
        """
        This is the actual executing part of the command.  It is
        called directly after self.parse(). See the docstring of this
        module for which object properties are available (beyond those
        set in self.parse())

        """
        self.get_command_info()

    def get_extra_info(self, caller, **kwargs):
        """
        Display some extra information that may help distinguish this
        command from others, for instance, in a disambiguity prompt.

        If this command is a potential match in an ambiguous
        situation, one distinguishing feature may be its attachment to
        a nearby object, so we include this if available.

        Args:
            caller (TypedObject): The caller who typed an ambiguous
            term handed to the search function.

        Returns:
            A string with identifying information to disambiguate the
            object, conventionally with a preceding space.

        """
        if hasattr(self, "obj") and self.obj and self.obj != caller:
            return " (%s)" % self.obj.get_display_name(caller).strip()
        return ""

    def get_help(self, caller, cmdset):
        """
        Return the help message for this command and this caller.

        By default, return self.__doc__ (the docstring just under
        the class definition).  You can override this behavior,
        though, and even customize it depending on the caller, or other
        commands the caller can use.

        Args:
            caller (Object or Account): the caller asking for help on the command.
            cmdset (CmdSet): the command set (if you need additional commands).

        Returns:
            docstring (str): the help text to provide the caller for this command.

        """
        return self.__doc__

    def web_get_detail_url(self):
        """
        Returns the URI path for a View that allows users to view details for
        this object.

        ex. Oscar (Character) = '/characters/oscar/1/'

        For this to work, the developer must have defined a named view somewhere
        in urls.py that follows the format 'modelname-action', so in this case
        a named view of 'character-detail' would be referenced by this method.

        ex.
        ::
            url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$',
                CharDetailView.as_view(), name='character-detail')

        If no View has been created and defined in urls.py, returns an
        HTML anchor.

        This method is naive and simply returns a path. Securing access to
        the actual view and limiting who can view this object is the developer's
        responsibility.

        Returns:
            path (str): URI path to object detail page, if defined.

        """
        try:
            return reverse(
                "help-entry-detail",
                kwargs={"category": slugify(self.help_category), "topic": slugify(self.key)},
            )
        except Exception as e:
            return "#"

    def web_get_admin_url(self):
        """
        Returns the URI path for the Django Admin page for this object.

        ex. Account#1 = '/admin/accounts/accountdb/1/change/'

        Returns:
            path (str): URI path to Django Admin page for object.

        """

    def client_width(self):
        """
        Get the client screenwidth for the session using this command.

        Returns:
            client width (int): The width (in characters) of the client window.

        """
        if self.session:
            return self.session.protocol_flags.get(
                "SCREENWIDTH", {0: settings.CLIENT_DEFAULT_WIDTH}
            )[0]
        return settings.CLIENT_DEFAULT_WIDTH

    def styled_table(self, *args, **kwargs):
        """
        Create an EvTable styled by on user preferences.

        Args:
            *args (str): Column headers. If not colored explicitly, these will get colors
                from user options.
            any (str, int or dict): EvTable options, including, optionally a `table` dict
                detailing the contents of the table.
        Returns:
            table (EvTable): An initialized evtable entity, either complete (if using `table` kwarg)
                or incomplete and ready for use with `.add_row` or `.add_collumn`.

        """
        border_color = self.account.options.get("border_color")
        column_color = self.account.options.get("column_names_color")

        colornames = ["|%s%s|n" % (column_color, col) for col in args]

        h_line_char = kwargs.pop("header_line_char", "~")
        header_line_char = ANSIString(f"|{border_color}{h_line_char}|n")
        c_char = kwargs.pop("corner_char", "+")
        corner_char = ANSIString(f"|{border_color}{c_char}|n")

        b_left_char = kwargs.pop("border_left_char", "||")
        border_left_char = ANSIString(f"|{border_color}{b_left_char}|n")

        b_right_char = kwargs.pop("border_right_char", "||")
        border_right_char = ANSIString(f"|{border_color}{b_right_char}|n")

        b_bottom_char = kwargs.pop("border_bottom_char", "-")
        border_bottom_char = ANSIString(f"|{border_color}{b_bottom_char}|n")

        b_top_char = kwargs.pop("border_top_char", "-")
        border_top_char = ANSIString(f"|{border_color}{b_top_char}|n")

        table = EvTable(
            *colornames,
            header_line_char=header_line_char,
            corner_char=corner_char,
            border_left_char=border_left_char,
            border_right_char=border_right_char,
            border_top_char=border_top_char,
            border_bottom_char=border_bottom_char,
            **kwargs,
        )
        return table

    def _render_decoration(
        self,
        header_text=None,
        fill_character=None,
        edge_character=None,
        mode="header",
        color_header=True,
        width=None,
    ):
        """
        Helper for formatting a string into a pretty display, for a header, separator or footer.

            header_text (str): Text to include in header.
            fill_character (str): This single character will be used to fill the width of the
                display.
            edge_character (str): This character caps the edges of the display.
            mode(str): One of 'header', 'separator' or 'footer'.
            color_header (bool): If the header should be colorized based on user options.
            width (int): If not given, the client's width will be used if available.

        Returns:
            string (str): The decorated and formatted text.

        """

        colors = dict()
        colors["border"] = self.account.options.get("border_color")
        colors["headertext"] = self.account.options.get("%s_text_color" % mode)
        colors["headerstar"] = self.account.options.get("%s_star_color" % mode)

        width = width or self.client_width()
        if edge_character:
            width -= 2

        if header_text:
            if color_header:
                header_text = ANSIString(header_text).clean()
                header_text = ANSIString("|n|%s%s|n" % (colors["headertext"], header_text))
            if mode == "header":
                begin_center = ANSIString(
                    "|n|%s<|%s* |n" % (colors["border"], colors["headerstar"])
                )
                end_center = ANSIString("|n |%s*|%s>|n" % (colors["headerstar"], colors["border"]))
                center_string = ANSIString(begin_center + header_text + end_center)
            else:
                center_string = ANSIString("|n |%s%s |n" % (colors["headertext"], header_text))
        else:
            center_string = ""

        fill_character = self.account.options.get("%s_fill" % mode)

        remain_fill = width - len(center_string)
        if remain_fill % 2 == 0:
            right_width = remain_fill / 2
            left_width = remain_fill / 2
        else:
            right_width = math.floor(remain_fill / 2)
            left_width = math.ceil(remain_fill / 2)

        right_fill = ANSIString("|n|%s%s|n" % (colors["border"], fill_character * int(right_width)))
        left_fill = ANSIString("|n|%s%s|n" % (colors["border"], fill_character * int(left_width)))

            edge_fill = ANSIString("|n|%s%s|n" % (colors["border"], edge_character))
            main_string = ANSIString(center_string)
            final_send = (
                ANSIString(edge_fill) + left_fill + main_string + right_fill + ANSIString(edge_fill)
            )
        else:
            final_send = left_fill + ANSIString(center_string) + right_fill
        return final_send

    def styled_header(self, *args, **kwargs):
        """
        Create a pretty header.
        """

        if "mode" not in kwargs:
            kwargs["mode"] = "header"
        return self._render_decoration(*args, **kwargs)

    def styled_separator(self, *args, **kwargs):
        """
        Create a separator.

        """
            kwargs["mode"] = "separator"

    def styled_footer(self, *args, **kwargs):
        """
        Create a pretty footer.

        """
            kwargs["mode"] = "footer"

"""
The default command parser. Use your own by assigning
`settings.COMMAND_PARSER` to a Python path to a module containing the
replacing cmdparser function. The replacement parser must accept the
same inputs as the default one.

"""

import re

from evennia.utils.logger import log_trace

_MULTIMATCH_REGEX = re.compile(settings.SEARCH_MULTIMATCH_REGEX, re.I + re.U)
_CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES

def create_match(cmdname, string, cmdobj, raw_cmdname):
    """
    Builds a command match by splitting the incoming string and
    evaluating the quality of the match.

    Args:
        cmdname (str): Name of command to check for.
        string (str): The string to match against.
        cmdobj (str): The full Command instance.
        raw_cmdname (str, optional): If CMD_IGNORE_PREFIX is set and the cmdname starts with
            one of the prefixes to ignore, this contains the raw, unstripped cmdname,
            otherwise it is None.

    Returns:
        match (tuple): This is on the form (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname),
            where `cmdname` is the command's name and `args` is the rest of the incoming
            string, without said command name. `cmdobj` is
            the Command instance, the cmdlen is the same as len(cmdname) and mratio
            is a measure of how big a part of the full input string the cmdname
            takes up - an exact match would be 1.0. Finally, the `raw_cmdname` is
            the cmdname unmodified by eventual prefix-stripping.

    """
    cmdlen, strlen = len(str(cmdname)), len(str(string))
    mratio = 1 - (strlen - cmdlen) / (1.0 * strlen)
    args = string[cmdlen:]
    return (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname)

def build_matches(raw_string, cmdset, include_prefixes=False):
    """
    Build match tuples by matching raw_string against available commands.

    Args:
        raw_string (str): Input string that can look in any way; the only assumption is
            that the sought command's name/alias must be *first* in the string.
        cmdset (CmdSet): The current cmdset to pick Commands from.
        include_prefixes (bool): If set, include prefixes like @, ! etc (specified in settings)
            in the match, otherwise strip them before matching.

    Returns:
        matches (list) A list of match tuples created by `cmdparser.create_match`.

    """
    matches = []
    try:
        orig_string = raw_string
        if not include_prefixes and len(raw_string) > 1:
            raw_string = raw_string.lstrip(_CMD_IGNORE_PREFIXES)
        search_string = raw_string.lower()
        for cmd in cmdset:
            cmdname, raw_cmdname = cmd.match(search_string, include_prefixes=include_prefixes)
            if cmdname:
                matches.append(create_match(cmdname, raw_string, cmd, raw_cmdname))
        log_trace("cmdhandler error. raw_input:%s" % raw_string)
    return matches

def try_num_differentiators(raw_string):
    """
    Test if user tried to separate multi-matches with a number separator
    (default 1-name, 2-name etc). This is usually called last, if no other
    match was found.

    Args:
        raw_string (str): The user input to parse.

    Returns:
        mindex, new_raw_string (tuple): If a multimatch-separator was detected,
            this is stripped out as an integer to separate between the matches. The
            new_raw_string is the result of stripping out that identifier. If no
            such form was found, returns (None, None).

    Example:
        In the default configuration, entering 2-ball (e.g. in a room will more
        than one 'ball' object), will lead to a multimatch and this function
        will parse `"2-ball"` and return `(2, "ball")`.

    """
    # no matches found
    num_ref_match = _MULTIMATCH_REGEX.match(raw_string)
    if num_ref_match:
        # the user might be trying to identify the command
        # with a #num-command style syntax. We expect the regex to
        # contain the groups "number" and "name".
        mindex, new_raw_string = (
            num_ref_match.group("number"),
            num_ref_match.group("name") + num_ref_match.group("args"),
        )
        return int(mindex), new_raw_string
    else:

def cmdparser(raw_string, cmdset, caller, match_index=None):
    """
    This function is called by the cmdhandler once it has
    gathered and merged all valid cmdsets valid for this particular parsing.

    Args:
        raw_string (str): The unparsed text entered by the caller.
        cmdset (CmdSet): The merged, currently valid cmdset
        caller (Session, Account or Object): The caller triggering this parsing.
        match_index (int, optional): Index to pick a given match in a
            list of same-named command matches. If this is given, it suggests
            this is not the first time this function was called: normally
            the first run resulted in a multimatch, and the index is given
            to select between the results for the second run.

    Returns:
        matches (list): This is a list of match-tuples as returned by `create_match`.
            If no matches were found, this is an empty list.

    Notes:
        The cmdparser understand the following command combinations (where
        [] marks optional parts.

        ```
        [cmdname[ cmdname2 cmdname3 ...] [the rest]
        ```

        A command may consist of any number of space-separated words of any
        length, and contain any character. It may also be empty.

        The parser makes use of the cmdset to find command candidates. The
        parser return a list of matches. Each match is a tuple with its
        first three elements being the parsed cmdname (lower case),
        the remaining arguments, and the matched cmdobject from the cmdset.

    """
    if not raw_string:
        return []

    # find matches, first using the full name
    matches = build_matches(raw_string, cmdset, include_prefixes=True)

    if not matches or len(matches) > 1:
        # no single match, try parsing for optional numerical tags like 1-cmd
        # or cmd-2, cmd.2 etc
        match_index, new_raw_string = try_num_differentiators(raw_string)
        if match_index is not None:
            matches.extend(build_matches(new_raw_string, cmdset, include_prefixes=True))

    if not matches and _CMD_IGNORE_PREFIXES:
        # still no match. Try to strip prefixes
        raw_string = raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else raw_string
        matches = build_matches(raw_string, cmdset, include_prefixes=False)

    # only select command matches we are actually allowed to call.
    matches = [match for match in matches if match[2].access(caller, "cmd")]

    # try to bring the number of matches down to 1
    if len(matches) > 1:
        # See if it helps to analyze the match with preserved case but only if
        # it leaves at least one match.
        trimmed = [match for match in matches if raw_string.startswith(match[0])]
        if trimmed:
            matches = trimmed

        # we still have multiple matches. Sort them by count quality.
        matches = sorted(matches, key=lambda m: m[3])
        # only pick the matches with highest count quality
        quality = [mat[3] for mat in matches]
        matches = matches[-quality.count(quality[-1]) :]

        # still multiple matches. Fall back to ratio-based quality.
        matches = sorted(matches, key=lambda m: m[4])
        # only pick the highest rated ratio match
        quality = [mat[4] for mat in matches]

    if len(matches) > 1 and match_index is not None:
        # We couldn't separate match by quality, but we have an
        # index argument to tell us which match to use.
        if 0 < match_index <= len(matches):
            matches = [matches[match_index - 1]]
        else:
            # we tried to give an index outside of the range - this means
            # a no-match
            matches = []

    # no matter what we have at this point, we have to return it.

"""

A Command Set (CmdSet) holds a set of commands. The Cmdsets can be
merged and combined to create new sets of commands in a
non-destructive way. This makes them very powerful for implementing
custom game states where different commands (or different variations
of commands) are available to the accounts depending on circumstance.

The available merge operations are partly borrowed from mathematical
Set theory.

* Union The two command sets are merged so that as many commands as
    possible of each cmdset ends up in the merged cmdset. Same-name
    commands are merged by priority.  This is the most common default.
    Ex: A1,A3 + B1,B2,B4,B5 = A1,B2,A3,B4,B5
* Intersect - Only commands found in *both* cmdsets (i.e. which have
    same names) end up in the merged cmdset, with the higher-priority
    cmdset replacing the lower one. Ex: A1,A3 + B1,B2,B4,B5 = A1
* Replace -   The commands of this cmdset completely replaces the
    lower-priority cmdset's commands, regardless of if same-name commands
    exist. Ex: A1,A3 + B1,B2,B4,B5 = A1,A3
* Remove -    This removes the relevant commands from the
    lower-priority cmdset completely.  They are not replaced with
    anything, so this in effects uses the high-priority cmdset as a filter
    to affect the low-priority cmdset.  Ex: A1,A3 + B1,B2,B4,B5 = B2,B4,B5

"""
from weakref import WeakKeyDictionary

from evennia.utils.utils import inherits_from, is_iter

__all__ = ("CmdSet",)

class _CmdSetMeta(type):
    """
    This metaclass makes some minor on-the-fly convenience fixes to
    the cmdset class.

    """

        """
        Fixes some things in the cmdclass

        """
        # by default we key the cmdset the same as the
        # name of its class.
        if not hasattr(cls, "key") or not cls.key:
            cls.key = cls.__name__
        cls.path = "%s.%s" % (cls.__module__, cls.__name__)

        if not isinstance(cls.key_mergetypes, dict):
            cls.key_mergetypes = {}

class CmdSet(object, metaclass=_CmdSetMeta):
    """
    This class describes a unique cmdset that understands priorities.
    CmdSets can be merged and made to perform various set operations
    on each other.  CmdSets have priorities that affect which of their
    ingoing commands gets used.

    In the examples, cmdset A always have higher priority than cmdset B.

    key - the name of the cmdset. This can be used on its own for game
    operations

    mergetype (partly from Set theory):

        Union -    The two command sets are merged so that as many
                    commands as possible of each cmdset ends up in the
                    merged cmdset. Same-name commands are merged by
                    priority.  This is the most common default.
                    Ex: A1,A3 + B1,B2,B4,B5 = A1,B2,A3,B4,B5
        Intersect - Only commands found in *both* cmdsets
                    (i.e. which have same names) end up in the merged
                    cmdset, with the higher-priority cmdset replacing the
                    lower one.  Ex: A1,A3 + B1,B2,B4,B5 = A1
        Replace -   The commands of this cmdset completely replaces
                    the lower-priority cmdset's commands, regardless
                    of if same-name commands exist.
                    Ex: A1,A3 + B1,B2,B4,B5 = A1,A3
        Remove -    This removes the relevant commands from the
                    lower-priority cmdset completely.  They are not
                    replaced with anything, so this in effects uses the
                    high-priority cmdset as a filter to affect the
                    low-priority cmdset.
                    Ex: A1,A3 + B1,B2,B4,B5 = B2,B4,B5

                 Note: Commands longer than 2 characters and starting
                       with double underscrores, like '__noinput_command'
                       are considered 'system commands' and are
                       excempt from all merge operations - they are
                       ALWAYS included across mergers and only affected
                       if same-named system commands replace them.

    priority- All cmdsets are always merged in pairs of two so that
              the higher set's mergetype is applied to the
              lower-priority cmdset. Default commands have priority 0,
              high-priority ones like Exits and Channels have 10 and 9.
              Priorities can be negative as well to give default
              commands preference.

    duplicates - determines what happens when two sets of equal
                 priority merge (only). Defaults to None and has the first of them in the
                 merger (i.e. A above) automatically taking
                 precedence. But if `duplicates` is true, the
                 result will be a merger with more than one of each
                 name match.  This will usually lead to the account
                 receiving a multiple-match error higher up the road,
                 but can be good for things like cmdsets on non-account
                 objects in a room, to allow the system to warn that
                 more than one 'ball' in the room has the same 'kick'
                 command defined on it, so it may offer a chance to
                 select which ball to kick ...  Allowing duplicates
                 only makes sense for Union and Intersect, the setting
                 is ignored for the other mergetypes.
                 Note that the `duplicates` flag is *not* propagated in
                 a cmdset merger. So `A + B = C` will result in
                 a cmdset with duplicate commands, but C.duplicates will
                 be `None`. For duplication to apply to a whole cmdset
                 stack merge, _all_ cmdsets in the stack must have
                 `.duplicates=True` set.
                Finally, if a final cmdset has `.duplicates=None` (the normal
                unless created alone with another value), the cmdhandler
                will assume True for object-based cmdsets and False for
                all other. This is usually the most intuitive outcome.

    key_mergetype (dict) - allows the cmdset to define a unique
             mergetype for particular cmdsets.  Format is
             {CmdSetkeystring:mergetype}. Priorities still apply.
             Example: {'Myevilcmdset','Replace'} which would make
             sure for this set to always use 'Replace' on
             Myevilcmdset no matter what overall mergetype this set
             has.

    no_objs  - don't include any commands from nearby objects
                  when searching for suitable commands
    no_exits  - ignore the names of exits when matching against
                        commands
    no_channels   - ignore the name of channels when matching against
                        commands (WARNING- this is dangerous since the
                        account can then not even ask staff for help if
                        something goes wrong)

    """

    key = "Unnamed CmdSet"
    mergetype = "Union"
    priority = 0

    # These flags, if set to None should be interpreted as 'I don't care' and,
    # will allow "pass-through" even of lower-prio cmdsets' explicitly True/False
    # options. If this is set to True/False however, priority matters.
    no_exits = None
    no_objs = None
    no_channels = None
    # The .duplicates setting does not propagate and since duplicates can only happen
    # on same-prio cmdsets, there is no concept of passthrough on `None`.
    # The merger of two cmdsets always return in a cmdset with `duplicates=None`
    # (even if the result may have duplicated commands).
    # If a final cmdset has `duplicates=None` (normal, unless the cmdset is
    # created on its own with the flag set), the cmdhandler will auto-assume it to be
    # True for Object-based cmdsets and stay None/False for all other entities.
    #
    # Example:
    #  A and C has .duplicates=True, B has .duplicates=None (or False)
    #  B + A = BA, where BA will have duplicate cmds, but BA.duplicates = None
    #  BA + C = BAC, where BAC will have more duplication, but BAC.duplicates = None
    #
    # Basically, for the `.duplicate` setting to survive throughout a
    # merge-stack, every cmdset in the stack must have `duplicates` set explicitly.
    duplicates = None

    persistent = False
    key_mergetypes = {}
    errmessage = ""
    # pre-store properties to duplicate straight off
    to_duplicate = (
        "key",
        "cmdsetobj",
        "no_exits",
        "no_objs",
        "no_channels",
        "persistent",
        "mergetype",
        "priority",
        "duplicates",
        "errmessage",
    )

    def __init__(self, cmdsetobj=None, key=None):
        """
        Creates a new CmdSet instance.

        Args:
            cmdsetobj (Session, Account, Object, optional): This is the database object
                to which this particular instance of cmdset is related. It
                is often a character but may also be a regular object, Account
                or Session.
            key (str, optional): The idenfier for this cmdset. This
                helps if wanting to selectively remov cmdsets.

        """

        if key:
            self.key = key
        self.commands = []
        self.system_commands = []
        self.actual_mergetype = self.mergetype
        self.cmdsetobj = cmdsetobj
        # this is set only on merged sets, in cmdhandler.py, in order to
        # track, list and debug mergers correctly.
        self.merged_from = []

        # initialize system
        self.at_cmdset_creation()
        self._contains_cache = WeakKeyDictionary()  # {}

    # Priority-sensitive merge operations for cmdsets

    def _union(self, cmdset_a, cmdset_b):
        """
        Merge two sets using union merger

        Args:
            cmdset_a (Cmdset): Cmdset given higher priority in the case of a tie.
            cmdset_b (Cmdset): Cmdset given lower priority in the case of a tie.

        Returns:
            cmdset_c (Cmdset): The result of A U B operation.

        Notes:
            Union, C = A U B,  means that C gets all elements from both A and B.

        """
        cmdset_c = cmdset_a._duplicate()
        # we make copies, not refs by use of [:]
        cmdset_c.commands = cmdset_a.commands[:]
        if cmdset_a.duplicates and cmdset_a.priority == cmdset_b.priority:
            cmdset_c.commands.extend(cmdset_b.commands)
        else:
            cmdset_c.commands.extend([cmd for cmd in cmdset_b if cmd not in cmdset_a])
        return cmdset_c

    def _intersect(self, cmdset_a, cmdset_b):
        """
        Merge two sets using intersection merger

        Args:

        Returns:
            cmdset_c (Cmdset): The result of A (intersect) B operation.

        Notes:
            Intersection, C = A (intersect) B, means that C only gets the
                parts of A and B that are the same (that is, the commands
                of each set having the same name. Only the one of these
                having the higher prio ends up in C).

        """
            for cmd in [cmd for cmd in cmdset_a if cmd in cmdset_b]:
                cmdset_c.add(cmd)
                cmdset_c.add(cmdset_b.get(cmd))
        else:
            cmdset_c.commands = [cmd for cmd in cmdset_a if cmd in cmdset_b]

    def _replace(self, cmdset_a, cmdset_b):
        """
        Replace the contents of one set with another

        Args:
            cmdset_a (Cmdset): Cmdset replacing
            cmdset_b (Cmdset): Cmdset to replace

        Returns:
            cmdset_c (Cmdset): This is indentical to cmdset_a.

        Notes:
            C = A, where B is ignored.

        """

    def _remove(self, cmdset_a, cmdset_b):
        """
        Filter a set by another.

        Args:
            cmdset_a (Cmdset): Cmdset acting as a removal filter.
            cmdset_b (Cmdset): Cmdset to filter

        Returns:
            cmdset_c (Cmdset): B, with all matching commands from A removed.

        Notes:
            C = B - A, where A is used to remove the commands of B.

        """

        cmdset_c.commands = [cmd for cmd in cmdset_b if cmd not in cmdset_a]

    def _instantiate(self, cmd):
        """
        checks so that object is an instantiated command and not, say
        a cmdclass. If it is, instantiate it.  Other types, like
        strings, are passed through.

        Args:
            cmd (any): Entity to analyze.

        Returns:
            result (any): An instantiated Command or the input unmodified.

        """
        if callable(cmd):
            return cmd()
        else:
            return cmd

    def _duplicate(self):
        """
        Returns a new cmdset with the same settings as this one (no
        actual commands are copied over)

        Returns:
            cmdset (Cmdset): A copy of the current cmdset.
        """
        cmdset = CmdSet()
        for key, val in ((key, getattr(self, key)) for key in self.to_duplicate):
            if val != getattr(cmdset, key):
                # only copy if different from default; avoid turning
                # class-vars into instance vars
                setattr(cmdset, key, val)
        cmdset.key_mergetypes = self.key_mergetypes.copy()
        return cmdset

        """
        Show all commands in cmdset when printing it.

        Returns:
            commands (str): Representation of commands in Cmdset.

        """
        perm = "perm" if self.persistent else "non-perm"
        options = ", ".join(
            [
                "{}:{}".format(opt, "T" if getattr(self, opt) else "F")
                for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
                if getattr(self, opt) is not None
            ]
        )
        options = (", " + options) if options else ""
        return (
            f"<CmdSet {self.key}, {self.mergetype}, {perm}, prio {self.priority}{options}>: "
            + ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)])
        )

        """
        Allows for things like 'for cmd in cmdset':

        Returns:
            iterable (iter): Commands in Cmdset.

        """
        return iter(self.commands)

    def __contains__(self, othercmd):
        """
        Returns True if this cmdset contains the given command (as
        defined by command name and aliases). This allows for things
        like 'if cmd in cmdset'

        """
        ret = self._contains_cache.get(othercmd)
        if ret is None:
            ret = othercmd in self.commands
            self._contains_cache[othercmd] = ret
        return ret

    def __add__(self, cmdset_a):
        """
        Merge this cmdset (B) with another cmdset (A) using the + operator,

        C = B + A

        Here, we (by convention) say that 'A is merged onto B to form
        C'.  The actual merge operation used in the 'addition' depends
        on which priorities A and B have. The one of the two with the
        highest priority will apply and give its properties to C. In
        the case of a tie, A  takes priority and replaces the
        same-named commands in B unless A has the 'duplicate' variable
        set (which means both sets' commands are kept).
        """

        # It's okay to merge with None
        if not cmdset_a:

        sys_commands_a = cmdset_a.get_system_cmds()
        sys_commands_b = self.get_system_cmds()

        if self.priority <= cmdset_a.priority:
            # A higher or equal priority to B

            # preserve system __commands
            sys_commands = sys_commands_a + [
                cmd for cmd in sys_commands_b if cmd not in sys_commands_a
            ]

            mergetype = cmdset_a.key_mergetypes.get(self.key, cmdset_a.mergetype)
            if mergetype == "Intersect":
                cmdset_c = self._intersect(cmdset_a, self)
            elif mergetype == "Replace":
                cmdset_c = self._replace(cmdset_a, self)
            elif mergetype == "Remove":
                cmdset_c = self._remove(cmdset_a, self)
            else:  # Union
                cmdset_c = self._union(cmdset_a, self)

            # pass through options whenever they are set, unless the merging or higher-prio
            # set changes the setting (i.e. has a non-None value). We don't pass through
            # the duplicates setting; that is per-merge; the resulting .duplicates value
            # is always None (so merging cmdsets must all have explicit values if wanting
            # to cause duplicates).
            cmdset_c.no_channels = (
                self.no_channels if cmdset_a.no_channels is None else cmdset_a.no_channels
            )
            cmdset_c.no_exits = self.no_exits if cmdset_a.no_exits is None else cmdset_a.no_exits
            cmdset_c.no_objs = self.no_objs if cmdset_a.no_objs is None else cmdset_a.no_objs
            cmdset_c.duplicates = None

        else:
            # B higher priority than A

            # preserver system __commands
            sys_commands = sys_commands_b + [
                cmd for cmd in sys_commands_a if cmd not in sys_commands_b
            ]

            mergetype = self.key_mergetypes.get(cmdset_a.key, self.mergetype)
                cmdset_c = self._intersect(self, cmdset_a)
                cmdset_c = self._replace(self, cmdset_a)
                cmdset_c = self._remove(self, cmdset_a)
                cmdset_c = self._union(self, cmdset_a)

            # pass through options whenever they are set, unless the higher-prio
            # the duplicates setting; that is per-merge; the resulting .duplicates value#
                cmdset_a.no_channels if self.no_channels is None else self.no_channels
            )
            cmdset_c.no_exits = cmdset_a.no_exits if self.no_exits is None else self.no_exits
            cmdset_c.no_objs = cmdset_a.no_objs if self.no_objs is None else self.no_objs

        # we store actual_mergetype since key_mergetypes
        # might be different from the main mergetype.
        # This is used for diagnosis.
        cmdset_c.actual_mergetype = mergetype

        # print "__add__ for %s (prio %i)  called with %s (prio %i)." % (self.key, self.priority,
        # cmdset_a.key, cmdset_a.priority)

        # return the system commands to the cmdset
        cmdset_c.add(sys_commands, allow_duplicates=True)

    def add(self, cmd, allow_duplicates=False):
        """
        Add a new command or commands to this CmdSet, a list of
        commands or a cmdset to this cmdset. Note that this is *not*
        a merge operation (that is handled by the + operator).

        Args:
            cmd (Command, list, Cmdset): This allows for adding one or
                more commands to this Cmdset in one go. If another Cmdset
                is given, all its commands will be added.
            allow_duplicates (bool, optional): If set, will not try to remove
                duplicate cmds in the set. This is needed during the merge process
                to avoid wiping commands coming from cmdsets with duplicate=True.

        Notes:
            If cmd already exists in set, it will replace the old one
            (no priority checking etc happens here). This is very useful
            when overloading default commands).

            If cmd is another cmdset class or -instance, the commands of
            that command set is added to this one, as if they were part of
            the original cmdset definition. No merging or priority checks
            are made, rather later added commands will simply replace
            existing ones to make a unique set.

        """
        if inherits_from(cmd, "evennia.commands.cmdset.CmdSet"):
            # cmd is a command set so merge all commands in that set
            # to this one. We raise a visible error if we created
            # an infinite loop (adding cmdset to itself somehow)
            cmdset = cmd
            try:
                cmdset = self._instantiate(cmdset)
                err = (
                    "Adding cmdset {cmdset} to {cls} lead to an "
                    "infinite loop. When adding a cmdset to another, "
                    "make sure they are not themself cyclically added to "
                    "the new cmdset somewhere in the chain."
                )
                raise RuntimeError(_(err.format(cmdset=cmdset, cls=self.__class__)))
            cmds = cmdset.commands
        elif is_iter(cmd):
            cmds = [self._instantiate(c) for c in cmd]
        else:
            cmds = [self._instantiate(cmd)]

        commands = self.commands
        system_commands = self.system_commands

        for cmd in cmds:
            # Ensure commands know their source cmdset.
            cmd.cmdset_source = self
            # add all commands
            if not hasattr(cmd, "obj") or cmd.obj is None:
                cmd.obj = self.cmdsetobj

            # remove duplicates and add new
            for _dum in range(commands.count(cmd)):
                commands.remove(cmd)
            commands.append(cmd)

            # add system_command to separate list as well,
            # for quick look-up. These have no
            if cmd.key.startswith("__"):
                # remove same-matches and add new
                for _dum in range(system_commands.count(cmd)):
                    system_commands.remove(cmd)
                system_commands.append(cmd)

        if not allow_duplicates:
            # extra run to make sure to avoid doublets
            commands = list(set(commands))
        self.commands = commands

    def remove(self, cmd):
        """
        Remove a command instance from the cmdset.

        Args:
            cmd (Command or str): Either the Command object to remove
                or the key of such a command.

        """
        if isinstance(cmd, str):
            _cmd = next((_cmd for _cmd in self.commands if _cmd.key == cmd), None)
            if _cmd is None:
                if not cmd.startswith("__"):
                    # if a syscommand, keep the original string and instantiate on it
            else:
                cmd = _cmd

        cmd = self._instantiate(cmd)
        if cmd.key.startswith("__"):
            try:
                ic = self.system_commands.index(cmd)
                del self.system_commands[ic]
            except ValueError:
                # ignore error
                pass
        else:
            self.commands = [oldcmd for oldcmd in self.commands if oldcmd != cmd]

    def get(self, cmd):
        """
        Get a command from the cmdset. This is mostly useful to
        check if the command is part of this cmdset or not.

        Args:
            cmd (Command or str): Either the Command object or its key.

        Returns:
            cmd (Command): The first matching Command in the set.

        """
            else:
                cmd = _cmd

        for thiscmd in self.commands:
            if thiscmd == cmd:
                return thiscmd

        """
        Number of commands in set.

        Returns:
            N (int): Number of commands in this Cmdset.

        """
        return len(self.commands)

    def get_system_cmds(self):
        """
        Get system commands in cmdset

        Returns:
            sys_cmds (list): The system commands in the set.

        Notes:
            As far as the Cmdset is concerned, system commands are any
            commands with a key starting with double underscore __.
            These are excempt from merge operations.

        """
        return self.system_commands

    def make_unique(self, caller):
        """
        Remove duplicate command-keys (unsafe)

        Args:
            caller (object): Commands on this object will
                get preference in the duplicate removal.

        Notes:
            This is an unsafe command meant to clean out a cmdset of
            doublet commands after it has been created. It is useful
            for commands inheriting cmdsets from the cmdhandler where
            obj-based cmdsets always are added double. Doublets will
            be weeded out with preference to commands defined on
            caller, otherwise just by first-come-first-served.

        """
        unique = {}
        for cmd in self.commands:
            if cmd.key in unique:
                ocmd = unique[cmd.key]
                if (hasattr(cmd, "obj") and cmd.obj == caller) and not (
                    hasattr(ocmd, "obj") and ocmd.obj == caller
                ):
                    unique[cmd.key] = cmd
            else:
                unique[cmd.key] = cmd
        self.commands = list(unique.values())

    def get_all_cmd_keys_and_aliases(self, caller=None):
        """
        Collects keys/aliases from commands

        Args:
            caller (Object, optional): If set, this is used to check access permissions
                on each command. Only commands that pass are returned.

        Returns:
            names (list): A list of all command keys and aliases in this cmdset. If `caller`
                was given, this list will only contain commands to which `caller` passed
                the `call` locktype check.

        """
        names = []
        if caller:
            [names.extend(cmd._keyaliases) for cmd in self.commands if cmd.access(caller)]
        else:
            [names.extend(cmd._keyaliases) for cmd in self.commands]
        return names

    def at_cmdset_creation(self):
        """
        Hook method - this should be overloaded in the inheriting
        class, and should take care of populating the cmdset by use of
        self.add().

        """
        pass

"""
CmdSethandler

The Cmdsethandler tracks an object's 'Current CmdSet', which is the
current merged sum of all CmdSets added to it.

A CmdSet constitues a set of commands. The CmdSet works as a special
intelligent container that, when added to other CmdSet make sure that
same-name commands are treated correctly (usually so there are no
doublets).  This temporary but up-to-date merger of CmdSet is jointly
called the Current Cmset. It is this Current CmdSet that the
commandhandler looks through whenever an account enters a command (it
also adds CmdSets from objects in the room in real-time). All account
objects have a 'default cmdset' containing all the normal in-game mud
commands (look etc).

So what is all this cmdset complexity good for?

In its simplest form, a CmdSet has no commands, only a key name. In
this case the cmdset's use is up to each individual game - it can be
used by an AI module for example (mobs in cmdset 'roam' move from room
to room, in cmdset 'attack' they enter combat with accounts).

Defining commands in cmdsets offer some further powerful game-design
consequences however. Here are some examples:

As mentioned above, all accounts always have at least the Default
CmdSet.  This contains the set of all normal-use commands in-game,
stuff like look and @desc etc. Now assume our players end up in a dark
room. You don't want the player to be able to do much in that dark
room unless they light a candle. You could handle this by changing all
your normal commands to check if the player is in a dark room. This
rapidly goes unwieldly and error prone. Instead you just define a
cmdset with only those commands you want to be available in the 'dark'
cmdset - maybe a modified look command and a 'light candle' command -
and have this completely replace the default cmdset.

Another example: Say you want your players to be able to go
fishing. You could implement this as a 'fish' command that fails
whenever the account has no fishing rod. Easy enough.  But what if you
want to make fishing more complex - maybe you want four-five different
commands for throwing your line, reeling in, etc? Most players won't
(we assume) have fishing gear, and having all those detailed commands
is cluttering up the command list. And what if you want to use the
'throw' command also for throwing rocks etc instead of 'using it up'
for a minor thing like fishing?

So instead you put all those detailed fishing commands into their own
CommandSet called 'Fishing'. Whenever the player gives the command
'fish' (presumably the code checks there is also water nearby), only
THEN this CommandSet is added to the Cmdhandler of the account. The
'throw' command (which normally throws rocks) is replaced by the
custom 'fishing variant' of throw. What has happened is that the
Fishing CommandSet was merged on top of the Default ones, and due to
how we defined it, its command overrules the default ones.

When we are tired of fishing, we give the 'go home' command (or
whatever) and the Cmdhandler simply removes the fishing CommandSet
so that we are back at defaults (and can throw rocks again).

Since any number of CommandSets can be piled on top of each other, you
can then implement separate sets for different situations. For
example, you can have a 'On a boat' set, onto which you then tack on
the 'Fishing' set. Fishing from a boat? No problem!

"""
import sys
from importlib import import_module
from inspect import trace

from evennia.commands.cmdset import CmdSet

__all__ = ("import_cmdset", "CmdSetHandler")

_CACHED_CMDSETS = {}
_CMDSET_PATHS = utils.make_iter(settings.CMDSET_PATHS)
_CMDSET_FALLBACKS = settings.CMDSET_FALLBACKS

# Output strings

_ERROR_CMDSET_IMPORT = _(
    """{traceback}
Error loading cmdset '{path}'
(Traceback was logged {timestamp})"""
)

_ERROR_CMDSET_KEYERROR = _(
    """Error loading cmdset: No cmdset class '{classname}' in '{path}'.
)

_ERROR_CMDSET_SYNTAXERROR = _(
SyntaxError encountered when loading cmdset '{path}'.
)

_ERROR_CMDSET_EXCEPTION = _(
Compile/Run error when loading cmdset '{path}'.
)

_ERROR_CMDSET_FALLBACK = _(
    """
Error encountered for cmdset at path '{path}'.
Replacing with fallback '{fallback_path}'.
"""
)

_ERROR_CMDSET_NO_FALLBACK = _("""Fallback path '{fallback_path}' failed to generate a cmdset.""")

class _ErrorCmdSet(CmdSet):
    """
    This is a special cmdset used to report errors.
    """

    key = "_CMDSET_ERROR"
    errmessage = "Error when loading cmdset."

class _EmptyCmdSet(CmdSet):
    """
    This cmdset represents an empty cmdset
    """

    key = "_EMPTY_CMDSET"
    priority = -101

def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
    """
    This helper function is used by the cmdsethandler to load a cmdset
    instance from a python module, given a python_path. It's usually accessed
    through the cmdsethandler's add() and add_default() methods.
    path - This is the full path to the cmdset object on python dot-form

    Args:
        path (str): The path to the command set to load.
        cmdsetobj (CmdSet): The database object/typeclass on which this cmdset is to be
            assigned (this can be also channels and exits, as well as accounts
            but there will always be such an object)
        emit_to_obj (Object, optional): If given, error is emitted to
            this object (in addition to logging)
        no_logging (bool, optional): Don't log/send error messages.
            This can be useful if import_cmdset is just used to check if
            this is a valid python path or not.
    Returns:
        cmdset (CmdSet): The imported command set. If an error was
            encountered, `commands.cmdsethandler._ErrorCmdSet` is returned
            for the benefit of the handler.

    """
    python_paths = [path] + [
        "%s.%s" % (prefix, path) for prefix in _CMDSET_PATHS if not path.startswith(prefix)
    ]
    errstring = ""
    for python_path in python_paths:
        if "." in path:
            modpath, classname = python_path.rsplit(".", 1)
        else:
            raise ImportError(f"The path '{path}' is not on the form modulepath.ClassName")

        try:
            # first try to get from cache
            cmdsetclass = _CACHED_CMDSETS.get(python_path, None)

            if not cmdsetclass:
                try:
                    module = import_module(modpath, package="evennia")
                except ImportError as exc:
                    if len(trace()) > 2:
                        # error in module, make sure to not hide it.
                        dum, dum, tb = sys.exc_info()
                        raise exc.with_traceback(tb)
                    else:
                        # try next suggested path
                        errstring += _("\n(Unsuccessfully tried '{path}').").format(
                            path=python_path
                        )
                        continue
                try:
                    cmdsetclass = getattr(module, classname)
                except AttributeError as exc:
                        # Attribute error within module, don't hide it
                    else:
                        )
                        continue
                _CACHED_CMDSETS[python_path] = cmdsetclass

            # instantiate the cmdset (and catch its errors)
            if callable(cmdsetclass):
                cmdsetclass = cmdsetclass(cmdsetobj)
            return cmdsetclass
        except ImportError as err:
            errstring += _ERROR_CMDSET_IMPORT
            if _IN_GAME_ERRORS:
                errstring = errstring.format(
                    path=python_path, traceback=format_exc(), timestamp=logger.timeformat()
                )
            else:
                    path=python_path, traceback=err, timestamp=logger.timeformat()
                )
            break
            errstring += _ERROR_CMDSET_KEYERROR
            errstring = errstring.format(
                classname=classname, path=python_path, timestamp=logger.timeformat()
            )
            break
        except SyntaxError as err:
            errstring += _ERROR_CMDSET_SYNTAXERROR
                )
            else:
                )
            break
        except Exception as err:
            errstring += _ERROR_CMDSET_EXCEPTION
                )
            else:
                )
            break

    if errstring:
        # returning an empty error cmdset
        errstring = errstring.strip()
        if not no_logging:
            logger.log_err(errstring)
            if emit_to_obj and not ServerConfig.objects.conf("server_starting_mode"):
                emit_to_obj.msg(errstring)
        err_cmdset = _ErrorCmdSet()
        err_cmdset.errmessage = errstring
        return err_cmdset
    return None  # undefined error

# classes

class CmdSetHandler(object):
    """
    The CmdSetHandler is always stored on an object, this object is supplied
    as an argument.

    The 'current' cmdset is the merged set currently active for this object.
    This is the set the game engine will retrieve when determining which
    commands are available to the object. The cmdset_stack holds a history of
    all CmdSets to allow the handler to remove/add cmdsets at will. Doing so
    will re-calculate the 'current' cmdset.
    """

    def __init__(self, obj, init_true=True):
        """
        This method is called whenever an object is recreated.

        Args:
            obj (Object): An reference to the game object this handler
                belongs to.
            init_true (bool, optional): Set when the handler is initializing
                and loads the current cmdset.

        """
        self.obj = obj

        # the id of the "merged" current cmdset for easy access.
        self.key = None
        # this holds the "merged" current command set. Note that while the .update
        # method updates this field in order to have it synced when operating on
        # cmdsets in-code, when the game runs, this field is kept up-to-date by
        # the cmdsethandler's get_and_merge_cmdsets!
        self.current = None
        # this holds a history of CommandSets
        self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)]
        # this tracks which mergetypes are actually in play in the stack
        self.mergetype_stack = ["Union"]

        # the subset of the cmdset_paths that are to be stored in the database
        self.persistent_paths = [""]

        if init_true:
            self.update(init_mode=True)  # is then called from the object __init__.

        """
        Display current commands

        """

        strings = ["<CmdSetHandler> stack:"]
        mergelist = []
        if len(self.cmdset_stack) > 1:
            # We have more than one cmdset in stack; list them all
            for snum, cmdset in enumerate(self.cmdset_stack):
                mergelist.append(str(snum + 1))
                strings.append(f" {snum + 1}: {cmdset}")

        # Display the currently active cmdset, limited by self.obj's permissions
        mergetype = self.mergetype_stack[-1]
        if mergetype != self.current.mergetype:
            merged_on = self.cmdset_stack[-2].key
            mergetype = _("custom {mergetype} on cmdset '{cmdset}'")
            mergetype = mergetype.format(mergetype=mergetype, cmdset=merged_on)

        if mergelist:
            # current is a result of mergers
            mergelist = "+".join(mergelist)
            strings.append(f" <Merged {mergelist}>: {self.current}")
        else:
            # current is a single cmdset
            strings.append(" " + str(self.current))
        return "\n".join(strings).rstrip()

    def _import_cmdset(self, cmdset_path, emit_to_obj=None):
        """
        Method wrapper for import_cmdset; Loads a cmdset from a
        module.

        Args:
            cmdset_path (str): The python path to an cmdset object.
            emit_to_obj (Object): The object to send error messages to

        Returns:
            cmdset (Cmdset): The imported cmdset.

        """
        if not emit_to_obj:
            emit_to_obj = self.obj
        return import_cmdset(cmdset_path, self.obj, emit_to_obj)

    def update(self, init_mode=False):
        """
        Re-adds all sets in the handler to have an updated current

        Args:
            init_mode (bool, optional): Used automatically right after
                this handler was created; it imports all persistent cmdsets
                from the database.

        Notes:
            This method is necessary in order to always have a `.current`
            cmdset when working with the cmdsethandler in code. But the
            CmdSetHandler doesn't (cannot) consider external cmdsets and game
            state. This means that the .current calculated from this method
            will likely not match the true current cmdset as determined at
            run-time by `cmdhandler.get_and_merge_cmdsets()`. So in a running
            game the responsibility of keeping `.current` upt-to-date belongs
            to the central `cmdhandler.get_and_merge_cmdsets()`!

        """
        if init_mode:
            # reimport all persistent cmdsets
            storage = self.obj.cmdset_storage
            if storage:
                self.cmdset_stack = []
                for pos, path in enumerate(storage):
                    if pos == 0 and not path:
                        self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)]
                    elif path:
                        cmdset = self._import_cmdset(path)
                        if cmdset:
                            if cmdset.key == "_CMDSET_ERROR":
                                # If a cmdset fails to load, check if we have a fallback path to use
                                fallback_path = _CMDSET_FALLBACKS.get(path, None)
                                if fallback_path:
                                    err = _ERROR_CMDSET_FALLBACK.format(
                                        path=path, fallback_path=fallback_path
                                    )
                                    logger.log_err(err)
                                    if _IN_GAME_ERRORS:
                                        self.obj.msg(err)
                                    cmdset = self._import_cmdset(fallback_path)
                                # If no cmdset is returned from the fallback, we can't go further
                                if not cmdset:
                                    err = _ERROR_CMDSET_NO_FALLBACK.format(
                                        fallback_path=fallback_path
                                    )
                                    continue
                            cmdset.persistent = cmdset.key != "_CMDSET_ERROR"
                            self.cmdset_stack.append(cmdset)

        # merge the stack into a new merged cmdset
        new_current = None
        self.mergetype_stack = []
        for cmdset in self.cmdset_stack:
            try:
                # for cmdset's '+' operator, order matters.
                new_current = cmdset + new_current
            except TypeError:
                continue
            self.mergetype_stack.append(new_current.actual_mergetype)
        self.current = new_current

    def add(self, cmdset, emit_to_obj=None, persistent=False, default_cmdset=False, **kwargs):
        """
        Add a cmdset to the handler, on top of the old ones, unless it
        is set as the default one (it will then end up at the bottom of the stack)

        Args:
            cmdset (CmdSet or str): Can be a cmdset object or the python path
                to such an object.
            emit_to_obj (Object, optional): An object to receive error messages.
            persistent (bool, optional): Let cmdset remain across server reload.
            default_cmdset (Cmdset, optional): Insert this to replace the
                default cmdset position (there is only one such position,
                always at the bottom of the stack).

        Notes:
          An interesting feature of this method is if you were to send
          it an *already instantiated cmdset* (i.e. not a class), the
          current cmdsethandler's obj attribute will then *not* be
          transferred over to this already instantiated set (this is
          because it might be used elsewhere and can cause strange
          effects).  This means you could in principle have the
          handler launch command sets tied to a *different* object
          than the handler. Not sure when this would be useful, but
          it's a 'quirk' that has to be documented.

        """
        if "permanent" in kwargs:
            logger.log_dep("obj.cmdset.add() kwarg 'permanent' has changed name to 'persistent'.")
            persistent = kwargs["permanent"] if persistent is False else persistent

        if not (isinstance(cmdset, str) or utils.inherits_from(cmdset, CmdSet)):
            string = _("Only CmdSets can be added to the cmdsethandler!")
            raise Exception(string)

        if callable(cmdset):
            cmdset = cmdset(self.obj)
        elif isinstance(cmdset, str):
            # this is (maybe) a python path. Try to import from cache.
            cmdset = self._import_cmdset(cmdset)
        if cmdset and cmdset.key != "_CMDSET_ERROR":
            cmdset.persistent = persistent
            if persistent and cmdset.key != "_CMDSET_ERROR":
                # store the path permanently
                storage = self.obj.cmdset_storage or [""]
                if default_cmdset:
                    storage[0] = cmdset.path
                else:
                    storage.append(cmdset.path)
                self.obj.cmdset_storage = storage
            if default_cmdset:
                self.cmdset_stack[0] = cmdset
            else:
                self.cmdset_stack.append(cmdset)
            self.update()

    def add_default(self, cmdset, emit_to_obj=None, persistent=True, **kwargs):
        """
        Shortcut for adding a default cmdset.

        Args:
            cmdset (Cmdset): The Cmdset to add.
            emit_to_obj (Object, optional): Gets error messages
            persistent (bool, optional): The new Cmdset should survive a server reboot.

        """
            logger.log_dep(
                "obj.cmdset.add_default() kwarg 'permanent' has changed name to 'persistent'."
            )
            persistent = kwargs["permanent"] if persistent is None else persistent
        self.add(cmdset, emit_to_obj=emit_to_obj, persistent=persistent, default_cmdset=True)

    def remove(self, cmdset=None, default_cmdset=False):
        """
        Remove a cmdset from the  handler.

        Args:
            cmdset (CommandSet or str, optional): This can can be supplied either as a cmdset-key,
                an instance of the CmdSet or a python path to the cmdset.
                If no key is given, the last cmdset in the stack is
                removed. Whenever the cmdset_stack changes, the cmdset is
                updated. If default_cmdset is set, this argument is ignored.
            default_cmdset (bool, optional): If set, this will remove the
                default cmdset (at the bottom of the stack).

        """
        if default_cmdset:
            # remove the default cmdset only
            if self.cmdset_stack:
                cmdset = self.cmdset_stack[0]
                if cmdset.persistent:
                    storage = self.obj.cmdset_storage or [""]
                    storage[0] = ""
                    self.obj.cmdset_storage = storage
                self.cmdset_stack[0] = _EmptyCmdSet(cmdsetobj=self.obj)
            else:
                self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)]
            return

        if len(self.cmdset_stack) < 2:
            # don't allow deleting default cmdsets here.
            return

        if not cmdset:
            # remove the last one in the stack
            cmdset = self.cmdset_stack.pop()
            if cmdset.persistent:
                storage = self.obj.cmdset_storage
                storage.pop()
        else:
            # try it as a callable
            if callable(cmdset) and hasattr(cmdset, "path"):
                delcmdsets = [cset for cset in self.cmdset_stack[1:] if cset.path == cmdset.path]
            else:
                # try it as a path or key
                delcmdsets = [
                    cset
                    for cset in self.cmdset_stack[1:]
                    if cset.path == cmdset or cset.key == cmdset
                ]
            storage = []

            if any(cset.persistent for cset in delcmdsets):
                # only hit database if there's need to
                updated = False
                for cset in delcmdsets:
                    if cset.persistent:
                        try:
                            storage.remove(cset.path)
                            updated = True
                        except ValueError:
                            # nothing to remove
                            pass
                if updated:
            for cset in delcmdsets:
                # clean the in-memory stack
                try:
                    self.cmdset_stack.remove(cset)
                except ValueError:
                    # nothing to remove
                    pass
        # re-sync the cmdsethandler.
        self.update()

    # legacy alias
    delete = remove

    def remove_default(self):
        """
        This explicitly deletes only the default cmdset.

        """
        self.remove(default_cmdset=True)

    delete_default = remove_default

    def get(self):
        """
        Get all cmdsets.

        Returns:
            cmdsets (list): All the command sets currently in the handler.

        """
        return self.cmdset_stack

    # backwards-compatible alias
    all = get

    def clear(self):
        """
        Removes all Command Sets from the handler except the default one
        (use `self.remove_default` to remove that).

        """
        self.cmdset_stack = [self.cmdset_stack[0]]
        storage = self.obj.cmdset_storage
        if storage:
            storage = storage[0]
            self.obj.cmdset_storage = storage

    def has(self, cmdset, must_be_default=False):
        """
        checks so the cmdsethandler contains a given cmdset

        Args:
            cmdset (str or Cmdset): Cmdset key, pythonpath or
                Cmdset to check the existence for.
            must_be_default (bool, optional): Only return True if
                the checked cmdset is the default one.

        Returns:
            has_cmdset (bool): Whether or not the cmdset is in the handler.

        """
        if callable(cmdset) and hasattr(cmdset, "path"):
            if must_be_default:
                return self.cmdset_stack and (self.cmdset_stack[0].path == cmdset.path)
            else:
                return any([cset for cset in self.cmdset_stack if cset.path == cmdset.path])
        else:
            # try it as a path or key
                return self.cmdset_stack and (
                    self.cmdset_stack[0].key == cmdset or self.cmdset_stack[0].path == cmdset
                )
            else:
                return any(
                    [
                        cset
                        for cset in self.cmdset_stack
                        if cset.path == cmdset or cset.key == cmdset
                    ]
                )

    # backwards-compatability alias
    has_cmdset = has

    def reset(self):
        """
        Force reload of all cmdsets in handler. This should be called
        after _CACHED_CMDSETS have been cleared (normally this is
        handled automatically by @reload).

        """
        new_cmdset_stack = []
            if cmdset.key == "_EMPTY_CMDSET":
                new_cmdset_stack.append(cmdset)
            else:
                new_cmdset_stack.append(self._import_cmdset(cmdset.path))
        self.cmdset_stack = new_cmdset_stack

#
# Secret Labs' Regular Expression Engine
#
# re-compatible interface for the sre matching engine
#
# Copyright (c) 1998-2001 by Secret Labs AB.  All rights reserved.
#
# This version of the SRE library can be redistributed under CNRI's
# Python 1.6 license.  For any other use, please contact Secret Labs
# AB (info@pythonware.com).
#
# Portions of this engine have been developed in cooperation with
# CNRI.  Hewlett-Packard provided funding for 1.6 integration and
# other compatibility work.
#

r"""Support for regular expressions (RE).

This module provides regular expression matching operations similar to
those found in Perl.  It supports both 8-bit and Unicode strings; both
the pattern and the strings being processed can contain null bytes and
characters outside the US ASCII range.

Regular expressions can contain both special and ordinary characters.
Most ordinary characters, like "A", "a", or "0", are the simplest
regular expressions; they simply match themselves.  You can
concatenate ordinary characters, so last matches the string 'last'.

The special characters are:
    "."      Matches any character except a newline.
    "^"      Matches the start of the string.
    "$"      Matches the end of the string or just before the newline at
             the end of the string.
    "*"      Matches 0 or more (greedy) repetitions of the preceding RE.
             Greedy means that it will match as many repetitions as possible.
    "+"      Matches 1 or more (greedy) repetitions of the preceding RE.
    "?"      Matches 0 or 1 (greedy) of the preceding RE.
    *?,+?,?? Non-greedy versions of the previous three special characters.
    {m,n}    Matches from m to n repetitions of the preceding RE.
    {m,n}?   Non-greedy version of the above.
    "\\"     Either escapes special characters or signals a special sequence.
    []       Indicates a set of characters.
             A "^" as the first character indicates a complementing set.
    "|"      A|B, creates an RE that will match either A or B.
    (...)    Matches the RE inside the parentheses.
             The contents can be retrieved or matched later in the string.
    (?aiLmsux) The letters set the corresponding flags defined below.
    (?:...)  Non-grouping version of regular parentheses.
    (?P<name>...) The substring matched by the group is accessible by name.
    (?P=name)     Matches the text matched earlier by the group named name.
    (?#...)  A comment; ignored.
    (?=...)  Matches if ... matches next, but doesn't consume the string.
    (?!...)  Matches if ... doesn't match next.
    (?<=...) Matches if preceded by ... (must be fixed length).
    (?<!...) Matches if not preceded by ... (must be fixed length).
    (?(id/name)yes|no) Matches yes pattern if the group with id/name matched,
                       the (optional) no pattern otherwise.

The special sequences consist of "\\" and a character from the list
below.  If the ordinary character is not on the list, then the
resulting RE will match the second character.
    \number  Matches the contents of the group of the same number.
    \A       Matches only at the start of the string.
    \Z       Matches only at the end of the string.
    \b       Matches the empty string, but only at the start or end of a word.
    \B       Matches the empty string, but not at the start or end of a word.
    \d       Matches any decimal digit; equivalent to the set [0-9] in
             bytes patterns or string patterns with the ASCII flag.
             In string patterns without the ASCII flag, it will match the whole
             range of Unicode digits.
    \D       Matches any non-digit character; equivalent to [^\d].
    \s       Matches any whitespace character; equivalent to [ \t\n\r\f\v] in
             range of Unicode whitespace characters.
    \S       Matches any non-whitespace character; equivalent to [^\s].
    \w       Matches any alphanumeric character; equivalent to [a-zA-Z0-9_]
             in bytes patterns or string patterns with the ASCII flag.
             In string patterns without the ASCII flag, it will match the
             range of Unicode alphanumeric characters (letters plus digits
             plus underscore).
             With LOCALE, it will match the set [0-9_] plus characters defined
             as letters for the current locale.
    \W       Matches the complement of \w.
    \\       Matches a literal backslash.

This module exports the following functions:
    match     Match a regular expression pattern to the beginning of a string.
    fullmatch Match a regular expression pattern to all of a string.
    search    Search a string for the presence of a pattern.
    sub       Substitute occurrences of a pattern found in a string.
    subn      Same as sub, but also return the number of substitutions made.
    split     Split a string by the occurrences of a pattern.
    findall   Find all occurrences of a pattern in a string.
    finditer  Return an iterator yielding a Match object for each match.
    compile   Compile a pattern into a Pattern object.
    purge     Clear the regular expression cache.
    escape    Backslash all non-alphanumerics in a string.

Each function other than purge and escape can take an optional 'flags' argument
consisting of one or more of the following module constants, joined by "|".
A, L, and U are mutually exclusive.
    A  ASCII       For string patterns, make \w, \W, \b, \B, \d, \D
                   match the corresponding ASCII character categories
                   (rather than the whole Unicode categories, which is the
                   default).
                   For bytes patterns, this flag is the only available
                   behaviour and needn't be specified.
    I  IGNORECASE  Perform case-insensitive matching.
    L  LOCALE      Make \w, \W, \b, \B, dependent on the current locale.
    M  MULTILINE   "^" matches the beginning of lines (after a newline)
                   as well as the string.
                   "$" matches the end of lines (before a newline) as well
                   as the end of the string.
    S  DOTALL      "." matches any character at all, including the newline.
    X  VERBOSE     Ignore whitespace and comments for nicer looking RE's.
    U  UNICODE     For compatibility only. Ignored for string patterns (it
                   is the default), and forbidden for bytes patterns.

This module also defines an exception 'error'.

"""

import enum
import sre_compile
import sre_parse
try:
    import _locale
    _locale = None

# public symbols
__all__ = [
    "match", "fullmatch", "search", "sub", "subn", "split",
    "findall", "finditer", "compile", "purge", "template", "escape",
    "error", "Pattern", "Match", "A", "I", "L", "M", "S", "X", "U",
    "ASCII", "IGNORECASE", "LOCALE", "MULTILINE", "DOTALL", "VERBOSE",
    "UNICODE",
]

__version__ = "2.2.1"

class RegexFlag(enum.IntFlag):
    ASCII = A = sre_compile.SRE_FLAG_ASCII # assume ascii "locale"
    IGNORECASE = I = sre_compile.SRE_FLAG_IGNORECASE # ignore case
    LOCALE = L = sre_compile.SRE_FLAG_LOCALE # assume current 8-bit locale
    UNICODE = U = sre_compile.SRE_FLAG_UNICODE # assume unicode "locale"
    MULTILINE = M = sre_compile.SRE_FLAG_MULTILINE # make anchors look for newline
    DOTALL = S = sre_compile.SRE_FLAG_DOTALL # make dot match newline
    VERBOSE = X = sre_compile.SRE_FLAG_VERBOSE # ignore whitespace and comments
    # sre extensions (experimental, don't rely on these)
    TEMPLATE = T = sre_compile.SRE_FLAG_TEMPLATE # disable backtracking
    DEBUG = sre_compile.SRE_FLAG_DEBUG # dump pattern after compilation

        if self._name_ is not None:
            return f're.{self._name_}'
        value = self._value_
        members = []
        negative = value < 0
        if negative:
            value = ~value
        for m in self.__class__:
            if value & m._value_:
                value &= ~m._value_
                members.append(f're.{m._name_}')
        if value:
            members.append(hex(value))
        res = '|'.join(members)
            if len(members) > 1:
                res = f'~({res})'
            else:
                res = f'~{res}'
        return res
    __str__ = object.__str__
globals().update(RegexFlag.__members__)

# sre exception
error = sre_compile.error

# --------------------------------------------------------------------
# public interface

def match(pattern, string, flags=0):
    """Try to apply the pattern at the start of the string, returning
    a Match object, or None if no match was found."""
    return _compile(pattern, flags).match(string)

def fullmatch(pattern, string, flags=0):
    """Try to apply the pattern to all of the string, returning
    return _compile(pattern, flags).fullmatch(string)

def search(pattern, string, flags=0):
    """Scan through string looking for a match to the pattern, returning
    return _compile(pattern, flags).search(string)

def sub(pattern, repl, string, count=0, flags=0):
    """Return the string obtained by replacing the leftmost
    non-overlapping occurrences of the pattern in string by the
    replacement repl.  repl can be either a string or a callable;
    if a string, backslash escapes in it are processed.  If it is
    a callable, it's passed the Match object and must return
    a replacement string to be used."""
    return _compile(pattern, flags).sub(repl, string, count)

def subn(pattern, repl, string, count=0, flags=0):
    """Return a 2-tuple containing (new_string, number).
    new_string is the string obtained by replacing the leftmost
    non-overlapping occurrences of the pattern in the source
    string by the replacement repl.  number is the number of
    substitutions that were made. repl can be either a string or a
    callable; if a string, backslash escapes in it are processed.
    If it is a callable, it's passed the Match object and must
    return a replacement string to be used."""
    return _compile(pattern, flags).subn(repl, string, count)

def split(pattern, string, maxsplit=0, flags=0):
    """Split the source string by the occurrences of the pattern,
    returning a list containing the resulting substrings.  If
    capturing parentheses are used in pattern, then the text of all
    groups in the pattern are also returned as part of the resulting
    list.  If maxsplit is nonzero, at most maxsplit splits occur,
    and the remainder of the string is returned as the final element
    of the list."""
    return _compile(pattern, flags).split(string, maxsplit)

def findall(pattern, string, flags=0):
    """Return a list of all non-overlapping matches in the string.

    If one or more capturing groups are present in the pattern, return
    a list of groups; this will be a list of tuples if the pattern
    has more than one group.

    Empty matches are included in the result."""
    return _compile(pattern, flags).findall(string)

def finditer(pattern, string, flags=0):
    """Return an iterator over all non-overlapping matches in the
    string.  For each match, the iterator returns a Match object.

    return _compile(pattern, flags).finditer(string)

def compile(pattern, flags=0):
    "Compile a regular expression pattern, returning a Pattern object."
    return _compile(pattern, flags)

def purge():
    "Clear the regular expression caches"
    _cache.clear()
    _compile_repl.cache_clear()

def template(pattern, flags=0):
    "Compile a template pattern, returning a Pattern object"
    return _compile(pattern, flags|T)

# SPECIAL_CHARS
# closing ')', '}' and ']'
# '-' (a range in character set)
# '&', '~', (extended character set operations)
# '#' (comment) and WHITESPACE (ignored) in verbose mode
_special_chars_map = {i: '\\' + chr(i) for i in b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f'}

def escape(pattern):
    """
    Escape special characters in a string.
    """
    if isinstance(pattern, str):
        return pattern.translate(_special_chars_map)
    else:
        pattern = str(pattern, 'latin1')
        return pattern.translate(_special_chars_map).encode('latin1')

Pattern = type(sre_compile.compile('', 0))
Match = type(sre_compile.compile('', 0).match(''))

# internals

_cache = {}  # ordered!

_MAXCACHE = 512
def _compile(pattern, flags):
    # internal: compile pattern
    if isinstance(flags, RegexFlag):
        flags = flags.value
    try:
        return _cache[type(pattern), pattern, flags]
    except KeyError:
        pass
    if isinstance(pattern, Pattern):
        if flags:
                "cannot process flags argument with a compiled pattern")
        return pattern
    if not sre_compile.isstring(pattern):
        raise TypeError("first argument must be string or compiled pattern")
    p = sre_compile.compile(pattern, flags)
    if not (flags & DEBUG):
        if len(_cache) >= _MAXCACHE:
            # Drop the oldest item
            try:
                del _cache[next(iter(_cache))]
            except (StopIteration, RuntimeError, KeyError):
                pass
        _cache[type(pattern), pattern, flags] = p
    return p

@functools.lru_cache(_MAXCACHE)
def _compile_repl(repl, pattern):
    # internal: compile replacement pattern
    return sre_parse.parse_template(repl, pattern)

def _expand(pattern, match, template):
    # internal: Match.expand implementation hook
    template = sre_parse.parse_template(template, pattern)
    return sre_parse.expand_template(template, match)

def _subx(pattern, template):
    # internal: Pattern.sub/subn implementation helper
    template = _compile_repl(template, pattern)
    if not template[0] and len(template[1]) == 1:
        # literal replacement
        return template[1][0]
    def filter(match, template=template):
        return sre_parse.expand_template(template, match)
    return filter

# register myself for pickling

import copyreg

def _pickle(p):
    return _compile, (p.pattern, p.flags)

copyreg.pickle(Pattern, _pickle, _compile)

# experimental stuff (see python-dev discussions for details)

class Scanner:
    def __init__(self, lexicon, flags=0):
        from sre_constants import BRANCH, SUBPATTERN
        if isinstance(flags, RegexFlag):
            flags = flags.value
        self.lexicon = lexicon
        # combine phrases into a compound pattern
        p = []
        s = sre_parse.State()
        s.flags = flags
        for phrase, action in lexicon:
            gid = s.opengroup()
            p.append(sre_parse.SubPattern(s, [
                (SUBPATTERN, (gid, 0, 0, sre_parse.parse(phrase, flags))),
                ]))
            s.closegroup(gid, p[-1])
        p = sre_parse.SubPattern(s, [(BRANCH, (None, p))])
        self.scanner = sre_compile.compile(p)
    def scan(self, string):
        result = []
        append = result.append
        match = self.scanner.scanner(string).match
        i = 0
        while True:
            m = match()
            if not m:
                break
            j = m.end()
            if i == j:
                break
            action = self.lexicon[m.lastindex-1][1]
            if callable(action):
                self.match = m
                action = action(self, m.group())
            if action is not None:
                append(action)
            i = j
        return result, string[i:]

"""
Account (OOC) commands. These are stored on the Account object
and self.caller is thus always an Account, not an Object/Character.

These commands go in the AccountCmdset and are accessible also
when puppeting a Character (although with lower priority)

These commands use the account_caller property which tells the command
parent (MuxCommand, usually) to setup caller correctly. They use
self.account to make sure to always use the account object rather than
self.caller (which change depending on the level you are calling from)
The property self.character can be used to access the character when
these commands are triggered with a connected character (such as the
case of the `ooc` command), it is None if we are OOC.

Note that under MULTISESSION_MODE > 2, Account commands should use
self.msg() and similar methods to reroute returns to the correct
method. Otherwise all text will be returned to all connected sessions.

"""
from codecs import lookup as codecs_lookup

from evennia.utils import create, logger, search, utils

COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)

# limit symbol import for API
__all__ = (
    "CmdOOCLook",
    "CmdIC",
    "CmdOOC",
    "CmdPassword",
    "CmdQuit",
    "CmdCharCreate",
    "CmdOption",
    "CmdSessions",
    "CmdWho",
    "CmdColorTest",
    "CmdQuell",
    "CmdCharDelete",
    "CmdStyle",
)

class MuxAccountLookCommand(COMMAND_DEFAULT_CLASS):
    """
    Custom parent (only) parsing for OOC looking, sets a "playable"
    property on the command based on the parsing.

    """

        """Custom parsing"""

        super().parse()

        playable = self.account.characters
        # store playable property
        if self.args:
            self.playable = dict((utils.to_str(char.key.lower()), char) for char in playable).get(
                self.args.lower(), None
            )
        else:
            self.playable = playable

# Obs - these are all intended to be stored on the Account, and as such,
# use self.account instead of self.caller, just to be sure. Also self.msg()
# is used to make sure returns go to the right session

# note that this is inheriting from MuxAccountLookCommand,
# and has the .playable property.
class CmdOOCLook(MuxAccountLookCommand):
    """
    look while out-of-character

    Usage:
      look

    Look in the ooc state.
    """

    # This is an OOC version of the look command. Since a
    # Account doesn't have an in-game existence, there is no
    # concept of location or "self". If we are controlling
    # a character, pass control over to normal look.

    key = "look"
    aliases = ["l", "ls"]
    locks = "cmd:all()"
    help_category = "General"

    # this is used by the parent
    account_caller = True

        """implement the ooc look command"""

        if self.session.puppet:
            # if we are puppeting, this is only reached in the case the that puppet
            # has no look command on its own.
            self.msg("You currently have no ability to look around.")
            return

        if _AUTO_PUPPET_ON_LOGIN and _MAX_NR_CHARACTERS == 1 and self.playable:
            # only one exists and is allowed - simplify
            self.msg("You are out-of-character (OOC).\nUse |wic|n to get back into the game.")
            return

        # call on-account look helper method
        self.msg(self.account.at_look(target=self.playable, session=self.session))

class CmdCharCreate(COMMAND_DEFAULT_CLASS):
    """
    create a new character

    Usage:
      charcreate <charname> [= desc]

    Create a new character, optionally giving it a description. You
    may use upper-case letters in the name - you will nevertheless
    always be able to access your character using lower-case letters
    if you want.
    """

    key = "charcreate"
    locks = "cmd:pperm(Player)"

        """create the new character"""
        account = self.account
        if not self.args:
            self.msg("Usage: charcreate <charname> [= description]")
            return
        key = self.lhs
        description = self.rhs or "This is a character."

        new_character, errors = self.account.create_character(
            key=key, description=description, ip=self.session.address
        )

        if errors:
            self.msg(errors)
        if not new_character:
            return

        self.msg(
            f"Created new character {new_character.key}. Use |wic {new_character.key}|n to enter"
            " the game as this character."
        )

class CmdCharDelete(COMMAND_DEFAULT_CLASS):
    """
    delete a character - this cannot be undone!

    Usage:
        chardelete <charname>

    Permanently deletes one of your characters.
    """

    key = "chardelete"

        """delete the character"""

            self.msg("Usage: chardelete <charactername>")
            return

        # use the playable_characters list to search
        match = [
            char
            for char in utils.make_iter(account.characters)
            if char.key.lower() == self.args.lower()
        ]
        if not match:
            self.msg("You have no such character to delete.")
            return
        elif len(match) > 1:
            self.msg(
                "Aborting - there are two characters with the same name. Ask an admin to delete the"
                " right one."
            )
            return
        else:  # one match
            from evennia.utils.evmenu import get_input

            def _callback(caller, callback_prompt, result):
                if result.lower() == "yes":
                    # only take action
                    delobj = caller.ndb._char_to_delete
                    key = delobj.key
                    caller.characters.remove(delobj)
                    delobj.delete()
                    self.msg(f"Character '{key}' was permanently deleted.")
                    logger.log_sec(
                        f"Character Deleted: {key} (Caller: {account}, IP: {self.session.address})."
                    )
                else:
                    self.msg("Deletion was aborted.")
                del caller.ndb._char_to_delete

            match = match[0]
            account.ndb._char_to_delete = match

            # Return if caller has no permission to delete this
            if not match.access(account, "delete"):
                self.msg("You do not have permission to delete this character.")
                return

            prompt = (
                "|rThis will permanently destroy '%s'. This cannot be undone.|n Continue yes/[no]?"
            )
            get_input(account, prompt % match.key, _callback)

class CmdIC(COMMAND_DEFAULT_CLASS):
    """
    control an object you have permission to puppet

    Usage:
      ic <character>

    Go in-character (IC) as a given Character.

    This will attempt to "become" a different object assuming you have
    the right to do so. Note that it's the ACCOUNT character that puppets
    characters/objects and which needs to have the correct permission!

    You cannot become an object that is already controlled by another
    account. In principle <character> can be any in-game object as long
    as you the account have access right to puppet it.
    """

    key = "ic"
    # lock must be all() for different puppeted objects to access it.
    aliases = "puppet"

        """
        Main puppet method
        """
        session = self.session

        new_character = None
        character_candidates = []

            character_candidates = [account.db._last_puppet] if account.db._last_puppet else []
            if not character_candidates:
                self.msg("Usage: ic <character>")
                return
        else:
            # argument given

            if playables := account.characters:
                # look at the playable_characters list first
                character_candidates.extend(
                    utils.make_iter(
                        account.search(
                            self.args,
                            candidates=playables,
                            search_object=True,
                            quiet=True,
                        )
                    )
                )

            if account.locks.check_lockstring(account, "perm(Builder)"):
                # builders and higher should be able to puppet more than their
                # playable characters.
                if session.puppet:
                    # start by local search - this helps to avoid the user
                    # getting locked into their playable characters should one
                    # happen to be named the same as another. We replace the suggestion
                    # from playable_characters here - this allows builders to puppet objects
                    # with the same name as their playable chars should it be necessary
                    # (by going to the same location).
                    character_candidates = [
                        char
                        for char in session.puppet.search(self.args, quiet=True)
                        if char.access(account, "puppet")
                    ]
                if not character_candidates:
                    # fall back to global search only if Builder+ has no
                    # playable_characters in list and is not standing in a room
                    # with a matching char.
                    character_candidates.extend(
                        [
                            char
                            for char in search.object_search(self.args)
                            if char.access(account, "puppet")
                        ]
                    )

        # handle possible candidates
        if not character_candidates:
            self.msg("That is not a valid character choice.")
            return
        if len(character_candidates) > 1:
            self.msg(
                "Multiple targets with the same name:\n %s"
                % ", ".join("%s(#%s)" % (obj.key, obj.id) for obj in character_candidates)
            )
            return
        else:
            new_character = character_candidates[0]

        # do the puppet puppet
        try:
            account.puppet_object(session, new_character)
            account.db._last_puppet = new_character
            logger.log_sec(
                f"Puppet Success: (Caller: {account}, Target: {new_character}, IP:"
                f" {self.session.address})."
            )
        except RuntimeError as exc:
            self.msg(f"|rYou cannot become |C{new_character.name}|n: {exc}")
                f"Puppet Failed: %s (Caller: {account}, Target: {new_character}, IP:"
            )

# and as such has the .playable property.
class CmdOOC(MuxAccountLookCommand):
    """
    stop puppeting and go ooc

    Usage:
      ooc

    Go out-of-character (OOC).

    This will leave your current character and put you in a incorporeal OOC state.
    """

    key = "ooc"
    aliases = "unpuppet"

        """Implement function"""

        old_char = account.get_puppet(session)
        if not old_char:
            string = "You are already OOC."
            self.msg(string)
            return

        account.db._last_puppet = old_char

        # disconnect
        try:
            account.unpuppet_object(session)
            self.msg("\n|GYou go OOC.|n\n")

            if _AUTO_PUPPET_ON_LOGIN and _MAX_NR_CHARACTERS == 1 and self.playable:
                # only one character exists and is allowed - simplify
                self.msg("You are out-of-character (OOC).\nUse |wic|n to get back into the game.")
                return

            self.msg(account.at_look(target=self.playable, session=session))

            self.msg(f"|rCould not unpuppet from |c{old_char}|n: {exc}")

class CmdSessions(COMMAND_DEFAULT_CLASS):
    """
    check your connected session(s)

    Usage:
      sessions

    Lists the sessions currently connected to your account.

    """

    key = "sessions"

        sessions = account.sessions.all()
        table = self.styled_table(
            "|wsessid", "|wprotocol", "|whost", "|wpuppet/character", "|wlocation"
        )
        for sess in sorted(sessions, key=lambda x: x.sessid):
            char = account.get_puppet(sess)
            table.add_row(
                str(sess.sessid),
                str(sess.protocol_key),
                isinstance(sess.address, tuple) and sess.address[0] or sess.address,
                char and str(char) or "None",
                char and str(char.location) or "N/A",
            )
            self.msg(f"|wYour current session(s):|n\n{table}")

class CmdWho(COMMAND_DEFAULT_CLASS):
    """
    list who is currently online

    Usage:
      who
      doing

    Shows who is currently online. Doing is an alias that limits info
    also for those with all permissions.
    """

    key = "who"
    aliases = "doing"

        """
        Get all connected accounts by polling session.
        """
        session_list = evennia.SESSION_HANDLER.get_sessions()

        session_list = sorted(session_list, key=lambda o: o.account.key)

        if self.cmdstring == "doing":
            show_session_data = False
        else:
            show_session_data = account.check_permstring("Developer") or account.check_permstring(
                "Admins"
            )

        naccounts = evennia.SESSION_HANDLER.account_count()
        if show_session_data:
            # privileged info
            table = self.styled_table(
                "|wAccount Name",
                "|wOn for",
                "|wIdle",
                "|wPuppeting",
                "|wRoom",
                "|wCmds",
                "|wProtocol",
                "|wHost",
            )
            for session in session_list:
                if not session.logged_in:
                    continue
                delta_cmd = time.time() - session.cmd_last_visible
                delta_conn = time.time() - session.conn_time
                session_account = session.get_account()
                puppet = session.get_puppet()
                location = puppet.location.key if puppet and puppet.location else "None"
                table.add_row(
                    utils.crop(session_account.get_display_name(account), width=25),
                    utils.time_format(delta_conn, 0),
                    utils.time_format(delta_cmd, 1),
                    utils.crop(puppet.get_display_name(account) if puppet else "None", width=25),
                    utils.crop(location, width=25),
                    session.cmd_total,
                    session.protocol_key,
                    isinstance(session.address, tuple) and session.address[0] or session.address,
                )
        else:
            # unprivileged
            table = self.styled_table("|wAccount name", "|wOn for", "|wIdle")
                    continue
                )
        is_one = naccounts == 1
        self.msg(
            "|wAccounts:|n\n%s\n%s unique account%s logged in."
            % (table, "One" if is_one else naccounts, "" if is_one else "s")
        )

class CmdOption(COMMAND_DEFAULT_CLASS):
    """
    Set an account option

    Usage:
      option[/save] [name = value]

    Switches:
      save - Save the current option settings for future logins.
      clear - Clear the saved options.

    This command allows for viewing and setting client interface
    settings. Note that saved options may not be able to be used if
    later connecting with a client with different capabilities.

    """

    key = "option"
    aliases = "options"
    switch_options = ("save", "clear")

        """
        Implements the command
        """
        if self.session is None:
            return

        flags = self.session.protocol_flags

        # Display current options
            # list the option settings

            if "save" in self.switches:
                # save all options
                self.caller.db._saved_protocol_flags = flags
                self.msg("|gSaved all options. Use option/clear to remove.|n")
            if "clear" in self.switches:
                # clear all saves
                self.caller.db._saved_protocol_flags = {}
                self.msg("|gCleared all saved options.")

            options = dict(flags)  # make a copy of the flag dict
            saved_options = dict(self.caller.attributes.get("_saved_protocol_flags", default={}))

            if "SCREENWIDTH" in options:
                if len(options["SCREENWIDTH"]) == 1:
                    options["SCREENWIDTH"] = options["SCREENWIDTH"][0]
                else:
                    options["SCREENWIDTH"] = "  \n".join(
                        "%s : %s" % (screenid, size)
                        for screenid, size in options["SCREENWIDTH"].items()
                    )
            if "SCREENHEIGHT" in options:
                if len(options["SCREENHEIGHT"]) == 1:
                    options["SCREENHEIGHT"] = options["SCREENHEIGHT"][0]
                else:
                    options["SCREENHEIGHT"] = "  \n".join(
                        for screenid, size in options["SCREENHEIGHT"].items()
                    )
            options.pop("TTYPE", None)

            header = ("Name", "Value", "Saved") if saved_options else ("Name", "Value")
            table = self.styled_table(*header)
            for key in sorted(options):
                row = [key, options[key]]
                if saved_options:
                    saved = " |YYes|n" if key in saved_options else ""
                    changed = (
                        "|y*|n" if key in saved_options and flags[key] != saved_options[key] else ""
                    )
                    row.append("%s%s" % (saved, changed))
                table.add_row(*row)
            self.msg(f"|wClient settings ({self.session.protocol_key}):|n\n{table}|n")

            return

        if not self.rhs:
            self.msg("Usage: option [name = [value]]")
            return

        # Try to assign new values

        def validate_encoding(new_encoding):
            # helper: change encoding
            try:
                codecs_lookup(new_encoding)
            except LookupError:
                raise RuntimeError(f"The encoding '|w{new_encoding}|n' is invalid. ")
            return val

        def validate_size(new_size):
            return {0: int(new_size)}

        def validate_bool(new_bool):
            return True if new_bool.lower() in ("true", "on", "1") else False

        def update(new_name, new_val, validator):
            # helper: update property and report errors
            try:
                old_val = flags.get(new_name, False)
                new_val = validator(new_val)
                if old_val == new_val:
                    self.msg(f"Option |w{new_name}|n was kept as '|w{old_val}|n'.")
                else:
                    flags[new_name] = new_val
                    self.msg(
                        f"Option |w{new_name}|n was changed from '|w{old_val}|n' to"
                        f" '|w{new_val}|n'."
                    )
                return {new_name: new_val}
            except Exception as err:
                self.msg(f"|rCould not set option |w{new_name}|r:|n {err}")
                return False

        validators = {
            "ANSI": validate_bool,
            "CLIENTNAME": utils.to_str,
            "ENCODING": validate_encoding,
            "MCCP": validate_bool,
            "NOGOAHEAD": validate_bool,
            "MXP": validate_bool,
            "NOCOLOR": validate_bool,
            "NOPKEEPALIVE": validate_bool,
            "OOB": validate_bool,
            "RAW": validate_bool,
            "SCREENHEIGHT": validate_size,
            "SCREENWIDTH": validate_size,
            "SCREENREADER": validate_bool,
            "TERM": utils.to_str,
            "UTF-8": validate_bool,
            "XTERM256": validate_bool,
            "INPUTDEBUG": validate_bool,
            "FORCEDENDLINE": validate_bool,
            "LOCALECHO": validate_bool,
        }

        name = self.lhs.upper()
        val = self.rhs.strip()
        optiondict = False
        if val and name in validators:
            optiondict = update(name, val, validators[name])
        else:
            self.msg("|rNo option named '|w%s|r'." % name)
        if optiondict:
            # a valid setting
                # save this option only
                saved_options = self.account.attributes.get("_saved_protocol_flags", default={})
                saved_options.update(optiondict)
                self.account.attributes.add("_saved_protocol_flags", saved_options)
                for key in optiondict:
                    self.msg(f"|gSaved option {key}.|n")
                # clear this save
                    self.account.attributes.get("_saved_protocol_flags", {}).pop(key, None)
                    self.msg(f"|gCleared saved {key}.")
            self.session.update_flags(**optiondict)

class CmdPassword(COMMAND_DEFAULT_CLASS):
    """
    change your password

    Usage:
      password <old password> = <new password>

    Changes your password. Make sure to pick a safe one.
    """

    key = "password"

        """hook function."""

            self.msg("Usage: password <oldpass> = <newpass>")
            return
        oldpass = self.lhslist[0]  # Both of these are
        newpass = self.rhslist[0]  # already stripped by parse()

        validated, error = account.validate_password(newpass)

        if not account.check_password(oldpass):
            self.msg("The specified old password isn't correct.")
        elif not validated:
            errors = [e for suberror in error.messages for e in error.messages]
            string = "\n".join(errors)
        else:
            account.set_password(newpass)
            account.save()
            self.msg("Password changed.")
                f"Password Changed: {account} (Caller: {account}, IP: {self.session.address})."
            )

class CmdQuit(COMMAND_DEFAULT_CLASS):
    """
    quit the game

    Usage:
      quit

    Switch:
      all - disconnect all connected sessions

    Gracefully disconnect your current session from the
    game. Use the /all switch to disconnect from all sessions.
    """

    key = "quit"
    switch_options = ("all",)

        """hook function"""

        if "all" in self.switches:
            account.msg(
                "|RQuitting|n all sessions. Hope to see you soon again.", session=self.session
            )
            reason = "quit/all"
            for session in account.sessions.all():
                account.disconnect_session_from_account(session, reason)
        else:
            nsess = len(account.sessions.all())
            reason = "quit"
            if nsess == 2:
                account.msg("|RQuitting|n. One session is still connected.", session=self.session)
            elif nsess > 2:
                account.msg(
                    "|RQuitting|n. %i sessions are still connected." % (nsess - 1),
                    session=self.session,
                )
            else:
                # we are quitting the last available session
                account.msg("|RQuitting|n. Hope to see you again, soon.", session=self.session)
            account.disconnect_session_from_account(self.session, reason)

class CmdColorTest(COMMAND_DEFAULT_CLASS):
    """
    testing which colors your client support

    Usage:
      color ansi | xterm256

    Prints a color map along with in-mud color codes to use to produce
    them.  It also tests what is supported in your client. Choices are
    16-color ansi (supported in most muds) or the 256-color xterm256
    standard. No checking is done to determine your client supports
    color - if not you will see rubbish appear.
    """

    key = "color"

    # the slices of the ANSI_PARSER lists to use for retrieving the
    # relevant color tags to display. Replace if using another schema.
    # This command can only show one set of markup.
    slice_bright_fg = slice(7, 15)  # from ANSI_PARSER.ansi_map
    slice_dark_fg = slice(15, 23)  # from ANSI_PARSER.ansi_map
    slice_dark_bg = slice(-8, None)  # from ANSI_PARSER.ansi_map
    slice_bright_bg = slice(None, None)  # from ANSI_PARSER.ansi_xterm256_bright_bg_map

    def table_format(self, table):
        """
        Helper method to format the ansi/xterm256 tables.
        Takes a table of columns [[val,val,...],[val,val,...],...]
        """
        if not table:
            return [[]]

        extra_space = 1
        max_widths = [max([len(str(val)) for val in col]) for col in table]
        ftable = []
        for irow in range(len(table[0])):
            ftable.append(
                [
                    str(col[irow]).ljust(max_widths[icol]) + " " * extra_space
                    for icol, col in enumerate(table)
                ]
            )
        return ftable

        """Show color tables"""

        if self.args.startswith("a"):
            # show ansi 16-color table
            from evennia.utils import ansi

            ap = ansi.ANSI_PARSER
            # ansi colors
            # show all ansi color-related codes
            bright_fg = [
                "%s%s|n" % (code, code.replace("|", "||"))
                for code, _ in ap.ansi_map[self.slice_bright_fg]
            ]
            dark_fg = [
                for code, _ in ap.ansi_map[self.slice_dark_fg]
            ]
            dark_bg = [
                "%s%s|n" % (code.replace("\\", ""), code.replace("|", "||").replace("\\", ""))
                for code, _ in ap.ansi_map[self.slice_dark_bg]
            ]
            bright_bg = [
                for code, _ in ap.ansi_xterm256_bright_bg_map[self.slice_bright_bg]
            ]
            dark_fg.extend(["" for _ in range(len(bright_fg) - len(dark_fg))])
            table = utils.format_table([bright_fg, dark_fg, bright_bg, dark_bg])
            string = "ANSI colors:"
            for row in table:
                string += "\n " + " ".join(row)
            self.msg(
                "||X : black. ||/ : return, ||- : tab, ||_ : space, ||* : invert, ||u : underline\n"
                "To combine background and foreground, add background marker last, e.g. ||r||[B.\n"
                "Note: bright backgrounds like ||[r requires your client handling Xterm256 colors."
            )

        elif self.args.startswith("x"):
            # show xterm256 table
            table = [[], [], [], [], [], [], [], [], [], [], [], []]
            for ir in range(6):
                for ig in range(6):
                    for ib in range(6):
                        # foreground table
                        table[ir].append("|%i%i%i%s|n" % (ir, ig, ib, "||%i%i%i" % (ir, ig, ib)))
                        # background table
                        table[6 + ir].append(
                            "|%i%i%i|[%i%i%i%s|n"
                            % (5 - ir, 5 - ig, 5 - ib, ir, ig, ib, "||[%i%i%i" % (ir, ig, ib))
                        )
            table = self.table_format(table)
            string = (
                "Xterm256 colors (if not all hues show, your client might not report that it can"
                " handle xterm256):"
            )
            string += "\n" + "\n".join("".join(row) for row in table)
            for ibatch in range(4):
                for igray in range(6):
                    letter = chr(97 + (ibatch * 6 + igray))
                    inverse = chr(122 - (ibatch * 6 + igray))
                    table[0 + igray].append("|=%s%s |n" % (letter, "||=%s" % letter))
                    table[6 + igray].append("|=%s|[=%s%s |n" % (inverse, letter, "||[=%s" % letter))
            for igray in range(6):
                # the last row (y, z) has empty columns
                if igray < 2:
                    letter = chr(121 + igray)
                    inverse = chr(98 - igray)
                    fg = "|=%s%s |n" % (letter, "||=%s" % letter)
                    bg = "|=%s|[=%s%s |n" % (inverse, letter, "||[=%s" % letter)
                else:
                    fg, bg = " ", " "
                table[0 + igray].append(fg)
                table[6 + igray].append(bg)
        else:
            # malformed input
            self.msg("Usage: color ansi||xterm256")

class CmdQuell(COMMAND_DEFAULT_CLASS):
    """
    use character's permissions instead of account's

    Usage:
      quell
      unquell

    Normally the permission level of the Account is used when puppeting a
    Character/Object to determine access. This command will switch the lock
    system to make use of the puppeted Object's permissions instead. This is
    useful mainly for testing.
    Hierarchical permission quelling only work downwards, thus an Account cannot
    use a higher-permission Character to escalate their permission level.
    Use the unquell command to revert back to normal operation.
    """

    key = "quell"
    aliases = ["unquell"]

    def _recache_locks(self, account):
        """Helper method to reset the lockhandler on an already puppeted object"""
            char = self.session.puppet
            if char:
                # we are already puppeting an object. We need to reset
                # the lock caches (otherwise the superuser status change
                # won't be visible until repuppet)
                char.locks.reset()
        account.locks.reset()

        """Perform the command"""
        permstr = (
            account.is_superuser and "(superuser)" or "(%s)" % ", ".join(account.permissions.all())
        )
        if self.cmdstring in ("unquell", "unquell"):
            if not account.attributes.get("_quell"):
                self.msg(f"Already using normal Account permissions {permstr}.")
            else:
                account.attributes.remove("_quell")
                self.msg(f"Account permissions {permstr} restored.")
        else:
            if account.attributes.get("_quell"):
                self.msg(f"Already quelling Account {permstr} permissions.")
                return
            account.attributes.add("_quell", True)
            puppet = self.session.puppet if self.session else None
            if puppet:
                cpermstr = "(%s)" % ", ".join(puppet.permissions.all())
                cpermstr = f"Quelling to current puppet's permissions {cpermstr}."
                cpermstr += (
                    f"\n(Note: If this is higher than Account permissions {permstr},"
                    " the lowest of the two will be used.)"
                )
                cpermstr += "\nUse unquell to return to normal permission usage."
                self.msg(cpermstr)
            else:
                self.msg(f"Quelling Account permissions {permstr}. Use unquell to get them back.")
        self._recache_locks(account)

class CmdStyle(COMMAND_DEFAULT_CLASS):
    """
    In-game style options

    Usage:
      style
      style <option> = <value>

    Configure stylings for in-game display elements like table borders, help
    entries etc. Use without arguments to see all available options.

    """

    key = "style"
    switch_options = ["clear"]

            self.list_styles()
            return
        self.set()

    def list_styles(self):
        table = self.styled_table("Option", "Description", "Type", "Value", width=78)
        for op_key in self.account.options.options_dict.keys():
            op_found = self.account.options.get(op_key, return_obj=True)
                op_key, op_found.description, op_found.__class__.__name__, op_found.display()
            )
        self.msg(str(table))

    def set(self):
        try:
            result = self.account.options.set(self.lhs, self.rhs)
        except ValueError as e:
            self.msg(str(e))
            return
        self.msg(f"Style {result.key} set to {result.display()}")

"""

Admin commands

"""

import re

from evennia.utils import class_from_module, evtable, logger, search

COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)

PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]

# limit members for API inclusion
__all__ = (
    "CmdBoot",
    "CmdBan",
    "CmdUnban",
    "CmdEmit",
    "CmdNewPassword",
    "CmdPerm",
    "CmdWall",
    "CmdForce",
)

class CmdBoot(COMMAND_DEFAULT_CLASS):
    """
    kick an account from the server.

    Usage
      boot[/switches] <account obj> [: reason]

    Switches:
      quiet - Silently boot without informing account
      sid - boot by session id instead of name or dbref

    Boot an account object from the server. If a reason is
    supplied it will be echoed to the user unless /quiet is set.
    """

    key = "boot"
    switch_options = ("quiet", "sid")
    locks = "cmd:perm(boot) or perm(Admin)"
    help_category = "Admin"

        """Implementing the function"""
        caller = self.caller
        args = self.args

            caller.msg("Usage: boot[/switches] <account> [:reason]")
            return

        if ":" in args:
            args, reason = [a.strip() for a in args.split(":", 1)]
        else:
            args, reason = args, ""

        boot_list = []

        if "sid" in self.switches:
            # Boot a particular session id.
            sessions = evennia.SESSION_HANDLER.get_sessions(True)
            for sess in sessions:
                # Find the session with the matching session id.
                if sess.sessid == int(args):
                    boot_list.append(sess)
                    break
        else:
            # Boot by account object
            pobj = search.account_search(args)
            if not pobj:
                caller.msg(f"Account {args} was not found.")
                return
            pobj = pobj[0]
            if not pobj.access(caller, "boot"):
                caller.msg(f"You don't have the permission to boot {pobj.key}.")
                return
            # we have a bootable object with a connected user
            matches = evennia.SESSION_HANDLER.sessions_from_account(pobj)
            for match in matches:
                boot_list.append(match)

        if not boot_list:
            caller.msg("No matching sessions found. The Account does not seem to be online.")
            return

        # Carry out the booting of the sessions in the boot list.

        feedback = None
        if "quiet" not in self.switches:
            feedback = f"You have been disconnected by {caller.name}.\n"
            if reason:
                feedback += f"\nReason given: {reason}"

        for session in boot_list:
            session.msg(feedback)
            session.account.disconnect_session_from_account(session)

        if pobj and boot_list:
                f"Booted: {pobj} (Reason: {reason}, Caller: {caller}, IP: {self.session.address})."
            )

# regex matching IP addresses with wildcards, eg. 233.122.4.*
IPREGEX = re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}")

def list_bans(cmd, banlist):
    """
    Helper function to display a list of active bans. Input argument
    is the banlist read into the two commands ban and unban below.

    Args:
        cmd (Command): Instance of the Ban command.
        banlist (list): List of bans to list.
    """
    if not banlist:
        return "No active bans were found."

    table = cmd.styled_table("|wid", "|wname/ip", "|wdate", "|wreason")
    for inum, ban in enumerate(banlist):
        table.add_row(str(inum + 1), ban[0] and ban[0] or ban[1], ban[3], ban[4])
    return f"|wActive bans:|n\n{table}"

class CmdBan(COMMAND_DEFAULT_CLASS):
    """
    ban an account from the server

    Usage:
      ban [<name or ip> [: reason]]

    Without any arguments, shows numbered list of active bans.

    This command bans a user from accessing the game. Supply an optional
    reason to be able to later remember why the ban was put in place.

    It is often preferable to ban an account from the server than to
    delete an account with accounts/delete. If banned by name, that account
    account can no longer be logged into.

    IP (Internet Protocol) address banning allows blocking all access
    from a specific address or subnet. Use an asterisk (*) as a
    wildcard.

    Examples:
      ban thomas             - ban account 'thomas'
      ban/ip 134.233.2.111   - ban specific ip address
      ban/ip 134.233.2.*     - ban all in a subnet
      ban/ip 134.233.*.*     - even wider ban

    A single IP filter can be easy to circumvent by changing computers
    or requesting a new IP address. Setting a wide IP block filter with
    wildcards might be tempting, but remember that it may also
    accidentally block innocent users connecting from the same country
    or region.

    """

    key = "ban"
    aliases = ["bans"]
    locks = "cmd:perm(ban) or perm(Developer)"

        """
        Bans are stored in a serverconf db object as a list of
        dictionaries:
          [ (name, ip, ipregex, date, reason),
            (name, ip, ipregex, date, reason),...  ]
        where name and ip are set by the user and are shown in
        lists. ipregex is a converted form of ip where the * is
        replaced by an appropriate regex pattern for fast
        matching. date is the time stamp the ban was instigated and
        'reason' is any optional info given to the command. Unset
        values in each tuple is set to the empty string.
        """
        banlist = ServerConfig.objects.conf("server_bans")
        if not banlist:
            banlist = []

        if not self.args or (
            self.switches and not any(switch in ("ip", "name") for switch in self.switches)
        ):
            self.msg(list_bans(self, banlist))
            return

        now = time.ctime()
        reason = ""
        if ":" in self.args:
            ban, reason = self.args.rsplit(":", 1)
        else:
            ban = self.args
        ban = ban.lower()
        ipban = IPREGEX.findall(ban)
        if not ipban:
            # store as name
            typ = "Name"
            bantup = (ban, "", "", now, reason)
        else:
            # an ip address.
            typ = "IP"
            ban = ipban[0]
            # replace * with regex form and compile it
            ipregex = ban.replace(".", "\.")
            ipregex = ipregex.replace("*", "[0-9]{1,3}")
            ipregex = re.compile(r"%s" % ipregex)
            bantup = ("", ban, ipregex, now, reason)

        ret = yield (f"Are you sure you want to {typ}-ban '|w{ban}|n' [Y]/N?")
        if str(ret).lower() in ("no", "n"):
            self.msg("Aborted.")
            return

        # save updated banlist
        banlist.append(bantup)
        ServerConfig.objects.conf("server_bans", banlist)
        self.msg(f"{typ}-ban '|w{ban}|n' was added. Use |wunban|n to reinstate.")
            f"Banned {typ}: {ban.strip()} (Caller: {self.caller}, IP: {self.session.address})."
        )

class CmdUnban(COMMAND_DEFAULT_CLASS):
    """
    remove a ban from an account

    Usage:
      unban <banid>

    This will clear an account name/ip ban previously set with the ban
    command.  Use this command without an argument to view a numbered
    list of bans. Use the numbers in this list to select which one to
    unban.

    """

    key = "unban"
    locks = "cmd:perm(unban) or perm(Developer)"

        """Implement unbanning"""

            return

        try:
            num = int(self.args)
            self.msg("You must supply a valid ban id to clear.")
            return

            self.msg("There are no bans to clear.")
        elif not (0 < num < len(banlist) + 1):
            self.msg(f"Ban id |w{self.args}|n was not found.")
        else:
            # all is ok, ask, then clear ban
            ban = banlist[num - 1]
            value = (" ".join([s for s in ban[:2]])).strip()

            ret = yield (f"Are you sure you want to unban {num}: '|w{value}|n' [Y]/N?")
            if str(ret).lower() in ("n", "no"):
                self.msg("Aborted.")
                return

            del banlist[num - 1]
            ServerConfig.objects.conf("server_bans", banlist)
            self.msg(f"Cleared ban {num}: '{value}'")
                f"Unbanned: {value.strip()} (Caller: {self.caller}, IP: {self.session.address})."
            )

class CmdEmit(COMMAND_DEFAULT_CLASS):
    """
    admin command for emitting message to multiple objects

    Usage:
      emit[/switches] [<obj>, <obj>, ... =] <message>
      remit           [<obj>, <obj>, ... =] <message>
      pemit           [<obj>, <obj>, ... =] <message>

    Switches:
      room     -  limit emits to rooms only (default)
      accounts -  limit emits to accounts only
      contents -  send to the contents of matched objects too

    Emits a message to the selected objects or to
    your immediate surroundings. If the object is a room,
    send to its contents. remit and pemit are just
    limited forms of emit, for sending to rooms and
    to accounts respectively.
    """

    key = "emit"
    aliases = ["pemit", "remit"]
    switch_options = ("room", "accounts", "contents")
    locks = "cmd:perm(emit) or perm(Builder)"

        """Implement the command"""

            string = "Usage: "
            string += "\nemit[/switches] [<obj>, <obj>, ... =] <message>"
            string += "\nremit           [<obj>, <obj>, ... =] <message>"
            string += "\npemit           [<obj>, <obj>, ... =] <message>"
            caller.msg(string)
            return

        rooms_only = "rooms" in self.switches
        accounts_only = "accounts" in self.switches
        send_to_contents = "contents" in self.switches

        # we check which command was used to force the switches
        if self.cmdstring == "remit":
            rooms_only = True
            send_to_contents = True
        elif self.cmdstring == "pemit":
            accounts_only = True

            message = self.args
            objnames = [caller.location.key]
        else:
            message = self.rhs
            objnames = self.lhslist

        # send to all objects
        for objname in objnames:
            obj = caller.search(objname, global_search=True)
            if not obj:
                return
            if rooms_only and obj.location is not None:
                caller.msg(f"{objname} is not a room. Ignored.")
                continue
            if accounts_only and not obj.has_account:
                caller.msg(f"{objname} has no active account. Ignored.")
                continue
            if obj.access(caller, "tell"):
                obj.msg(message)
                if send_to_contents and hasattr(obj, "msg_contents"):
                    obj.msg_contents(message)
                    caller.msg(f"Emitted to {objname} and contents:\n{message}")
                else:
                    caller.msg(f"Emitted to {objname}:\n{message}")
            else:
                caller.msg(f"You are not allowed to emit to {objname}.")

class CmdNewPassword(COMMAND_DEFAULT_CLASS):
    """
    change the password of an account

    Usage:
      userpassword <user obj> = <new password>

    Set an account's password.
    """

    key = "userpassword"
    locks = "cmd:perm(newpassword) or perm(Admin)"

        """Implement the function."""

            self.msg("Usage: userpassword <user obj> = <new password>")
            return

        # the account search also matches 'me' etc.
        account = caller.search_account(self.lhs)
            return

        newpass = self.rhs

        if not validated:
            return

        account.set_password(newpass)
        account.save()
        self.msg(f"{account.name} - new password set to '{newpass}'.")
        if account.character != caller:
            account.msg(f"{caller.name} has changed your password to '{newpass}'.")
            f"Password Changed: {account} (Caller: {caller}, IP: {self.session.address})."
        )

class CmdPerm(COMMAND_DEFAULT_CLASS):
    """
    set the permissions of an account/object

    Usage:
      perm[/switch] <object> [= <permission>[,<permission>,...]]
      perm[/switch] *<account> [= <permission>[,<permission>,...]]

    Switches:
      del     -  delete the given permission from <object> or <account>.
      account -  set permission on an account (same as adding * to name)

    This command sets/clears individual permission strings on an object
    or account. If no permission is given, list all permissions on <object>.
    """

    key = "perm"
    aliases = "setperm"
    switch_options = ("del", "account")
    locks = "cmd:perm(perm) or perm(Developer)"

        switches = self.switches
        lhs, rhs = self.lhs, self.rhs

            string = "Usage: perm[/switch] object [ = permission, permission, ...]"
            return

        accountmode = "account" in self.switches or lhs.startswith("*")
        lhs = lhs.lstrip("*")

        if accountmode:
            obj = caller.search_account(lhs)
        else:
            obj = caller.search(lhs, global_search=True)
        if not obj:
            return

        if not rhs:
            if not obj.access(caller, "examine"):
                caller.msg("You are not allowed to examine this object.")
                return

            string = f"Permissions on |w{obj.key}|n: "
            if not obj.permissions.all():
                string += "<None>"
            else:
                string += ", ".join(obj.permissions.all())
                if (
                    hasattr(obj, "account")
                    and hasattr(obj.account, "is_superuser")
                    and obj.account.is_superuser
                ):
                    string += "\n(... but this object is currently controlled by a SUPERUSER! "
                    string += "All access checks are passed automatically.)"
            return

        # we supplied an argument on the form obj = perm
        locktype = "edit" if accountmode else "control"
        if not obj.access(caller, locktype):
            accountstr = "account" if accountmode else "object"
            caller.msg(f"You are not allowed to edit this {accountstr}'s permissions.")
            return

        caller_result = []
        target_result = []
        if "del" in switches:
            # delete the given permission(s) from object.
            for perm in self.rhslist:
                obj.permissions.remove(perm)
                if obj.permissions.get(perm):
                    caller_result.append(
                        f"\nPermissions {perm} could not be removed from {obj.name}."
                    )
                else:
                        f"\nPermission {perm} removed from {obj.name} (if they existed)."
                    )
                    target_result.append(
                        f"\n{caller.name} revokes the permission(s) {perm} from you."
                    )
                        f"Permissions Deleted: {perm}, {obj} (Caller: {caller}, IP: {self.session.address})."
                    )
        else:
            # add a new permission
            permissions = obj.permissions.all()

                # don't allow to set a permission higher in the hierarchy than
                # the one the caller has (to prevent self-escalation)
                if perm.lower() in PERMISSION_HIERARCHY and not obj.locks.check_lockstring(
                    caller, f"dummy:perm({perm})"
                ):
                    caller.msg(
                        "You cannot assign a permission higher than the one you have yourself."
                    )
                    return

                if perm in permissions:
                    caller_result.append(f"\nPermission '{perm}' is already defined on {obj.name}.")
                else:
                    obj.permissions.add(perm)
                    plystring = "the Account" if accountmode else "the Object/Character"
                        f"\nPermission '{perm}' given to {obj.name} ({plystring})."
                    )
                        f"\n{caller.name} gives you ({obj.name}, {plystring}) the permission '{perm}'."
                    )
                        f"Permissions Added: {perm}, {obj} (Caller: {caller}, IP: {self.session.address})."
                    )

        caller.msg("".join(caller_result).strip())
        if target_result:
            obj.msg("".join(target_result).strip())

class CmdWall(COMMAND_DEFAULT_CLASS):
    """
    make an announcement to all

    Usage:
      wall <message>

    Announces a message to all connected sessions
    including all currently unlogged in.
    """

    key = "wall"
    locks = "cmd:perm(wall) or perm(Admin)"

        """Implements command"""
            self.msg("Usage: wall <message>")
            return
        message = f'{self.caller.name} shouts "{self.args}"'
        self.msg("Announcing to all connected sessions ...")
        evennia.SESSION_HANDLER.announce_all(message)

class CmdForce(COMMAND_DEFAULT_CLASS):
    """
    forces an object to execute a command

    Usage:
        force <object>=<command string>

    Example:
        force bob=get stick
    """

    key = "force"
    locks = "cmd:perm(spawn) or perm(Builder)"
    help_category = "Building"
    perm_used = "edit"

        """Implements the force command"""
        if not self.lhs or not self.rhs:
            self.msg("You must provide a target and a command string to execute.")
            return
        targ = self.caller.search(self.lhs)
        if not targ:
            return
        if not targ.access(self.caller, self.perm_used):
            self.msg(f"You don't have permission to force {targ} to execute commands.")
            return
        targ.execute_cmd(self.rhs)
        self.msg(f"You have forced {targ} to: {self.rhs}")

"""
Batch processors

These commands implements the 'batch-command' and 'batch-code'
processors, using the functionality in evennia.utils.batchprocessors.
They allow for offline world-building.

Batch-command is the simpler system. This reads a file (*.ev)
containing a list of in-game commands and executes them in sequence as
if they had been entered in the game (including permission checks
etc).

Batch-code is a full-fledged python code interpreter that reads blocks
of python code (*.py) and executes them in sequence. This allows for
much more power than Batch-command, but requires knowing Python and
the Evennia API.  It is also a severe security risk and should
therefore always be limited to superusers only.

"""
import re

from evennia.utils.batchprocessors import BATCHCMD, BATCHCODE

_RE_COMMENT = re.compile(r"^#.*?$", re.MULTILINE + re.DOTALL)
_RE_CODE_START = re.compile(r"^# batchcode code:", re.MULTILINE)
_COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)

# limit symbols for API inclusion
__all__ = ("CmdBatchCommands", "CmdBatchCode")

_HEADER_WIDTH = 70
_UTF8_ERROR = """
 |rDecode error in '%s'.|n

 This file contains non-ascii character(s). This is common if you
 wrote some input in a language that has more letters and special
 symbols than English; such as accents or umlauts.  This is usually
 fine and fully supported! But for Evennia to know how to decode such
 characters in a universal way, the batchfile must be saved with the
 international 'UTF-8' encoding. This file is not.

 Please re-save the batchfile with the UTF-8 encoding (refer to the
 documentation of your text editor on how to do this, or switch to a
 better featured one) and try again.

 Error reported was: '%s'
"""

# -------------------------------------------------------------

def format_header(caller, entry):
    """
    Formats a header
    """
    width = _HEADER_WIDTH - 10
    # strip all comments for the header
    if caller.ndb.batch_batchmode != "batch_commands":
        # only do cleanup for  batchcode
        entry = _RE_CODE_START.split(entry, 1)[1]
        entry = _RE_COMMENT.sub("", entry).strip()
    header = utils.crop(entry, width=width)
    ptr = caller.ndb.batch_stackptr + 1
    stacklen = len(caller.ndb.batch_stack)
    header = "|w%02i/%02i|G: %s|n" % (ptr, stacklen, header)
    # add extra space to the side for padding.
    header = "%s%s" % (header, " " * (width - len(header)))
    header = header.replace("\n", "\\n")

    return header

def format_code(entry):
    """
    Formats the viewing of code and errors
    """
    code = ""
    for line in entry.split("\n"):
        code += "\n|G>>>|n %s" % line
    return code.strip()

def batch_cmd_exec(caller):
    """
    Helper function for executing a single batch-command entry
    """
    ptr = caller.ndb.batch_stackptr
    stack = caller.ndb.batch_stack
    command = stack[ptr]
    caller.msg(format_header(caller, command))
    try:
        caller.execute_cmd(command)
        logger.log_trace()

def batch_code_exec(caller):
    """
    Helper function for executing a single batch-code entry
    """
    debug = caller.ndb.batch_debug
    code = stack[ptr]

    caller.msg(format_header(caller, code))
    err = BATCHCODE.code_exec(code, extra_environ={"caller": caller}, debug=debug)
    if err:
        caller.msg(format_code(err))

def step_pointer(caller, step=1):
    """
    Step in stack, returning the item located.

    stackptr - current position in stack
    stack - the stack of units
    step - how many steps to move from stackptr
    """
    nstack = len(stack)
    if ptr + step <= 0:
        caller.msg("|RBeginning of batch file.")
    if ptr + step >= nstack:
        caller.msg("|REnd of batch file.")
    caller.ndb.batch_stackptr = max(0, min(nstack - 1, ptr + step))

def show_curr(caller, showall=False):
    """
    Show the current position in stack
    """
    stackptr = caller.ndb.batch_stackptr

    if stackptr >= len(stack):
        caller.ndb.batch_stackptr = len(stack) - 1
        show_curr(caller, showall)
        return

    entry = stack[stackptr]

    string = format_header(caller, entry)
    codeall = entry.strip()
    string += "|G(hh for help)"
    if showall:
        for line in codeall.split("\n"):
            string += "\n|G||n %s" % line
    caller.msg(string)

def purge_processor(caller):
    """
    This purges all effects running
    on the caller.
    """
    try:
        del caller.ndb.batch_stack
        del caller.ndb.batch_stackptr
        del caller.ndb.batch_pythonpath
        del caller.ndb.batch_batchmode
        # something might have already been erased; it's not critical
        pass
    # clear everything back to the state before the batch call
    if caller.ndb.batch_cmdset_backup:
        caller.cmdset.cmdset_stack = caller.ndb.batch_cmdset_backup
        caller.cmdset.update()
        del caller.ndb.batch_cmdset_backup
    else:
        # something went wrong. Purge cmdset except default
        caller.cmdset.clear()

    # caller.scripts.validate()  # this will purge interactive mode

# main access commands

class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
    """
    build from batch-command file

    Usage:
     batchcommands[/interactive] <python.path.to.file>

    Switch:
       interactive - this mode will offer more control when
                     executing the batch file, like stepping,
                     skipping, reloading etc.

    Runs batches of commands from a batch-cmd text file (*.ev).

    """

    key = "batchcommands"
    aliases = ["batchcommand", "batchcmd"]
    switch_options = ("interactive",)
    locks = "cmd:perm(batchcommands) or perm(Developer)"

        """Starts the processor."""

            caller.msg("Usage: batchcommands[/interactive] <path.to.file>")
            return
        python_path = self.args

        # parse indata file

        try:
            commands = BATCHCMD.parse_file(python_path)
        except UnicodeDecodeError as err:
            caller.msg(_UTF8_ERROR % (python_path, err))
            return
        except IOError as err:
            if err:
                err = "{}\n".format(str(err))
            else:
                err = ""
            string = (
                "%s'%s' could not load. You have to supply python paths "
                "from one of the defined batch-file directories\n (%s)."
            )
            caller.msg(string % (err, python_path, ", ".join(settings.BASE_BATCHPROCESS_PATHS)))
            return
        if not commands:
            caller.msg(f"File {python_path} seems empty of valid commands.")
            return

        # Store work data in cache
        caller.ndb.batch_stack = commands
        caller.ndb.batch_stackptr = 0
        caller.ndb.batch_pythonpath = python_path
        caller.ndb.batch_batchmode = "batch_commands"
        # we use list() here to create a new copy of the cmdset stack
        caller.ndb.batch_cmdset_backup = list(caller.cmdset.cmdset_stack)
        caller.cmdset.add(BatchSafeCmdSet)

        if "inter" in switches or "interactive" in switches:
            # Allow more control over how batch file is executed

            # Set interactive state directly
            caller.cmdset.add(BatchInteractiveCmdSet)

            caller.msg(f"\nBatch-command processor - Interactive mode for {python_path} ...")
            show_curr(caller)
        else:
            caller.msg(
                "Running Batch-command processor - Automatic mode "
                f"for {python_path} (this might take some time) ..."
            )

            # run in-process (might block)
            for _ in range(len(commands)):
                # loop through the batch file
                if not batch_cmd_exec(caller):
                    return
                step_pointer(caller, 1)
            # clean out the safety cmdset and clean out all other
            # temporary attrs.
            caller.msg(f"|G  Batchfile '{python_path}' applied.")
            purge_processor(caller)

class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
    """
    build from batch-code file

    Usage:
     batchcode[/interactive] <python path to file>

    Switch:
       debug - auto-delete all objects that has been marked as
               deletable in the script file (see example files for
               syntax). This is useful so as to to not leave multiple
               object copies behind when testing out the script.

    Runs batches of commands from a batch-code text file (*.py).

    """

    key = "batchcode"
    aliases = ["batchcodes"]
    switch_options = ("interactive", "debug")
    locks = "cmd:superuser()"

            caller.msg("Usage: batchcode[/interactive/debug] <path.to.file>")
            return
        debug = "debug" in self.switches

        try:
            codes = BATCHCODE.parse_file(python_path)
            return
            if err:
                err = f"{err}\n"
            else:
                err = ""
            string = (
            )
            return
        if not codes:
            caller.msg(f"File {python_path} seems empty of functional code.")
            return

        caller.ndb.batch_stack = codes
        caller.ndb.batch_batchmode = "batch_code"
        caller.ndb.batch_debug = debug
        # we use list() here to create a new copy of cmdset_stack

            caller.msg(f"\nBatch-code processor - Interactive mode for {python_path} ...")
        else:
            caller.msg(f"Running Batch-code processor - Automatic mode for {python_path} ...")

            for _ in range(len(codes)):
                if not batch_code_exec(caller):
                    return

# State-commands for the interactive batch processor modes
# (these are the same for both processors)

class CmdStateAbort(_COMMAND_DEFAULT_CLASS):
    """
    abort

    This is a safety feature. It force-ejects us out of the processor and to
    the default cmdset, regardless of what current cmdset the processor might
    have put us in (e.g. when testing buggy scripts etc).
    """

    key = "abort"
    help_category = "BatchProcess"
    locks = "cmd:perm(batchcommands)"

        """Exit back to default."""
        purge_processor(self.caller)
        self.msg("Exited processor and reset out active cmdset back to the default one.")

class CmdStateLL(_COMMAND_DEFAULT_CLASS):
    """
    ll

    Look at the full source for the current
    command definition.
    """

    key = "ll"

        show_curr(self.caller, showall=True)

class CmdStatePP(_COMMAND_DEFAULT_CLASS):
    """
    pp

    Process the currently shown command definition.
    """

    key = "pp"

        """
        This checks which type of processor we are running.
        """
        if caller.ndb.batch_batchmode == "batch_code":
            batch_code_exec(caller)
        else:
            batch_cmd_exec(caller)

class CmdStateRR(_COMMAND_DEFAULT_CLASS):
    """
    rr

    Reload the batch file, keeping the current
    position in it.
    """

    key = "rr"

            new_data = BATCHCODE.parse_file(caller.ndb.batch_pythonpath)
        else:
            new_data = BATCHCMD.parse_file(caller.ndb.batch_pythonpath)
        caller.ndb.batch_stack = new_data
        caller.msg(format_code("File reloaded. Staying on same command."))
        show_curr(caller)

class CmdStateRRR(_COMMAND_DEFAULT_CLASS):
    """
    rrr

    Reload the batch file, starting over
    from the beginning.
    """

    key = "rrr"

            BATCHCODE.parse_file(caller.ndb.batch_pythonpath)
        else:
            BATCHCMD.parse_file(caller.ndb.batch_pythonpath)
        caller.msg(format_code("File reloaded. Restarting from top."))

class CmdStateNN(_COMMAND_DEFAULT_CLASS):
    """
    nn

    Go to next command. No commands are executed.
    """

    key = "nn"

        arg = self.args
        if arg and arg.isdigit():
            step = int(self.args)
        else:
            step = 1
        step_pointer(caller, step)

class CmdStateNL(_COMMAND_DEFAULT_CLASS):
    """
    nl

    Go to next command, viewing its full source.
    No commands are executed.
    """

    key = "nl"

        else:
            step = 1
        show_curr(caller, showall=True)

class CmdStateBB(_COMMAND_DEFAULT_CLASS):
    """
    bb

    Backwards to previous command. No commands
    are executed.
    """

    key = "bb"

            step = -int(self.args)
        else:
            step = -1

class CmdStateBL(_COMMAND_DEFAULT_CLASS):
    """
    bl

    Backwards to previous command, viewing its full
    source. No commands are executed.
    """

    key = "bl"

        else:
            step = -1

class CmdStateSS(_COMMAND_DEFAULT_CLASS):
    """
    ss [steps]

    Process current command, then step to the next
    one. If steps is given,
    process this many commands.
    """

    key = "ss"

        else:
            step = 1

        for _ in range(step):
            if caller.ndb.batch_batchmode == "batch_code":
                batch_code_exec(caller)
            else:
                batch_cmd_exec(caller)
            step_pointer(caller, 1)

class CmdStateSL(_COMMAND_DEFAULT_CLASS):
    """
    sl [steps]

    one, viewing its full source. If steps is given,
    """

    key = "sl"

        else:
            step = 1

            else:

class CmdStateCC(_COMMAND_DEFAULT_CLASS):
    """
    cc

    Continue to process all remaining
    commands.
    """

    key = "cc"

        nstack = len(caller.ndb.batch_stack)
        ptr = caller.ndb.batch_stackptr
        step = nstack - ptr

            else:

        purge_processor(self)
        caller.msg(format_code("Finished processing batch file."))

class CmdStateJJ(_COMMAND_DEFAULT_CLASS):
    """
    jj <command number>

    Jump to specific command number
    """

    key = "jj"

            number = int(self.args) - 1
        else:
            caller.msg(format_code("You must give a number index."))
            return
        step = number - ptr

class CmdStateJL(_COMMAND_DEFAULT_CLASS):
    """
    jl <command number>

    Jump to specific command number and view its full source.
    """

    key = "jl"

        else:
            return

class CmdStateQQ(_COMMAND_DEFAULT_CLASS):
    """
    qq

    Quit the batchprocessor.
    """

    key = "qq"

        self.msg("Aborted interactive batch mode.")

class CmdStateHH(_COMMAND_DEFAULT_CLASS):
    """Help command"""

    key = "hh"

        string = """
    Interactive batch processing commands:

     nn [steps] - next command (no processing)
     nl [steps] - next & look
     bb [steps] - back to previous command (no processing)
     bl [steps] - back & look
     jj   <N>   - jump to command nr N (no processing)
     jl   <N>   - jump & look
     pp         - process currently shown command (no step)
     ss [steps] - process & step
     sl [steps] - process & step & look
     ll         - look at full definition of current command
     rr         - reload batch file (stay on current)
     rrr        - reload batch file (start from first)
     hh         - this help list

     cc         - continue processing to end, then quit.
     qq         - quit (abort all remaining commands)

     abort - this is a safety command that always is available
              regardless of what cmdsets gets added to us during
              batch-command processing. It immediately shuts down
              the processor and returns us to the default cmdset.
    """

#
# Defining the cmdsets for the interactive batchprocessor
# mode (same for both processors)
#

class BatchSafeCmdSet(CmdSet):
    """
    The base cmdset for the batch processor.
    This sets a 'safe' abort command that will
    always be available to get out of everything.
    """

    key = "Batch_default"
    priority = 150  # override other cmdsets.

        """Init the cmdset"""
        self.add(CmdStateAbort())

class BatchInteractiveCmdSet(CmdSet):
    """
    The cmdset for the interactive batch processor mode.
    """

    key = "Batch_interactive"
    priority = 104

        """init the cmdset"""
        self.add(CmdStateLL())
        self.add(CmdStatePP())
        self.add(CmdStateRR())
        self.add(CmdStateRRR())
        self.add(CmdStateNN())
        self.add(CmdStateNL())
        self.add(CmdStateBB())
        self.add(CmdStateBL())
        self.add(CmdStateSS())
        self.add(CmdStateSL())
        self.add(CmdStateCC())
        self.add(CmdStateJJ())
        self.add(CmdStateJL())
        self.add(CmdStateQQ())
        self.add(CmdStateHH())

"""
Building and world design commands
"""
import re

from django.core.paginator import Paginator
from django.db.models import Max, Min, Q

from evennia import InterruptCommand
from evennia.commands.cmdhandler import get_and_merge_cmdsets
from evennia.locks.lockhandler import LockException
from evennia.prototypes import menus as olc_menus
from evennia.prototypes import prototypes as protlib
from evennia.prototypes import spawner
from evennia.scripts.models import ScriptDB
from evennia.utils import create, funcparser, logger, search, utils
from evennia.utils.ansi import raw as ansi_raw
from evennia.utils.dbserialize import deserialize
from evennia.utils.eveditor import EvEditor
from evennia.utils.evmore import EvMore
    class_from_module,
    crop,
    dbref,
    display_len,
    format_grid,
    get_all_typeclasses,
    inherits_from,
    interactive,
    list_to_string,
)

_FUNCPARSER = None
_ATTRFUNCPARSER = None

_KEY_REGEX = re.compile(r"(?P<attr>.*?)(?P<key>(\[.*\]\ *)+)?$")

__all__ = (
    "ObjManipCommand",
    "CmdSetObjAlias",
    "CmdCopy",
    "CmdCpAttr",
    "CmdMvAttr",
    "CmdCreate",
    "CmdDesc",
    "CmdDestroy",
    "CmdDig",
    "CmdTunnel",
    "CmdLink",
    "CmdUnLink",
    "CmdSetHome",
    "CmdListCmdSets",
    "CmdName",
    "CmdOpen",
    "CmdSetAttribute",
    "CmdTypeclass",
    "CmdWipe",
    "CmdLock",
    "CmdExamine",
    "CmdFind",
    "CmdTeleport",
    "CmdScripts",
    "CmdObjects",
    "CmdTag",
    "CmdSpawn",
)

# used by set
from ast import literal_eval as _LITERAL_EVAL

LIST_APPEND_CHAR = "+"

# used by find
CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
ROOM_TYPECLASS = settings.BASE_ROOM_TYPECLASS
EXIT_TYPECLASS = settings.BASE_EXIT_TYPECLASS
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH

_PROTOTYPE_PARENTS = None

class ObjManipCommand(COMMAND_DEFAULT_CLASS):
    """
    This is a parent class for some of the defining objmanip commands
    since they tend to have some more variables to define new objects.

    Each object definition can have several components. First is
    always a name, followed by an optional alias list and finally an
    some optional data, such as a typeclass or a location. A comma ','
    separates different objects. Like this:

        name1;alias;alias;alias:option, name2;alias;alias ...

    Spaces between all components are stripped.

    A second situation is attribute manipulation. Such commands
    are simpler and offer combinations

        objname/attr/attr/attr, objname/attr, ...

    """

    # OBS - this is just a parent - it's not intended to actually be
    # included in a commandset on its own!

    # used by get_object_typeclass as defaults.
    default_typeclasses = {
        "object": settings.BASE_OBJECT_TYPECLASS,
        "character": settings.BASE_CHARACTER_TYPECLASS,
        "room": settings.BASE_ROOM_TYPECLASS,
        "exit": settings.BASE_EXIT_TYPECLASS,
    }

        """
        We need to expand the default parsing to get all
        the cases, see the module doc.
        """
        # get all the normal parsing done (switches etc)

        obj_defs = ([], [])  # stores left- and right-hand side of '='
        obj_attrs = ([], [])  # "

        for iside, arglist in enumerate((self.lhslist, self.rhslist)):
            # lhslist/rhslist is already split by ',' at this point
            for objdef in arglist:
                aliases, option, attrs = [], None, []
                if ":" in objdef:
                    objdef, option = [part.strip() for part in objdef.rsplit(":", 1)]
                if ";" in objdef:
                    objdef, aliases = [part.strip() for part in objdef.split(";", 1)]
                    aliases = [alias.strip() for alias in aliases.split(";") if alias.strip()]
                if "/" in objdef:
                    objdef, attrs = [part.strip() for part in objdef.split("/", 1)]
                    _attrs = []

                    # Should an attribute key is specified, ie. we're working
                    # on a dict, what we want is to lowercase attribute name
                    # as usual but to preserve dict key case as one would
                    # expect:
                    #
                    # set box/MyAttr = {'FooBar': 1}
                    # Created attribute box/myattr [category:None] = {'FooBar': 1}
                    # set box/MyAttr['FooBar'] = 2
                    # Modified attribute box/myattr [category:None] = {'FooBar': 2}
                    for match in (
                        match
                        for part in map(str.strip, attrs.split("/"))
                        if part and (match := _KEY_REGEX.match(part.strip()))
                    ):
                        attr = match.group("attr").lower()
                        # reappend untouched key, if present
                        if match.group("key"):
                            attr += match.group("key")
                        _attrs.append(attr)
                    attrs = _attrs
                # store data
                obj_defs[iside].append({"name": objdef, "option": option, "aliases": aliases})
                obj_attrs[iside].append({"name": objdef, "attrs": attrs})

        # store for future access
        self.lhs_objs = obj_defs[0]
        self.rhs_objs = obj_defs[1]
        self.lhs_objattr = obj_attrs[0]
        self.rhs_objattr = obj_attrs[1]

    def get_object_typeclass(
        self, obj_type: str = "object", typeclass: str = None, method: str = "cmd_create", **kwargs
    ) -> tuple[typing.Optional["Builder"], list[str]]:
        """
        This hook is called by build commands to determine which typeclass to use for a specific purpose. For instance,
        when using dig, the system can use this to autodetect which kind of Room typeclass to use based on where the
        builder is currently located.

        Note: Although intended to be used with typeclasses, as long as this hook returns a class with a create method,
            which accepts the same API as DefaultObject.create(), build commands and other places should take it.

        Args:
            obj_type (str, optional): The type of object that is being created. Defaults to "object". Evennia provides
                "room", "exit", and "character" by default, but this can be extended.
            typeclass (str, optional): The typeclass that was requested by the player. Defaults to None.
                Can also be an actual class.
            method (str, optional): The method that is calling this hook. Defaults to "cmd_create".
                Others are "cmd_dig", "cmd_open", "cmd_tunnel", etc.

        Returns:
            results_tuple (tuple[Optional[Builder], list[str]]): A tuple containing the typeclass to use and a list of
                errors. (which might be empty.)
        """

        found_typeclass = typeclass or self.default_typeclasses.get(obj_type, None)
        if not found_typeclass:
            return None, [f"No typeclass found for object type '{obj_type}'."]

        try:
            type_class = (
                class_from_module(found_typeclass, settings.TYPECLASS_PATHS)
                if isinstance(found_typeclass, str)
                else found_typeclass
            )
            return None, [f"Typeclass '{found_typeclass}' could not be imported."]

        if not hasattr(type_class, "create"):
            return None, [f"Typeclass '{found_typeclass}' is not creatable."]

        return type_class, []

class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
    """
    adding permanent aliases for object

    Usage:
      alias <obj> [= [alias[,alias,alias,...]]]
      alias <obj> =
      alias/category <obj> = [alias[,alias,...]:<category>

    Switches:
      category - requires ending input with :category, to store the
        given aliases with the given category.

    Assigns aliases to an object so it can be referenced by more
    than one name. Assign empty to remove all aliases from object. If
    assigning a category, all aliases given will be using this category.

    Observe that this is not the same thing as personal aliases
    created with the 'nick' command! Aliases set with alias are
    changing the object in question, making those aliases usable
    by everyone.
    """

    key = "@alias"
    aliases = "setobjalias"
    switch_options = ("category",)
    locks = "cmd:perm(setobjalias) or perm(Builder)"

    method_type = "cmd_create"

        """Set the aliases."""

        if not self.lhs:
            string = "Usage: alias <obj> [= [alias[,alias ...]]]"
            return
        objname = self.lhs

        # Find the object to receive aliases
        obj = caller.search(objname)
        if not obj:
            return
        if self.rhs is None:
            # no =, so we just list aliases on object.
            aliases = obj.aliases.all(return_key_and_category=True)
            if aliases:
                caller.msg(
                    "Aliases for %s: %s"
                    % (
                        obj.get_display_name(caller),
                        ", ".join(
                            "'%s'%s"
                            % (alias, "" if category is None else "[category:'%s']" % category)
                            for (alias, category) in aliases
                        ),
                    )
                )
            else:
                caller.msg(f"No aliases exist for '{obj.get_display_name(caller)}'.")
            return

        if not (obj.access(caller, "control") or obj.access(caller, "edit")):
            caller.msg("You don't have permission to do that.")
            return

            # we have given an empty =, so delete aliases
            old_aliases = obj.aliases.all()
            if old_aliases:
                    "Cleared aliases from %s: %s"
                    % (obj.get_display_name(caller), ", ".join(old_aliases))
                )
                obj.aliases.clear()
            else:
                caller.msg("No aliases to clear.")
            return

        category = None
        if "category" in self.switches:
            if ":" in self.rhs:
                rhs, category = self.rhs.rsplit(":", 1)
                category = category.strip()
            else:
                    "If specifying the /category switch, the category must be given "
                    "as :category at the end."
                )
        else:
            rhs = self.rhs

        # merge the old and new aliases (if any)
        old_aliases = obj.aliases.get(category=category, return_list=True)
        new_aliases = [alias.strip().lower() for alias in rhs.split(",") if alias.strip()]

        # make the aliases only appear once
        old_aliases.extend(new_aliases)
        aliases = list(set(old_aliases))

        # save back to object.
        obj.aliases.add(aliases, category=category)

        # we need to trigger this here, since this will force
        # (default) Exits to rebuild their Exit commands with the new
        # aliases
        obj.at_cmdset_get(force_init=True)

        # report all aliases on the object
        caller.msg(
            "Alias(es) for '%s' set to '%s'%s."
            % (
                obj.get_display_name(caller),
                str(obj.aliases),
                " (category: '%s')" % category if category else "",
            )
        )

class CmdCopy(ObjManipCommand):
    """
    copy an object and its properties

    Usage:
      copy <original obj> [= <new_name>][;alias;alias..]
      [:<new_location>] [,<new_name2> ...]

    Create one or more copies of an object. If you don't supply any targets,
    one exact copy of the original object will be created with the name *_copy.
    """

    key = "@copy"
    locks = "cmd:perm(copy) or perm(Builder)"

        """Uses ObjManipCommand.parse()"""

                "Usage: copy <obj> [=<new_name>[;alias;alias..]]"
                "[:<new_location>] [, <new_name2>...]"
            )
            return

            # this has no target =, so an identical new object is created.
            from_obj_name = self.args
            from_obj = caller.search(from_obj_name)
            if not from_obj:
                return
            to_obj_name = "%s_copy" % from_obj_name
            to_obj_aliases = ["%s_copy" % alias for alias in from_obj.aliases.all()]
            copiedobj = ObjectDB.objects.copy_object(
                from_obj, new_key=to_obj_name, new_aliases=to_obj_aliases
            )
            if copiedobj:
                string = "Identical copy of %s, named '%s' was created." % (
                    from_obj_name,
                    to_obj_name,
                )
            else:
                string = "There was an error copying %s."
        else:
            # we have specified =. This might mean many object targets
            from_obj_name = self.lhs_objs[0]["name"]
                return
            for objdef in self.rhs_objs:
                # loop through all possible copy-to targets
                to_obj_name = objdef["name"]
                to_obj_aliases = objdef["aliases"]
                to_obj_location = objdef["option"]
                if to_obj_location:
                    to_obj_location = caller.search(to_obj_location, global_search=True)
                    if not to_obj_location:
                        return

                copiedobj = ObjectDB.objects.copy_object(
                    from_obj,
                    new_key=to_obj_name,
                    new_location=to_obj_location,
                    new_aliases=to_obj_aliases,
                )
                if copiedobj:
                    string = (
                        f"Copied {from_obj_name} to '{to_obj_name}' (aliases: {to_obj_aliases})."
                    )
                else:
                    string = f"There was an error copying {from_obj_name} to '{to_obj_name}'."
        # we are done, echo to user
        caller.msg(string)

class CmdCpAttr(ObjManipCommand):
    """
    copy attributes between objects

    Usage:
      cpattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
      cpattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...]
      cpattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
      cpattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]

    Switches:
      move - delete the attribute from the source object after copying.

    Example:
      cpattr coolness = Anna/chillout, Anna/nicety, Tom/nicety
      ->
      copies the coolness attribute (defined on yourself), to attributes
      on Anna and Tom.

    Copy the attribute one object to one or more attributes on another object.
    If you don't supply a source object, yourself is used.
    """

    key = "@cpattr"
    switch_options = ("move",)
    locks = "cmd:perm(cpattr) or perm(Builder)"

    def check_from_attr(self, obj, attr, clear=False):
        """
        Hook for overriding on subclassed commands. Checks to make sure a
        caller can copy the attr from the object in question. If not, return a
        false value and the command will abort. An error message should be
        provided by this function.

        If clear is True, user is attempting to move the attribute.
        """

    def check_to_attr(self, obj, attr):
        """
        caller can write to the specified attribute on the specified object.
        If not, return a false value and the attribute will be skipped. An
        error message should be provided by this function.
        """

    def check_has_attr(self, obj, attr):
        """
        Hook for overriding on subclassed commands. Do any preprocessing
        required and verify an object has an attribute.
        """
        if not obj.attributes.has(attr):
            self.msg(f"{obj.name} doesn't have an attribute {attr}.")

    def get_attr(self, obj, attr):
        """
        required and get the attribute from the object.
        """
        return obj.attributes.get(attr)

        """
        Do the copying.
        """

            string = """Usage:
            cpattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
            cpattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...]
            cpattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
            cpattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]"""
            return

        lhs_objattr = self.lhs_objattr
        to_objs = self.rhs_objattr
        from_obj_name = lhs_objattr[0]["name"]
        from_obj_attrs = lhs_objattr[0]["attrs"]

        if not from_obj_attrs:
            # this means the from_obj_name is actually an attribute
            # name on self.
            from_obj_attrs = [from_obj_name]
            from_obj = self.caller
        else:
        if not from_obj or not to_objs:
            caller.msg("You have to supply both source object and target(s).")
            return
        # copy to all to_obj:ects
        if "move" in self.switches:
            clear = True
        else:
            clear = False
        if not self.check_from_attr(from_obj, from_obj_attrs[0], clear=clear):
            return

        for attr in from_obj_attrs:
            if not self.check_has_attr(from_obj, attr):
                return

        if (len(from_obj_attrs) != len(set(from_obj_attrs))) and clear:
            self.msg("|RCannot have duplicate source names when moving!")
            return

        result = []

        for to_obj in to_objs:
            to_obj_name = to_obj["name"]
            to_obj_attrs = to_obj["attrs"]
            to_obj = caller.search(to_obj_name)
            if not to_obj:
                result.append(f"\nCould not find object '{to_obj_name}'")
                continue
            for inum, from_attr in enumerate(from_obj_attrs):
                try:
                    to_attr = to_obj_attrs[inum]
                except IndexError:
                    # if there are too few attributes given
                    # on the to_obj, we copy the original name instead.
                    to_attr = from_attr
                if not self.check_to_attr(to_obj, to_attr):
                    continue
                value = self.get_attr(from_obj, from_attr)
                to_obj.attributes.add(to_attr, value)
                if clear and not (from_obj == to_obj and from_attr == to_attr):
                    from_obj.attributes.remove(from_attr)
                    result.append(
                        f"\nMoved {from_obj.name}.{from_attr} -> {to_obj_name}.{to_attr}. (value:"
                        f" {repr(value)})"
                    )
                else:
                        f"\nCopied {from_obj.name}.{from_attr} -> {to_obj.name}.{to_attr}. (value:"
                    )
        caller.msg("".join(result))

class CmdMvAttr(ObjManipCommand):
    """
    move attributes between objects

    Usage:
      mvattr[/switch] <obj>/<attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
      mvattr[/switch] <obj>/<attr> = <obj1> [,<obj2>,<obj3>,...]
      mvattr[/switch] <attr> = <obj1>/<attr1> [,<obj2>/<attr2>,<obj3>/<attr3>,...]
      mvattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]

    Switches:
      copy - Don't delete the original after moving.

    Move an attribute from one object to one or more attributes on another
    object. If you don't supply a source object, yourself is used.
    """

    key = "@mvattr"
    switch_options = ("copy",)
    locks = "cmd:perm(mvattr) or perm(Builder)"

        """
        Do the moving
        """
      mvattr[/switch] <attr> = <obj1>[,<obj2>,<obj3>,...]"""
            return

        # simply use cpattr for all the functionality
        if "copy" in self.switches:
            self.execute_cmd("cpattr %s" % self.args)
        else:
            self.execute_cmd("cpattr/move %s" % self.args)

class CmdCreate(ObjManipCommand):
    """
    create new objects

    Usage:
      create[/drop] <objname>[;alias;alias...][:typeclass], <objname>...

    switch:
       drop - automatically drop the new object into your current
              location (this is not echoed). This also sets the new
              object's home to the current location rather than to you.

    Creates one or more new objects. If typeclass is given, the object
    is created as a child of this typeclass. The typeclass script is
    assumed to be located under types/ and any further
    directory structure is given in Python notation. So if you have a
    correct typeclass 'RedButton' defined in
    types/examples/red_button.py, you could create a new
    object of this type like this:

       create/drop button;red : examples.red_button.RedButton

    """

    key = "@create"
    switch_options = ("drop",)
    locks = "cmd:perm(create) or perm(Builder)"

        """
        Creates the object.
        """

            string = "Usage: create[/drop] <newname>[;alias;alias...] [:typeclass.path]"
            return

        # create the objects
        for objdef in self.lhs_objs:
            string = ""
            name = objdef["name"]
            aliases = objdef["aliases"]

            obj_typeclass, errors = self.get_object_typeclass(
                obj_type="object", typeclass=objdef["option"]
            )
            if errors:
                self.msg(errors)
            if not obj_typeclass:
                continue

            obj, errors = obj_typeclass.create(
                name, caller, home=caller, aliases=aliases, report_to=caller, caller=caller
            )
            if errors:
            if not obj:
                continue

                string = (
                    f"You create a new {obj.typename}: {obj.name} (aliases: {', '.join(aliases)})."
                )
            else:
                string = f"You create a new {obj.typename}: {obj.name}."
            # set a default desc
            if not obj.db.desc:
                obj.db.desc = "You see nothing special."
            if "drop" in self.switches:
                if caller.location:
                    obj.home = caller.location
                    obj.move_to(caller.location, quiet=True, move_type="drop")
        if string:

def _desc_load(caller):
    return caller.db.evmenu_target.db.desc or ""

def _desc_save(caller, buf):
    """
    Save line buffer to the desc prop. This should
    return True if successful and also report its status to the user.
    """
    caller.db.evmenu_target.db.desc = buf
    caller.msg("Saved.")

def _desc_quit(caller):
    caller.attributes.remove("evmenu_target")
    caller.msg("Exited editor.")

class CmdDesc(COMMAND_DEFAULT_CLASS):
    """
    describe an object or the current room.

    Usage:
      desc [<obj> =] <description>

    Switches:
      edit - Open up a line editor for more advanced editing.

    Sets the "desc" attribute on an object. If an object is not given,
    describe the current room.
    """

    key = "@desc"
    switch_options = ("edit",)
    locks = "cmd:perm(desc) or perm(Builder)"

    def edit_handler(self):
        if self.rhs:
            self.msg("|rYou may specify a value, or use the edit switch, but not both.|n")
            return
            obj = self.caller.search(self.args)
        else:
            obj = self.caller.location or self.msg("|rYou can't describe oblivion.|n")
        if not obj:
            return

        if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")):
            self.msg(f"You don't have permission to edit the description of {obj.key}.")
            return

        self.caller.db.evmenu_target = obj
        # launch the editor
        EvEditor(
            self.caller,
            loadfunc=_desc_load,
            savefunc=_desc_save,
            quitfunc=_desc_quit,
            key="desc",
            persistent=True,
        )
        return

        """Define command"""

        if not self.args and "edit" not in self.switches:
            caller.msg("Usage: desc [<obj> =] <description>")
            return

        if "edit" in self.switches:
            self.edit_handler()
            return

        if "=" in self.args:
            # We have an =
            obj = caller.search(self.lhs)
            if not obj:
                return
            desc = self.rhs or ""
        else:
            obj = caller.location or self.msg("|rYou don't have a location to describe.|n")
            if not obj:
                return
            desc = self.args
        if obj.access(self.caller, "control") or obj.access(self.caller, "edit"):
            obj.db.desc = desc
            caller.msg(f"The description was set on {obj.get_display_name(caller)}.")
        else:
            caller.msg(f"You don't have permission to edit the description of {obj.key}.")

class CmdDestroy(COMMAND_DEFAULT_CLASS):
    """
    permanently delete objects

    Usage:
       destroy[/switches] [obj, obj2, obj3, [dbref-dbref], ...]

    Switches:
       override - The destroy command will usually avoid accidentally
                  destroying account objects. This switch overrides this safety.
       force - destroy without confirmation.
    Examples:
       destroy house, roof, door, 44-78
       destroy 5-10, flower, 45
       destroy/force north

    Destroys one or many objects. If dbrefs are used, a range to delete can be
    given, e.g. 4-10. Also the end points will be deleted. This command
    displays a confirmation before destroying, to make sure of your choice.
    You can specify the /force switch to bypass this confirmation.
    """

    key = "@destroy"
    aliases = ["@delete", "@del"]
    switch_options = ("override", "force")
    locks = "cmd:perm(destroy) or perm(Builder)"

    confirm = True  # set to False to always bypass confirmation
    default_confirm = "yes"  # what to assume if just pressing enter (yes/no)

        """Implements the command."""

        delete = True

        if not self.args or not self.lhslist:
            caller.msg("Usage: destroy[/switches] [obj, obj2, obj3, [dbref-dbref],...]")
            delete = False

        def delobj(obj):
            # helper function for deleting a single object
            string = ""
            if not obj.pk:
                string = f"\nObject {obj.db_key} was already deleted."
            else:
                objname = obj.name
                if not (obj.access(caller, "control") or obj.access(caller, "delete")):
                    return f"\nYou don't have permission to delete {objname}."
                if obj.account and "override" not in self.switches:
                    return (
                        f"\nObject {objname} is controlled by an active account. Use /override to"
                        " delete anyway."
                    )
                if obj.dbid == int(settings.DEFAULT_HOME.lstrip("#")):
                    return (
                        f"\nYou are trying to delete |c{objname}|n, which is set as DEFAULT_HOME. "
                        "Re-point settings.DEFAULT_HOME to another "
                        "object before continuing."
                    )

                # check if object to delete had exits or objects inside it
                obj_exits = obj.exits if hasattr(obj, "exits") else ()
                obj_contents = obj.contents if hasattr(obj, "contents") else ()
                had_exits = bool(obj_exits)
                had_objs = any(entity for entity in obj_contents if entity not in obj_exits)

                # do the deletion
                okay = obj.delete()
                if not okay:
                    string += (
                        f"\nERROR: {objname} not deleted, probably because delete() returned False."
                    )
                else:
                    string += f"\n{objname} was destroyed."
                    if had_exits:
                        string += f" Exits to and from {objname} were destroyed as well."
                    if had_objs:
                        string += f" Objects inside {objname} were moved to their homes."
            return string

        objs = []
        for objname in self.lhslist:
            if not delete:
                continue

            if "-" in objname:
                # might be a range of dbrefs
                dmin, dmax = [utils.dbref(part, reqhash=False) for part in objname.split("-", 1)]
                if dmin and dmax:
                    for dbref in range(int(dmin), int(dmax + 1)):
                        obj = caller.search("#" + str(dbref))
                        if obj:
                            objs.append(obj)
                    continue
                else:
                    obj = caller.search(objname)
            else:
                obj = caller.search(objname)

            if obj is None:
                self.msg(
                    " (Objects to destroy must either be local or specified with a unique #dbref.)"
                )
            elif obj not in objs:
                objs.append(obj)

        if objs and ("force" not in self.switches and type(self).confirm):
            confirm = "Are you sure you want to destroy "
            if len(objs) == 1:
                confirm += objs[0].get_display_name(caller)
            elif len(objs) < 5:
                confirm += ", ".join([obj.get_display_name(caller) for obj in objs])
            else:
                confirm += ", ".join(["#{}".format(obj.id) for obj in objs])
            confirm += " [yes]/no?" if self.default_confirm == "yes" else " yes/[no]"
            answer = ""
            answer = yield (confirm)
            answer = self.default_confirm if answer == "" else answer

            if answer and answer not in ("yes", "y", "no", "n"):
                    "Canceled: Either accept the default by pressing return or specify yes/no."
                )
                delete = False
            elif answer.strip().lower() in ("n", "no"):
                caller.msg("Canceled: No object was destroyed.")

        if delete:
            results = []
                results.append(delobj(obj))

            if results:
                caller.msg("".join(results).strip())

class CmdDig(ObjManipCommand):
    """
    build new rooms and connect them to the current location

    Usage:
      dig[/switches] <roomname>[;alias;alias...][:typeclass]
            [= <exit_to_there>[;alias][:typeclass]]
               [, <exit_to_here>[;alias][:typeclass]]

    Switches:
       tel or teleport - move yourself to the new room

    Examples:
       dig kitchen = north;n, south;s
       dig house:myrooms.MyHouseTypeclass
       dig sheer cliff;cliff;sheer = climb up, climb down

    This command is a convenient way to build rooms quickly; it creates the
    new room and you can optionally set up exits back and forth between your
    current room and the new one. You can add as many aliases as you
    like to the name of the room and the exits in question; an example
    would be 'north;no;n'.
    """

    key = "@dig"
    switch_options = ("teleport",)
    locks = "cmd:perm(dig) or perm(Builder)"

    method_type = "cmd_dig"

    # lockstring of newly created rooms, for easy overloading.
    # Will be formatted with the {id} of the creating object.
    new_room_lockstring = (
        "control:id({id}) or perm(Admin); "
        "delete:id({id}) or perm(Admin); "
        "edit:id({id}) or perm(Admin)"
    )

        """Do the digging. Inherits variables from ObjManipCommand.parse()"""

            string = "Usage: dig[/teleport] <roomname>[;alias;alias...][:parent] [= <exit_there>"
            string += "[;alias;alias..][:parent]] "
            string += "[, <exit_back_here>[;alias;alias..][:parent]]"
            return

        room = self.lhs_objs[0]

        if not room["name"]:
            caller.msg("You must supply a new room name.")
            return
        location = caller.location

        # Create the new room
        room_typeclass, errors = self.get_object_typeclass(
            obj_type="room", typeclass=room["option"], method=self.method_type
        )
        if errors:
            self.msg("|rError creating room:|n %s" % errors)
        if not room_typeclass:
            return

        # create room
        new_room, errors = room_typeclass.create(
            room["name"],
            aliases=room["aliases"],
            report_to=caller,
            caller=caller,
            method=self.method_type,
        )
        if errors:
        if not new_room:
            return

        alias_string = ""
        if new_room.aliases.all():
            alias_string = " (%s)" % ", ".join(new_room.aliases.all())

        room_string = f"Created room {new_room}({new_room.dbref}){alias_string} of type {new_room}."

        # create exit to room

        exit_to_string = ""
        exit_back_string = ""

        if self.rhs_objs:
            to_exit = self.rhs_objs[0]
            if not to_exit["name"]:
                exit_to_string = "\nNo exit created to new room."
            elif not location:
                exit_to_string = "\nYou cannot create an exit from a None-location."
            else:
                # Build the exit to the new room from the current one
                exit_typeclass, errors = self.get_object_typeclass(
                    obj_type="exit", typeclass=to_exit["option"], method=self.method_type
                )
                if errors:
                    self.msg("|rError creating exit:|n %s" % errors)
                if not exit_typeclass:
                    return

                new_to_exit, errors = exit_typeclass.create(
                    to_exit["name"],
                    location=location,
                    destination=new_room,
                    aliases=to_exit["aliases"],
                    report_to=caller,
                    caller=caller,
                    method=self.method_type,
                )
                if errors:
                if not new_to_exit:
                    return

                alias_string = ""
                if new_to_exit.aliases.all():
                    alias_string = " (%s)" % ", ".join(new_to_exit.aliases.all())
                exit_to_string = (
                    f"\nCreated Exit from {location.name} to {new_room.name}:"
                    f" {new_to_exit}({new_to_exit.dbref}){alias_string}."
                )

        # Create exit back from new room

        if len(self.rhs_objs) > 1:
            # Building the exit back to the current room
            back_exit = self.rhs_objs[1]
            if not back_exit["name"]:
                exit_back_string = "\nNo back exit created."
                exit_back_string = "\nYou cannot create an exit back to a None-location."
            else:
                    obj_type="exit", typeclass=back_exit["option"], method=self.method_type
                )
                if errors:
                    return
                new_back_exit, errors = exit_typeclass.create(
                    back_exit["name"],
                    location=new_room,
                    destination=location,
                    aliases=back_exit["aliases"],
                )
                if errors:
                if not new_back_exit:
                    return
                if new_back_exit.aliases.all():
                    alias_string = " (%s)" % ", ".join(new_back_exit.aliases.all())
                exit_back_string = (
                    f"\nCreated Exit back from {new_room.name} to {location.name}:"
                    f" {new_back_exit}({new_back_exit.dbref}){alias_string}."
                )
        caller.msg(f"{room_string}{exit_to_string}{exit_back_string}")
        if new_room and "teleport" in self.switches:
            caller.move_to(new_room, move_type="teleport")

class CmdTunnel(COMMAND_DEFAULT_CLASS):
    """
    create new rooms in cardinal directions only

    Usage:
      tunnel[/switch] <direction>[:typeclass] [= <roomname>[;alias;alias;...][:typeclass]]

    Switches:
      oneway - do not create an exit back to the current location
      tel - teleport to the newly created room

    Example:
      tunnel n
      tunnel n = house;mike's place;green building

    This is a simple way to build using pre-defined directions:
     |wn,ne,e,se,s,sw,w,nw|n (north, northeast etc)
     |wu,d|n (up and down)
     |wi,o|n (in and out)
    The full names (north, in, southwest, etc) will always be put as
    main name for the exit, using the abbreviation as an alias (so an
    exit will always be able to be used with both "north" as well as
    "n" for example). Opposite directions will automatically be
    created back from the new room unless the /oneway switch is given.
    For more flexibility and power in creating rooms, use dig.
    """

    key = "@tunnel"
    aliases = ["@tun"]
    switch_options = ("oneway", "tel")
    locks = "cmd: perm(tunnel) or perm(Builder)"

    method_type = "cmd_tunnel"

    # store the direction, full name and its opposite
    directions = {
        "n": ("north", "s"),
        "ne": ("northeast", "sw"),
        "e": ("east", "w"),
        "se": ("southeast", "nw"),
        "s": ("south", "n"),
        "sw": ("southwest", "ne"),
        "w": ("west", "e"),
        "nw": ("northwest", "se"),
        "u": ("up", "d"),
        "d": ("down", "u"),
        "i": ("in", "o"),
        "o": ("out", "i"),
    }

        """Implements the tunnel command"""

        if not self.args or not self.lhs:
            string = (
                "Usage: tunnel[/switch] <direction>[:typeclass] [= <roomname>"
                "[;alias;alias;...][:typeclass]]"
            )
            return

        # If we get a typeclass, we need to get just the exitname
        exitshort = self.lhs.split(":")[0]

        if exitshort not in self.directions:
            string = "tunnel can only understand the following directions: %s." % ",".join(
                sorted(self.directions.keys())
            )
            string += "\n(use dig for more freedom)"
            return

        # retrieve all input and parse it
        exitname, backshort = self.directions[exitshort]
        backname = self.directions[backshort][0]

        # if we received a typeclass for the exit, add it to the alias(short name)
        if ":" in self.lhs:
            # limit to only the first : character
            exit_typeclass = ":" + self.lhs.split(":", 1)[-1]
            # exitshort and backshort are the last part of the exit strings,
            # so we add our typeclass argument after
            exitshort += exit_typeclass
            backshort += exit_typeclass

        roomname = "Some place"
            roomname = self.rhs  # this may include aliases; that's fine.

        telswitch = ""
        if "tel" in self.switches:
            telswitch = "/teleport"
        backstring = ""
        if "oneway" not in self.switches:
            backstring = f", {backname};{backshort}"

        # build the string we will use to call dig
        digstring = f"dig{telswitch} {roomname} = {exitname};{exitshort}{backstring}"
        self.execute_cmd(digstring)

class CmdLink(COMMAND_DEFAULT_CLASS):
    """
    link existing rooms together with exits

    Usage:
      link[/switches] <object> = <target>
      link[/switches] <object> =
      link[/switches] <object>

    Switch:
      twoway - connect two exits. For this to work, BOTH <object>
               and <target> must be exit objects.

    If <object> is an exit, set its destination to <target>. Two-way operation
    instead sets the destination to the *locations* of the respective given
    arguments.
    The second form (a lone =) sets the destination to None (same as
    the unlink command) and the third form (without =) just shows the
    currently set destination.
    """

    key = "@link"
    locks = "cmd:perm(link) or perm(Builder)"

        """Perform the link"""

            caller.msg("Usage: link[/twoway] <object> = <target>")
            return

        object_name = self.lhs

        # try to search locally first
        results = caller.search(object_name, quiet=True)
        if len(results) > 1:  # local results was a multimatch. Inform them to be more specific
            _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
            return _AT_SEARCH_RESULT(results, caller, query=object_name)
        elif len(results) == 1:  # A unique local match
            obj = results[0]
        else:  # No matches. Search globally
            obj = caller.search(object_name, global_search=True)
            if not obj:
                return

            # this means a target name was given
            target = caller.search(self.rhs, global_search=True)
            if not target:
                return

            if target == obj:
                self.msg("Cannot link an object to itself.")
                return

            string = ""
            note = (
                "Note: %s(%s) did not have a destination set before. Make sure you linked the right"
                " thing."
            )
            if not obj.destination:
                string = note % (obj.name, obj.dbref)
            if "twoway" in self.switches:
                if not (target.location and obj.location):
                    string = (
                        f"To create a two-way link, {obj} and {target} must both have a location"
                    )
                    string += " (i.e. they cannot be rooms, but should be exits)."
                    self.msg(string)
                    return
                if not target.destination:
                    string += note % (target.name, target.dbref)
                obj.destination = target.location
                target.destination = obj.location
                string += (
                    f"\nLink created {obj.name} (in {obj.location}) <-> {target.name} (in"
                    f" {target.location}) (two-way)."
                )
            else:
                obj.destination = target
                string += f"\nLink created {obj.name} -> {target} (one way)."

        elif self.rhs is None:
            # this means that no = was given (otherwise rhs
            # would have been an empty string). So we inspect
            # the home/destination on object
            dest = obj.destination
            if dest:
                string = f"{obj.name} is an exit to {dest.name}."
            else:
                string = f"{obj.name} is not an exit. Its home location is {obj.home}."

        else:
            # We gave the command link 'obj = ' which means we want to
            # clear destination.
            if obj.destination:
                obj.destination = None
                string = f"Former exit {obj.name} no longer links anywhere."
            else:
                string = f"{obj.name} had no destination to unlink."
        # give feedback
        caller.msg(string.strip())

class CmdUnLink(CmdLink):
    """
    remove exit-connections between rooms

    Usage:
      unlink <Object>

    Unlinks an object, for example an exit, disconnecting
    it from whatever it was connected to.
    """

    # this is just a child of CmdLink

    key = "unlink"
    locks = "cmd:perm(unlink) or perm(Builder)"
    help_key = "Building"

        """
        All we need to do here is to set the right command
        and call func in CmdLink
        """

            caller.msg("Usage: unlink <object>")
            return

        # This mimics 'link <obj> = ' which is the same as unlink
        self.rhs = ""

        # call the link functionality
        super().func()

class CmdSetHome(CmdLink):
    """
    set an object's home location

    Usage:
      sethome <obj> [= <home_location>]
      sethome <obj>

    The "home" location is a "safety" location for objects; they
    will be moved there if their current location ceases to exist. All
    objects should always have a home location for this reason.
    It is also a convenient target of the "home" command.

    If no location is given, just view the object's home location.
    """

    key = "@sethome"
    locks = "cmd:perm(sethome) or perm(Builder)"

        """implement the command"""
            string = "Usage: sethome <obj> [= <home_location>]"
            return

        obj = self.caller.search(self.lhs, global_search=True)
        if not obj:
            return
            # just view
            home = obj.home
            if not home:
                string = "This object has no home location set!"
            else:
                string = f"{obj}'s current home is {home}({home.dbref})."
        else:
            # set a home location
            new_home = self.caller.search(self.rhs, global_search=True)
            if not new_home:
                return
            old_home = obj.home
            obj.home = new_home
            if old_home:
                string = (
                    f"Home location of {obj} was changed from {old_home}({old_home.dbref} to"
                    f" {new_home}({new_home.dbref})."
                )
            else:
                string = f"Home location of {obj} was set to {new_home}({new_home.dbref})."

class CmdListCmdSets(COMMAND_DEFAULT_CLASS):
    """
    list command sets defined on an object

    Usage:
      cmdsets <obj>

    This displays all cmdsets assigned
    to a user. Defaults to yourself.
    """

    key = "@cmdsets"
    locks = "cmd:perm(listcmdsets) or perm(Builder)"

        """list the cmdsets"""

        if self.arglist:
            obj = caller.search(self.arglist[0])
            if not obj:
                return
        else:
            obj = caller
        string = f"{obj.cmdset}"

class CmdName(ObjManipCommand):
    """
    change the name and/or aliases of an object

    Usage:
      name <obj> = <newname>;alias1;alias2

    Rename an object to something new. Use *obj to
    rename an account.

    """

    key = "@name"
    aliases = ["@rename"]
    locks = "cmd:perm(rename) or perm(Builder)"

        """change the name"""

            caller.msg("Usage: name <obj> = <newname>[;alias;alias;...]")
            return

        obj = None
        if self.lhs_objs:
            objname = self.lhs_objs[0]["name"]
            if objname.startswith("*"):
                # account mode
                obj = caller.account.search(objname.lstrip("*"))
                if obj:
                    if self.rhs_objs[0]["aliases"]:
                        caller.msg("Accounts can't have aliases.")
                        return
                    newname = self.rhs
                    if not newname:
                        caller.msg("No name defined!")
                        return
                    if not (obj.access(caller, "control") or obj.access(caller, "edit")):
                        caller.msg(f"You don't have right to edit this account {obj}.")
                        return
                    obj.username = newname
                    obj.save()
                    caller.msg(f"Account's name changed to '{newname}'.")
                    return
            # object search, also with *
            obj = caller.search(objname)
            if not obj:
                return
            newname = self.rhs_objs[0]["name"]
            aliases = self.rhs_objs[0]["aliases"]
        else:
            newname = self.rhs
            aliases = None
        if not newname and not aliases:
            caller.msg("No names or aliases defined!")
            return
            caller.msg(f"You don't have the right to edit {obj}.")
            return
        # change the name and set aliases:
        if newname:
            obj.key = newname
        astring = ""
        if aliases:
            [obj.aliases.add(alias) for alias in aliases]
            astring = " (%s)" % ", ".join(aliases)
        # fix for exits - we need their exit-command to change name too
        if obj.destination:
            obj.flush_from_cache(force=True)
        caller.msg(f"Object's name changed to '{newname}'{astring}.")

class CmdOpen(ObjManipCommand):
    """
    open a new exit from the current room

    Usage:
      open <new exit>[;alias;alias..][:typeclass] [,<return exit>[;alias;..][:typeclass]]] = <destination>

    Handles the creation of exits. If a destination is given, the exit
    will point there. The <return exit> argument sets up an exit at the
    destination leading back to the current room. Destination name
    can be given both as a #dbref and a name, if that name is globally
    unique.

    """

    key = "@open"
    locks = "cmd:perm(open) or perm(Builder)"

    method_type = "cmd_open"

    new_obj_lockstring = "control:id({id}) or perm(Admin);delete:id({id}) or perm(Admin)"

    # a custom member method to chug out exits and do checks
    def create_exit(self, exit_name, location, destination, exit_aliases=None, typeclass=None):
        """
        Helper function to avoid code duplication.
        At this point we know destination is a valid location

        """
        string = ""
        # check if this exit object already exists at the location.
        # we need to ignore errors (so no automatic feedback)since we
        # have to know the result of the search to decide what to do.
        exit_obj = caller.search(exit_name, location=location, quiet=True, exact=True)
        if len(exit_obj) > 1:
            # give error message and return
            caller.search(exit_name, location=location, exact=True)
        if exit_obj:
            exit_obj = exit_obj[0]
            if not exit_obj.destination:
                # we are trying to link a non-exit
                    f"'{exit_name}' already exists and is not an exit!\nIf you want to convert it "
                    "to an exit, you must assign an object to the 'destination' property first."
                )
                return None
            # we are re-linking an old exit.
            old_destination = exit_obj.destination
            if old_destination:
                string = f"Exit {exit_name} already exists."
                if old_destination.id != destination.id:
                    # reroute the old exit.
                    exit_obj.destination = destination
                    if exit_aliases:
                        [exit_obj.aliases.add(alias) for alias in exit_aliases]
                    string += (
                        f" Rerouted its old destination '{old_destination.name}' to"
                        f" '{destination.name}' and changed aliases."
                    )
                else:
                    string += " It already points to the correct place."

        else:
            # exit does not exist before. Create a new one.
            exit_typeclass, errors = self.get_object_typeclass(
                obj_type="exit", typeclass=typeclass, method=self.method_type
            )
            if errors:
                self.msg("|rError creating exit:|n %s" % errors)
            if not exit_typeclass:
                return
            exit_obj, errors = exit_typeclass.create(
                exit_name,
                location=location,
                aliases=exit_aliases,
                report_to=caller,
                caller=caller,
                method=self.method_type,
            )
            if errors:
            if not exit_obj:
                return
            if exit_obj:
                # storing a destination is what makes it an exit!
                exit_obj.destination = destination
                string = (
                    ""
                    if not exit_aliases
                    else " (aliases: %s)" % ", ".join([str(e) for e in exit_aliases])
                )
                string = (
                    f"Created new Exit '{exit_name}' from {location.name} to"
                    f" {destination.name}{string}."
                )
            else:
                string = f"Error: Exit '{exit.name}' not created."
        # emit results
        return exit_obj

        self.location = self.caller.location
        if not self.args or not self.rhs:
            self.msg(
                "Usage: open <new exit>[;alias...][:typeclass]"
                "[,<return exit>[;alias..][:typeclass]]] "
                "= <destination>"
            )
            raise InterruptCommand
        if not self.location:
            self.msg("You cannot create an exit from a None-location.")
        self.destination = self.caller.search(self.rhs, global_search=True)
        if not self.destination:
        self.exit_name = self.lhs_objs[0]["name"]
        self.exit_aliases = self.lhs_objs[0]["aliases"]
        self.exit_typeclass = self.lhs_objs[0]["option"]

        """
        This is where the processing starts.
        Uses the ObjManipCommand.parser() for pre-processing
        as well as the self.create_exit() method.
        """
        # Create exit
        ok = self.create_exit(
            self.exit_name, self.location, self.destination, self.exit_aliases, self.exit_typeclass
        )
        if not ok:
            # an error; the exit was not created, so we quit.
            return
        # Create back exit, if any
        if len(self.lhs_objs) > 1:
            back_exit_name = self.lhs_objs[1]["name"]
            back_exit_aliases = self.lhs_objs[1]["aliases"]
            back_exit_typeclass = self.lhs_objs[1]["option"]
            self.create_exit(
                back_exit_name,
                self.destination,
                self.location,
                back_exit_aliases,
                back_exit_typeclass,
            )

def _convert_from_string(cmd, strobj):
    """
    Converts a single object in *string form* to its equivalent python
    type.

     Python earlier than 2.6:
    Handles floats, ints, and limited nested lists and dicts
    (can't handle lists in a dict, for example, this is mainly due to
    the complexity of parsing this rather than any technical difficulty -
    if there is a need for set-ing such complex structures on the
    command line we might consider adding it).
     Python 2.6 and later:
    Supports all Python structures through literal_eval as long as they
    are valid Python syntax. If they are not (such as [test, test2], ie
    without the quotes around the strings), the entire structure will
    be converted to a string and a warning will be given.

    We need to convert like this since all data being sent over the
    telnet connection by the Account is text - but we will want to
    store it as the "real" python type so we can do convenient
    comparisons later (e.g.  obj.db.value = 2, if value is stored as a
    string this will always fail).
    """

    # Use literal_eval to parse python structure exactly.
    try:
        return _LITERAL_EVAL(strobj)
    except (SyntaxError, ValueError):
        # treat as string
        strobj = utils.to_str(strobj)
        string = (
            f'|RNote: name "|r{strobj}|R" was converted to a string. Make sure this is acceptable.'
        )
        cmd.caller.msg(string)
        return strobj
    except Exception as err:
        string = f"|RUnknown error in evaluating Attribute: {err}"
        return string

class CmdSetAttribute(ObjManipCommand):
    """
    set attribute on an object or account

    Usage:
      set[/switch] <obj>/<attr>[:category] = <value>
      set[/switch] <obj>/<attr>[:category] =                  # delete attribute
      set[/switch] <obj>/<attr>[:category]                    # view attribute
      set[/switch] *<account>/<attr>[:category] = <value>

    Switch:
        edit: Open the line editor (string values only)
        script: If we're trying to set an attribute on a script
        channel: If we're trying to set an attribute on a channel
        account: If we're trying to set an attribute on an account
        room: Setting an attribute on a room (global search)
        exit: Setting an attribute on an exit (global search)
        char: Setting an attribute on a character (global search)
        character: Alias for char, as above.

    Example:
        set self/foo = "bar"
        set/delete self/foo
        set self/foo = $dbref(#53)

    Sets attributes on objects. The second example form above clears a
    previously set attribute while the third form inspects the current value of
    the attribute (if any). The last one (with the star) is a shortcut for
    operating on a player Account rather than an Object.

    If you want <value> to be an object, use $dbef(#dbref) or
    $search(key) to assign it. You need control or edit access to
    the object you are adding.

    The most common data to save with this command are strings and
    numbers. You can however also set Python primitives such as lists,
    dictionaries and tuples on objects (this might be important for
    the functionality of certain custom objects).  This is indicated
    by you starting your value with one of |c'|n, |c"|n, |c(|n, |c[|n
    or |c{ |n.

    Once you have stored a Python primitive as noted above, you can include
    |c[<key>]|n in <attr> to reference nested values in e.g. a list or dict.

    Remember that if you use Python primitives like this, you must
    write proper Python syntax too - notably you must include quotes
    around your strings or you will get an error.

    """

    key = "@set"
    locks = "cmd:perm(set) or perm(Builder)"
    nested_re = re.compile(r"\[.*?\]")
    not_found = object()

    def check_obj(self, obj):
        """
        This may be overridden by subclasses in case restrictions need to be
        placed on whether certain objects can have attributes set by certain
        accounts.

        This function is expected to display its own error message.

        Returning False will abort the command.
        """

    def check_attr(self, obj, attr_name, category):
        """
        placed on what attributes can be set by who beyond the normal lock.

        This functions is expected to display its own error message. It is
        run once for every attribute that is checked, blocking only those
        attributes which are not permitted and letting the others through.
        """
        return attr_name

    def split_nested_attr(self, attr):
        """
        Yields tuples of (possible attr name, nested keys on that attr).
        For performance, this is biased to the deepest match, but allows compatibility
        with older attrs that might have been named with `[]`'s.

        > list(split_nested_attr("nested['asdf'][0]"))
        [
            ('nested', ['asdf', 0]),
            ("nested['asdf']", [0]),
            ("nested['asdf'][0]", []),
        ]
        """
        quotes = "\"'"

        def clean_key(val):
            val = val.strip("[]")
            if val[0] in quotes:
                return val.strip(quotes)
            if val[0] == LIST_APPEND_CHAR:
                # List insert/append syntax
                return val
            try:
                return int(val)
                return val

        parts = self.nested_re.findall(attr)

        base_attr = ""
        if parts:
            base_attr = attr[: attr.find(parts[0])]
        for index, part in enumerate(parts):
            yield (base_attr, [clean_key(p) for p in parts[index:]])
            base_attr += part
        yield (attr, [])

    def do_nested_lookup(self, value, *keys):
        result = value
        for key in keys:
            try:
                result = result.__getitem__(key)
            except (IndexError, KeyError, TypeError):
                return self.not_found

    def view_attr(self, obj, attr, category):
        """
        Look up the value of an attribute and return a string displaying it.
        """
        nested = False
        for key, nested_keys in self.split_nested_attr(attr):
            nested = True
            if obj.attributes.has(key):
                val = obj.attributes.get(key)
                val = self.do_nested_lookup(val, *nested_keys)
                if val is not self.not_found:
                    return f"\nAttribute {obj.name}/|w{attr}|n [category:{category}] = {val}"
        error = f"\nAttribute {obj.name}/|w{attr} [category:{category}] does not exist."
        if nested:
            error += " (Nested lookups attempted)"
        return error

    def rm_attr(self, obj, attr, category):
        """
        Remove an attribute from the object, or a nested data structure, and report back.
        """
            if obj.attributes.has(key, category):
                if nested_keys:
                    del_key = nested_keys[-1]
                    val = obj.attributes.get(key, category=category)
                    deep = self.do_nested_lookup(val, *nested_keys[:-1])
                    if deep is not self.not_found:
                        try:
                            del deep[del_key]
                        except (IndexError, KeyError, TypeError):
                            continue
                    return f"\nDeleted attribute {obj.name}/|w{attr}|n [category:{category}]."
                else:
                    exists = obj.attributes.has(key, category)
                    if exists:
                        obj.attributes.remove(attr, category=category)
                        return f"\nDeleted attribute {obj.name}/|w{attr}|n [category:{category}]."
                    else:
                        return (
                            f"\nNo attribute {obj.name}/|w{attr}|n [category: {category}] "
                            "was found to delete."
                        )
        error = f"\nNo attribute {obj.name}/|w{attr}|n [category: {category}] was found to delete."
        if nested:

    def set_attr(self, obj, attr, value, category):
        done = False
            if obj.attributes.has(key, category) and nested_keys:
                acc_key = nested_keys[-1]
                lookup_value = obj.attributes.get(key, category)
                deep = self.do_nested_lookup(lookup_value, *nested_keys[:-1])
                if deep is not self.not_found:
                    # To support appending and inserting to lists
                    # a key that starts with LIST_APPEND_CHAR will insert a new item at that
                    # location, and move the other elements down.
                    # Using LIST_APPEND_CHAR alone will append to the list
                    if isinstance(acc_key, str) and acc_key[0] == LIST_APPEND_CHAR:
                        try:
                            if len(acc_key) > 1:
                                where = int(acc_key[1:])
                                deep.insert(where, value)
                            else:
                                deep.append(value)
                        except (ValueError, AttributeError):
                            pass
                        else:
                            value = lookup_value
                            attr = key
                            done = True
                            break

                    # List magic failed, just use like a key/index
                    try:
                        deep[acc_key] = value
                    except TypeError as err:
                        # Tuples can't be modified
                        return f"\n{err} - {deep}"

                    value = lookup_value
                    attr = key
                    done = True
                    break

        verb = "Modified" if obj.attributes.has(attr) else "Created"
        try:
            if not done:
                obj.attributes.add(attr, value, category)
            return f"\n{verb} attribute {obj.name}/|w{attr}|n [category:{category}] = {value}"
        except SyntaxError:
            # this means literal_eval tried to parse a faulty string
            return (
                "\n|RCritical Python syntax error in your value. Only "
                "primitive Python structures are allowed.\nYou also "
                "need to use correct Python syntax. Remember especially "
                "to put quotes around all strings inside lists and "
                "dicts.|n"
            )

    @interactive
    def edit_handler(self, obj, attr, caller):
        """Activate the line editor"""

        def load(caller):
            """Called for the editor to load the buffer"""

            try:
                old_value = obj.attributes.get(attr, raise_exception=True)
                # we set empty buffer on nonexisting Attribute because otherwise
                # we'd always have the string "None" in the buffer to start with
                old_value = ""
            return str(old_value)  # we already confirmed we are ok with this

        def save(caller, buf):
            """Called when editor saves its buffer."""
            obj.attributes.add(attr, buf)
            caller.msg(f"Saved Attribute {attr}.")

        # check non-strings before activating editor
        try:
            old_value = obj.attributes.get(attr, raise_exception=True)
            if not isinstance(old_value, str):
                answer = yield (
                    f"|rWarning: Attribute |w{attr}|r is of type |w{type(old_value).__name__}|r. "
                    "\nTo continue editing, it must be converted to (and saved as) a string. "
                    "Continue? [Y]/N?"
                )
                if answer.lower() in ("n", "no"):
                    self.msg("Aborted edit.")
                    return
            pass

        # start the editor
        EvEditor(self.caller, load, save, key=f"{obj}/{attr}")

    def search_for_obj(self, objname):
        """
        Searches for an object matching objname. The object may be of different typeclasses.
        Args:
            objname: Name of the object we're looking for

        Returns:
            A typeclassed object, or None if nothing is found.
        """
        from evennia.utils.utils import variable_from_module

        _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
        if objname.startswith("*") or "account" in self.switches:
            found_obj = caller.search_account(objname.lstrip("*"))
        elif "script" in self.switches:
            found_obj = _AT_SEARCH_RESULT(search.search_script(objname), caller)
        elif "channel" in self.switches:
            found_obj = _AT_SEARCH_RESULT(search.search_channel(objname), caller)
        else:
            global_search = True
            if "char" in self.switches or "character" in self.switches:
                typeclass = settings.BASE_CHARACTER_TYPECLASS
            elif "room" in self.switches:
                typeclass = settings.BASE_ROOM_TYPECLASS
            elif "exit" in self.switches:
                typeclass = settings.BASE_EXIT_TYPECLASS
            else:
                global_search = False
                typeclass = None
            found_obj = caller.search(objname, global_search=global_search, typeclass=typeclass)
        return found_obj

        """Implement the set attribute - a limited form of py."""

            caller.msg("Usage: set obj/attr[:category] = value. Use empty value to clear.")
            return

        # get values prepared by the parser
        value = self.rhs
        objname = self.lhs_objattr[0]["name"]
        attrs = self.lhs_objattr[0]["attrs"]
        category = self.lhs_objs[0].get("option")  # None if unset

        obj = self.search_for_obj(objname)
        if not obj:
            return

        if not self.check_obj(obj):
            return

        result = []
            # edit in the line editor
            if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")):
                caller.msg(f"You don't have permission to edit {obj.key}.")
                return

            if len(attrs) > 1:
                caller.msg("The Line editor can only be applied to one attribute at a time.")
                return
            if not attrs:
                    "Use `set/edit <objname>/<attr>` to define the Attribute to edit.\nTo "
                    "edit the current room description, use `set/edit here/desc` (or "
                    "use the `desc` command)."
                )
                return
            self.edit_handler(obj, attrs[0], caller)
            return
        if not value:
            if self.rhs is None:
                # no = means we inspect the attribute(s)
                if not attrs:
                    attrs = [
                        attr.key
                        for attr in obj.attributes.get(
                            category=None, return_obj=True, return_list=True
                        )
                    ]
                for attr in attrs:
                    if not self.check_attr(obj, attr, category):
                        continue
                    result.append(self.view_attr(obj, attr, category))
            else:
                # deleting the attribute(s)
                if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")):
                    caller.msg(f"You don't have permission to edit {obj.key}.")
                    return
                        continue
                    result.append(self.rm_attr(obj, attr, category))
        else:
            # setting attribute(s). Make sure to convert to real Python type before saving.
            # add support for $dbref() and $search() in set argument
            global _ATTRFUNCPARSER
            if not _ATTRFUNCPARSER:
                _ATTRFUNCPARSER = funcparser.FuncParser(
                    {
                        "dbref": funcparser.funcparser_callable_search,
                        "search": funcparser.funcparser_callable_search,
                    }
                )

                return
            for attr in attrs:
                if not self.check_attr(obj, attr, category):
                    continue
                # from evennia import set_trace;set_trace()
                parsed_value = _ATTRFUNCPARSER.parse(value, return_str=False, caller=caller)
                if hasattr(parsed_value, "access"):
                    # if this is an object we must have the right to read it, if so,
                    # we will not convert it to a string
                    if not (
                        parsed_value.access(caller, "control")
                        or parsed_value.access(self.caller, "edit")
                    ):
                        caller.msg(
                            f"You don't have permission to set object with identifier '{value}'."
                        )
                        continue
                    value = parsed_value
                else:
                    value = _convert_from_string(self, value)
                result.append(self.set_attr(obj, attr, value, category))
        # check if anything was done
        if not result:
                "No valid attributes were found. Usage: set obj/attr[:category] = value. Use empty"
                " value to clear."
            )
        else:
            # send feedback
            caller.msg("".join(result).strip("\n"))

class CmdTypeclass(COMMAND_DEFAULT_CLASS):
    """
    set or change an object's typeclass

    Usage:
      typeclass[/switch] <object> [= typeclass.path]
      typeclass/prototype <object> = prototype_key

      typeclasses or typeclass/list/show [typeclass.path]
      swap - this is a shorthand for using /force/reset flags.
      update - this is a shorthand for using the /force/reload flag.

    Switch:
      show, examine - display the current typeclass of object (default) or, if
            given a typeclass path, show the docstring of that typeclass.
      update - *only* re-run at_object_creation on this object
              meaning locks or other properties set later may remain.
      reset - clean out *all* the attributes and properties on the
              object - basically making this a new clean object. This will also
              reset cmdsets!
      force - change to the typeclass also if the object
              already has a typeclass of the same name.
      list - show available typeclasses. Only typeclasses in modules actually
             imported or used from somewhere in the code will show up here
             (those typeclasses are still available if you know the path)
      prototype - clean and overwrite the object with the specified
               prototype key - effectively making a whole new object.

    Example:
      type button = examples.red_button.RedButton
      type/prototype button=a red button

    If the typeclass_path is not given, the current object's typeclass is
    assumed.

    View or set an object's typeclass. If setting, the creation hooks of the
    new typeclass will be run on the object. If you have clashing properties on
    the old class, use /reset. By default you are protected from changing to a
    typeclass of the same name as the one you already have - use /force to
    override this protection.

    The given typeclass must be identified by its location using python
    dot-notation pointing to the correct module and class. If no typeclass is
    given (or a wrong typeclass is given). Errors in the path or new typeclass
    will lead to the old typeclass being kept. The location of the typeclass
    module is searched from the default typeclass directory, as defined in the
    server settings.

    """

    key = "@typeclass"
    aliases = ["@type", "@parent", "@swap", "@update", "@typeclasses"]
    switch_options = ("show", "examine", "update", "reset", "force", "list", "prototype")
    locks = "cmd:perm(typeclass) or perm(Builder)"

    def _generic_search(self, query, typeclass_path):
        if typeclass_path:
            # make sure we search the right database table
            try:
                new_typeclass = class_from_module(typeclass_path)
                # this could be a prototype and not a typeclass at all
                return caller.search(query)

            dbclass = new_typeclass.__dbclass__

            if caller.__dbclass__ == dbclass:
                # object or account match
                obj = caller.search(query)
                if not obj:
                    return
            elif self.account and self.account.__dbclass__ == dbclass:
                # applying account while caller is object
                caller.msg(f"Trying to search {new_typeclass} with query '{self.lhs}'.")
                obj = self.account.search(query)
                if not obj:
                    return
            elif hasattr(caller, "puppet") and caller.puppet.__dbclass__ == dbclass:
                # applying object while caller is account
                obj = caller.puppet.search(query)
                if not obj:
                    return
            else:
                # other mismatch between caller and specified typeclass
                obj = new_typeclass.search(query)
                if not obj:
                    if isinstance(obj, list):
                        caller.msg(f"Could not find {new_typeclass} with query '{self.lhs}'.")
                    return
        else:
            # no rhs, use caller's typeclass
            obj = caller.search(query)
            if not obj:
                return

        return obj

        if "list" in self.switches or self.cmdname in ("typeclasses", "@typeclasses"):
            tclasses = get_all_typeclasses()
            contribs = [key for key in sorted(tclasses) if key.startswith("evennia.contrib")] or [
                "<None loaded>"
            ]
            core = [
                key for key in sorted(tclasses) if key.startswith("evennia") and key not in contribs
            ] or ["<None loaded>"]
            game = [key for key in sorted(tclasses) if not key.startswith("evennia")] or [
            ]
            string = (
                "|wCore typeclasses|n\n"
                "    {core}\n"
                "|wLoaded Contrib typeclasses|n\n"
                "    {contrib}\n"
                "|wGame-dir typeclasses|n\n"
                "    {game}"
            ).format(
                core="\n    ".join(core), contrib="\n    ".join(contribs), game="\n    ".join(game)
            )
            EvMore(caller, string, exit_on_lastpage=True)
            return

            caller.msg("Usage: %s <object> [= typeclass]" % self.cmdstring)
            return

        if "show" in self.switches or "examine" in self.switches:
            oquery = self.lhs
            obj = caller.search(oquery, quiet=True)
            if not obj:
                # no object found to examine, see if it's a typeclass-path instead
                tclasses = get_all_typeclasses()
                matches = [
                    (key, tclass) for key, tclass in tclasses.items() if key.endswith(oquery)
                ]
                nmatches = len(matches)
                if nmatches > 1:
                        "Multiple typeclasses found matching {}:\n  {}".format(
                            oquery, "\n  ".join(tup[0] for tup in matches)
                        )
                    )
                elif not matches:
                    caller.msg(f"No object or typeclass path found to match '{oquery}'")
                else:
                    # one match found
                    caller.msg(f"Docstring for typeclass '{oquery}': \n{matches[0][1].__doc__}")
            else:
                # do the search again to get the error handling in case of multi-match
                obj = caller.search(oquery)
                if not obj:
                    return
                    f"{obj.name}'s current typeclass is"
                    f" '{obj.__class__.__module__}.{obj.__class__.__name__}'"
                )
            return

        obj = self._generic_search(self.lhs, self.rhs)
        if not obj:
            return

        if not hasattr(obj, "__dbclass__"):
            string = "%s is not a typed object." % obj.name
            return

        new_typeclass = self.rhs or obj.path

        prototype = None
        if "prototype" in self.switches:
            key = self.rhs
            prototype = protlib.search_prototype(key=key)
            if len(prototype) > 1:
                    "More than one match for {}:\n{}".format(
                        key, "\n".join(proto.get("prototype_key", "") for proto in prototype)
                    )
                )
                return
            elif prototype:
                # one match
                prototype = prototype[0]
            else:
                # no match
                caller.msg(f"No prototype '{key}' was found.")
                return
            new_typeclass = prototype["typeclass"]
            self.switches.append("force")

            caller.msg(f"{obj.name}'s current typeclass is '{obj.__class__}'")
            return

        if self.cmdstring in ("swap", "@swap"):
            self.switches.append("reset")
        elif self.cmdstring in ("update", "@update"):
            self.switches.append("update")

            caller.msg("You are not allowed to do that.")
            return

        if not hasattr(obj, "swap_typeclass"):
            caller.msg("This object cannot have a type at all!")
            return

        is_same = obj.is_typeclass(new_typeclass, exact=True)
        if is_same and "force" not in self.switches:
            string = (
                f"{obj.name} already has the typeclass '{new_typeclass}'. Use /force to override."
            )
        else:
            reset = "reset" in self.switches
            update = "update" in self.switches or not reset  # default to update

            hooks = "at_object_creation" if update and not reset else "all"
            old_typeclass_path = obj.typeclass_path

            if reset:
                    "|yNote that this will reset the object back to its typeclass' default state,"
                    " removing any custom locks/perms/attributes etc that may have been added by an"
                    " explicit create_object call. Use `update` or type/force instead in order to"
                    " keep such data. Continue [Y]/N?|n"
                )
                if answer.upper() in ("N", "NO"):
                    caller.msg("Aborted.")
                    return

            # special prompt for the user in cases where we want
            # to confirm changes.
            if "prototype" in self.switches:
                diff, _ = spawner.prototype_diff_from_object(prototype, obj)
                txt = spawner.format_diff(diff)
                prompt = (
                    f"Applying prototype '{prototype['key']}' over '{obj.name}' will cause the"
                    f" follow changes:\n{txt}\n"
                )
                if not reset:
                    prompt += (
                        "\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank"
                        " state."
                    )
                prompt += "\nAre you sure you want to apply these changes [yes]/no?"
                answer = yield (prompt)
                if answer and answer in ("no", "n"):
                    caller.msg("Canceled: No changes were applied.")
                    return

            # we let this raise exception if needed
            obj.swap_typeclass(
                new_typeclass, clean_attributes=reset, clean_cmdsets=reset, run_start_hooks=hooks
            )

                modified = spawner.batch_update_objects_with_prototype(
                    prototype, objects=[obj], caller=self.caller
                )
                prototype_success = modified > 0
                if not prototype_success:
                    caller.msg(f"Prototype {prototype['key']} failed to apply.")

            if is_same:
                string = f"{obj.name} updated its existing typeclass ({obj.path}).\n"
            else:
                string = (
                    f"{obj.name} changed typeclass from {old_typeclass_path} to"
                    f" {obj.typeclass_path}.\n"
                )
            if update:
                string += "Only the at_object_creation hook was run (update mode)."
            else:
                string += "All object creation hooks were run."
            if reset:
                string += " All old attributes where deleted before the swap."
            else:
                string += (
                    " Attributes set before swap were not removed\n(use `swap` or `type/reset` to"
                    " clear all)."
                )
            if "prototype" in self.switches and prototype_success:
                string += (
                    f" Prototype '{prototype['key']}' was successfully applied over the object"
                    " type."
                )

class CmdWipe(ObjManipCommand):
    """
    clear all attributes from an object

    Usage:
      wipe <object>[/<attr>[/<attr>...]]

    Example:
      wipe box
      wipe box/colour

    Wipes all of an object's attributes, or optionally only those
    matching the given attribute-wildcard search string.
    """

    key = "@wipe"
    locks = "cmd:perm(wipe) or perm(Builder)"

        """
        inp is the dict produced in ObjManipCommand.parse()
        """

            caller.msg("Usage: wipe <object>[/<attr>/<attr>...]")
            return

        # get the attributes set by our custom parser

        if not obj:
            return
            return
        if not attrs:
            # wipe everything
            obj.attributes.clear()
            string = f"Wiped all attributes on {obj.name}."
        else:
            for attrname in attrs:
                obj.attributes.remove(attrname)
            string = f"Wiped attributes {','.join(attrs)} on {obj.name}."

class CmdLock(ObjManipCommand):
    """
    assign a lock definition to an object

    Usage:
      lock <object or *account>[ = <lockstring>]
      or
      lock[/switch] <object or *account>/<access_type>

    Switch:
      del - delete given access type
      view - view lock associated with given access type (default)

    If no lockstring is given, shows all locks on
    object.

    Lockstring is of the form
       access_type:[NOT] func1(args)[ AND|OR][ NOT] func2(args) ...]
    Where func1, func2 ... valid lockfuncs with or without arguments.
    Separator expressions need not be capitalized.

    For example:
       'get: id(25) or perm(Admin)'
    The 'get' lock access_type is checked e.g. by the 'get' command.
    An object locked with this example lock will only be possible to pick up
    by Admins or by an object with id=25.

    You can add several access_types after one another by separating
    them by ';', i.e:
       'get:id(25); delete:perm(Builder)'
    """

    key = "@lock"
    aliases = ["@locks"]
    locks = "cmd: perm(locks) or perm(Builder)"

        """Sets up the command"""

            string = "Usage: lock <object>[ = <lockstring>] or lock[/switch] <object>/<access_type>"
            return

        if "/" in self.lhs:
            # call of the form lock obj/access_type
            objname, access_type = [p.strip() for p in self.lhs.split("/", 1)]
            obj = None
                obj = caller.search_account(objname.lstrip("*"))
            if not obj:
                if not obj:
                    return
            has_control_access = obj.access(caller, "control")
            if access_type == "control" and not has_control_access:
                # only allow to change 'control' access if you have 'control' access already
                caller.msg("You need 'control' access to change this type of lock.")
                return

            if not (has_control_access or obj.access(caller, "edit")):
                caller.msg("You are not allowed to do that.")
                return

            lockdef = obj.locks.get(access_type)

            if lockdef:
                if "del" in self.switches:
                    obj.locks.delete(access_type)
                    string = "deleted lock %s" % lockdef
                else:
                    string = lockdef
            else:
                string = f"{obj} has no lock of access type '{access_type}'."
            return

            # we have a = separator, so we are assigning a new lock
            if self.switches:
                swi = ", ".join(self.switches)
                    f"Switch(es) |w{swi}|n can not be used with a "
                    "lock assignment. Use e.g. "
                    "|wlock/del objname/locktype|n instead."
                )
                return

            objname, lockdef = self.lhs, self.rhs
            obj = None
            if not obj:
                if not obj:
                    return
            if not (obj.access(caller, "control") or obj.access(caller, "edit")):
                return
            ok = False
            lockdef = re.sub(r"\'|\"", "", lockdef)
            try:
                ok = obj.locks.add(lockdef)
            except LockException as e:
                caller.msg(str(e))
            if "cmd" in lockdef.lower() and inherits_from(
                obj, "evennia.objects.objects.DefaultExit"
            ):
                # special fix to update Exits since "cmd"-type locks won't
                # update on them unless their cmdsets are rebuilt.
                obj.at_init()
            if ok:
                caller.msg(f"Added lock '{lockdef}' to {obj}.")
            return

        # if we get here, we are just viewing all locks on obj
        obj = None
        if self.lhs.startswith("*"):
            obj = caller.search_account(self.lhs.lstrip("*"))
        if not obj:
        if not obj:
            return
            return
        caller.msg("\n".join(obj.locks.all()))

class CmdExamine(ObjManipCommand):
    """
    get detailed information about an object

    Usage:
      examine [<object>[/attrname]]
      examine [*<account>[/attrname]]

    Switch:
      account - examine an Account (same as adding *)
      object - examine an Object (useful when OOC)
      script - examine a Script
      channel - examine a Channel

    The examine command shows detailed game info about an
    object and optionally a specific attribute on it.
    If object is not specified, the current location is examined.

    Append a * before the search string to examine an account.

    """

    key = "@examine"
    aliases = ["@ex", "@exam"]
    locks = "cmd:perm(examine) or perm(Builder)"
    arg_regex = r"(/\w+?(\s|$))|\s|$"
    switch_options = ["account", "object", "script", "channel"]

    object_type = "object"

    detail_color = "|c"
    header_color = "|w"
    quell_color = "|r"
    separator = "-"

    def msg(self, text):
        """
        Central point for sending messages to the caller. This tags
        the message as 'examine' for eventual custom markup in the client.

        Attributes:
            text (str): The text to send.

        """
        super().msg(text=(text, {"type": "examine"}))

    def format_key(self, obj):
        return f"{obj.name} ({obj.dbref})"

    def format_aliases(self, obj):
        if hasattr(obj, "aliases") and obj.aliases.all():
            return ", ".join(utils.make_iter(str(obj.aliases)))

    def format_typeclass(self, obj):
        if hasattr(obj, "typeclass_path"):
            return f"{obj.typename} ({obj.typeclass_path})"

    def format_sessions(self, obj):
        if hasattr(obj, "sessions"):
            sessions = obj.sessions.all()
            if sessions:
                return ", ".join(f"#{sess.sessid}" for sess in obj.sessions.all())

    def format_email(self, obj):
        if hasattr(obj, "email") and obj.email:
            return f"{self.detail_color}{obj.email}|n"

    def format_last_login(self, obj):
        if hasattr(obj, "last_login") and obj.last_login:
            return f"{self.detail_color}{obj.last_login}|n"

    def format_account_key(self, account):
        return f"{self.detail_color}{account.name}|n ({account.dbref})"

    def format_account_typeclass(self, account):
        return f"{account.typename} ({account.typeclass_path})"

    def format_account_permissions(self, account):
        perms = account.permissions.all()
        if account.is_superuser:
            perms = ["<Superuser>"]
        elif not perms:
            perms = ["<None>"]
        perms = ", ".join(perms)
        if account.attributes.has("_quell"):
            perms += f" {self.quell_color}(quelled)|n"
        return perms

    def format_location(self, obj):
        if hasattr(obj, "location") and obj.location:
            return f"{obj.location.key} (#{obj.location.id})"

    def format_home(self, obj):
        if hasattr(obj, "home") and obj.home:
            return f"{obj.home.key} (#{obj.home.id})"

    def format_destination(self, obj):
        if hasattr(obj, "destination") and obj.destination:
            return f"{obj.destination.key} (#{obj.destination.id})"

    def format_permissions(self, obj):
        perms = obj.permissions.all()
        if perms:
            perms_string = ", ".join(perms)
            if obj.is_superuser:
                perms_string += " <Superuser>"
            return perms_string

    def format_locks(self, obj):
        locks = str(obj.locks)
        if locks:
            return utils.fill("; ".join([lock for lock in locks.split(";")]), indent=2)
        return "Default"

    def format_scripts(self, obj):
        if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all():
            return f"{obj.scripts}"

    def format_single_tag(self, tag):
        if tag.db_category:
            return f"{tag.db_key}[{tag.db_category}]"
        else:
            return f"{tag.db_key}"

    def format_tags(self, obj):
        if hasattr(obj, "tags"):
            tags = sorted(obj.tags.all(return_objs=True))
            if tags:
                formatted_tags = [self.format_single_tag(tag) for tag in tags]
                return utils.fill(", ".join(formatted_tags), indent=2)

    def format_single_cmdset_options(self, cmdset):
        def _truefalse(string, value):
            if value is None:
                return ""
            if value:
                return f"{string}: T"
            return f"{string}: F"

        txt = ", ".join(
            _truefalse(opt, getattr(cmdset, opt))
            for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
            if getattr(cmdset, opt) is not None
        )
        return ", " + txt if txt else ""

    def format_single_cmdset(self, cmdset):
        options = self.format_single_cmdset_options(cmdset)
        return f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority}{options})"

    def format_stored_cmdsets(self, obj):
        if hasattr(obj, "cmdset"):
            stored_cmdset_strings = []
            stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority, reverse=True)
            for cmdset in stored_cmdsets:
                if cmdset.key != "_EMPTY_CMDSET":
                    stored_cmdset_strings.append(self.format_single_cmdset(cmdset))
            return "\n  " + "\n  ".join(stored_cmdset_strings)

    def format_merged_cmdsets(self, obj, current_cmdset):
        if not hasattr(obj, "cmdset"):

        all_cmdsets = [(cmdset.key, cmdset) for cmdset in current_cmdset.merged_from]
        # we always at least try to add account- and session sets since these are ignored
        # if we merge on the object level.
        if inherits_from(obj, evennia.DefaultObject) and obj.account:
            # get Attribute-cmdsets if they exist
            all_cmdsets.extend([(cmdset.key, cmdset) for cmdset in obj.account.cmdset.all()])
            if obj.sessions.count():
                # if there are more sessions than one on objects it's because of multisession mode
                # we only show the first session's cmdset here (it is -in principle- possible
                # that different sessions have different cmdsets but for admins who want such
                # madness it is better that they overload with their own CmdExamine to handle it).
                all_cmdsets.extend(
                    [(cmdset.key, cmdset) for cmdset in obj.account.sessions.all()[0].cmdset.all()]
                )
        else:
            try:
                # we have to protect this since many objects don't have sessions.
                    [
                        (cmdset.key, cmdset)
                        for cmdset in obj.get_session(obj.sessions.get()).cmdset.all()
                    ]
                )
            except (TypeError, AttributeError):
                # an error means we are merging an object without a session
                pass
        all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()]
        all_cmdsets.sort(key=lambda x: x.priority, reverse=True)

        merged_cmdset_strings = []
        for cmdset in all_cmdsets:
            if cmdset.key != "_EMPTY_CMDSET":
                merged_cmdset_strings.append(self.format_single_cmdset(cmdset))
        return "\n  " + "\n  ".join(merged_cmdset_strings)

    def format_current_cmds(self, obj, current_cmdset):
        current_commands = sorted([cmd.key for cmd in current_cmdset if cmd.access(obj, "cmd")])
        return "\n" + utils.fill(", ".join(current_commands), indent=2)

    def _get_attribute_value_type(self, attrvalue):
        typ = ""
        if not isinstance(attrvalue, str):
            try:
                name = attrvalue.__class__.__name__
                try:
                    name = attrvalue.__name__
                    name = attrvalue
            if str(name).startswith("_Saver"):
                try:
                    typ = str(type(deserialize(attrvalue)))
            else:
                typ = str(type(attrvalue))
        return typ

    def format_single_attribute_detail(self, obj, attr):
        global _FUNCPARSER
        if not _FUNCPARSER:
            _FUNCPARSER = funcparser.FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES)

        key, category, value = attr.db_key, attr.db_category, attr.value
        typ = self._get_attribute_value_type(value)
        typ = f" |B[type: {typ}]|n" if typ else ""
        value = utils.to_str(value)
        value = _FUNCPARSER.parse(ansi_raw(value), escape=True)
        return (
            f"Attribute {obj.name}/{self.header_color}{key}|n "
            f"[category={category}]{typ}:\n\n{value}"
        )

    def format_single_attribute(self, attr):

        value = utils.crop(value)
        if category:
            return f"{self.header_color}{key}|n[{category}]={value}{typ}"
        else:
            return f"{self.header_color}{key}|n={value}{typ}"

    def format_attributes(self, obj):
        output = "\n  " + "\n  ".join(
            sorted(self.format_single_attribute(attr) for attr in obj.db_attributes.all())
        )
        if output.strip():
            # we don't want just an empty line
            return output

    def format_nattributes(self, obj):
        try:
            ndb_attr = obj.nattributes.all()
            return

        if ndb_attr and ndb_attr[0]:
            return "\n  " + "\n  ".join(
                sorted(self.format_single_attribute(attr) for attr in ndb_attr)
            )

    def format_exits(self, obj):
        if hasattr(obj, "exits"):
            exits = ", ".join(f"{exit.name}({exit.dbref})" for exit in obj.exits)
            return exits if exits else None

    def format_chars(self, obj):
        if hasattr(obj, "contents"):
            chars = ", ".join(f"{obj.name}({obj.dbref})" for obj in obj.contents if obj.account)
            return chars if chars else None

    def format_things(self, obj):
            things = ", ".join(
                f"{obj.name}({obj.dbref})"
                for obj in obj.contents
                if not obj.account and not obj.destination
            )
            return things if things else None

    def format_script_desc(self, obj):
        if hasattr(obj, "db_desc") and obj.db_desc:
            return crop(obj.db_desc, 20)

    def format_script_is_persistent(self, obj):
        if hasattr(obj, "db_persistent"):
            return "T" if obj.db_persistent else "F"

    def format_script_timer_data(self, obj):
        if hasattr(obj, "db_interval") and obj.db_interval > 0:
            start_delay = "T" if obj.db_start_delay else "F"
            next_repeat = obj.time_until_next_repeat()
            active = "|grunning|n" if obj.db_is_active and next_repeat else "|rinactive|n"
            interval = obj.db_interval
            next_repeat = "N/A" if next_repeat is None else f"{next_repeat}s"
            repeats = ""
            if obj.db_repeats:
                remaining_repeats = obj.remaining_repeats()
                remaining_repeats = 0 if remaining_repeats is None else remaining_repeats
                repeats = f" - {remaining_repeats}/{obj.db_repeats} remain"
            return (
                f"{active} - interval: {interval}s "
                f"(next: {next_repeat}{repeats}, start_delay: {start_delay})"
            )

    def format_channel_sub_totals(self, obj):
        if hasattr(obj, "db_account_subscriptions"):
            account_subs = obj.db_account_subscriptions.all()
            object_subs = obj.db_object_subscriptions.all()
            online = len(obj.subscriptions.online())
            ntotal = account_subs.count() + object_subs.count()
            return f"{ntotal} ({online} online)"

    def format_channel_account_subs(self, obj):
            if account_subs:
                return "\n  " + "\n  ".join(
                    format_grid([sub.key for sub in account_subs], sep=" ", width=_DEFAULT_WIDTH)
                )

    def format_channel_object_subs(self, obj):
        if hasattr(obj, "db_object_subscriptions"):
            if object_subs:
                    format_grid([sub.key for sub in object_subs], sep=" ", width=_DEFAULT_WIDTH)
                )

    def get_formatted_obj_data(self, obj, current_cmdset):
        """
        Calls all other `format_*` methods.

        """
        objdata = {}
        objdata["Name/key"] = self.format_key(obj)
        objdata["Aliases"] = self.format_aliases(obj)
        objdata["Typeclass"] = self.format_typeclass(obj)
        objdata["Sessions"] = self.format_sessions(obj)
        objdata["Email"] = self.format_email(obj)
        objdata["Last Login"] = self.format_last_login(obj)
        if inherits_from(obj, evennia.DefaultObject) and obj.has_account:
            objdata["Account"] = self.format_account_key(obj.account)
            objdata["  Account Typeclass"] = self.format_account_typeclass(obj.account)
            objdata["  Account Permissions"] = self.format_account_permissions(obj.account)
        objdata["Location"] = self.format_location(obj)
        objdata["Home"] = self.format_home(obj)
        objdata["Destination"] = self.format_destination(obj)
        objdata["Permissions"] = self.format_permissions(obj)
        objdata["Locks"] = self.format_locks(obj)
        if current_cmdset and not (
            len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"
        ):
            objdata["Stored Cmdset(s)"] = self.format_stored_cmdsets(obj)
            objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset)
            objdata[
                f"Commands available to {obj.key} (result of Merged Cmdset(s))"
            ] = self.format_current_cmds(obj, current_cmdset)
        if self.object_type == "script":
            objdata["Description"] = self.format_script_desc(obj)
            objdata["Persistent"] = self.format_script_is_persistent(obj)
            objdata["Script Repeat"] = self.format_script_timer_data(obj)
        objdata["Scripts"] = self.format_scripts(obj)
        objdata["Tags"] = self.format_tags(obj)
        objdata["Persistent Attributes"] = self.format_attributes(obj)
        objdata["Non-Persistent Attributes"] = self.format_nattributes(obj)
        objdata["Exits"] = self.format_exits(obj)
        objdata["Characters"] = self.format_chars(obj)
        objdata["Content"] = self.format_things(obj)
        if self.object_type == "channel":
            objdata["Subscription Totals"] = self.format_channel_sub_totals(obj)
            objdata["Account Subscriptions"] = self.format_channel_account_subs(obj)
            objdata["Object Subscriptions"] = self.format_channel_object_subs(obj)

        return objdata

    def format_output(self, obj, current_cmdset):
        """
        Formats the full examine page return.

        """
        objdata = self.get_formatted_obj_data(obj, current_cmdset)

        # format output
        main_str = []
        max_width = -1
        for header, block in objdata.items():
            if block is not None:
                blockstr = f"{self.header_color}{header}|n: {block}"
                max_width = max(max_width, max(display_len(line) for line in blockstr.split("\n")))
                main_str.append(blockstr)
        main_str = "\n".join(main_str)

        max_width = max(0, min(self.client_width(), max_width))
        sep = self.separator * max_width

        return f"{sep}\n{main_str}\n{sep}"

    def _search_by_object_type(self, obj_name, objtype):
        """
        Route to different search functions depending on the object type being
        examined. This also handles error reporting for multimatches/no matches.

        Args:
            obj_name (str): The search query.
            objtype (str): One of 'object', 'account', 'script' or 'channel'.
        Returns:
            any: `None` if no match or multimatch, otherwise a single result.

        """
        obj = None

        if objtype == "object":
            obj = self.caller.search(obj_name)
        elif objtype == "account":
            try:
                obj = self.caller.search_account(obj_name.lstrip("*"))
                # this means we are calling examine from an account object
                obj = self.caller.search(
                    obj_name.lstrip("*"), search_object="object" in self.switches
                )
        else:
            obj = getattr(search, f"search_{objtype}")(obj_name)
            if not obj:
                self.msg(f"No {objtype} found with key {obj_name}.")
                obj = None
            elif len(obj) > 1:
                err = "Multiple {objtype} found with key {obj_name}:\n{matches}"
                self.msg(
                    err.format(
                        obj_name=obj_name, matches=", ".join(f"{ob.key}(#{ob.id})" for ob in obj)
                    )
                )
                obj = None
            else:
                obj = obj[0]
        return obj

        self.examine_objs = []

            # If no arguments are provided, examine the invoker's location.
            if hasattr(self.caller, "location"):
                self.examine_objs.append((self.caller.location, None))
            else:
                self.msg("You need to supply a target to examine.")
                raise InterruptCommand
        else:
            for objdef in self.lhs_objattr:
                # note that we check the objtype for every repeat; this will always
                # be the same result, but it makes for a cleaner code and multi-examine
                # is not so common anyway.

                obj = None
                obj_name = objdef["name"]  # name
                obj_attrs = objdef["attrs"]  # /attrs

                # identify object type, in prio account - script - channel
                object_type = "object"
                if (
                    utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount")
                    or "account" in self.switches
                    or obj_name.startswith("*")
                ):
                    object_type = "account"
                elif "script" in self.switches:
                    object_type = "script"
                elif "channel" in self.switches:
                    object_type = "channel"

                self.object_type = object_type
                obj = self._search_by_object_type(obj_name, object_type)

                if obj:
                    self.examine_objs.append((obj, obj_attrs))

        """Process command"""
        for obj, obj_attrs in self.examine_objs:
            # these are parsed out in .parse already

            if not obj.access(self.caller, "examine"):
                # If we don't have special info access, just look
                # at the object instead.
                self.msg(self.caller.at_look(obj))
                continue

            if obj_attrs:
                # we are only interested in specific attributes
                attrs = [attr for attr in obj.db_attributes.all() if attr.db_key in obj_attrs]
                    self.msg(f"No attributes found on {obj.name}.")
                else:
                    out_strings = []
                    for attr in attrs:
                        out_strings.append(self.format_single_attribute_detail(obj, attr))
                    out_str = "\n".join(out_strings)
                    max_width = max(display_len(line) for line in out_strings)
                    max_width = max(0, min(max_width, self.client_width()))
                    sep = self.separator * max_width
                    self.msg(f"{sep}\n{out_str}")
                return

            # examine the obj itself

            if self.object_type in ("object", "account"):
                # for objects and accounts we need to set up an asynchronous
                # fetch of the cmdset and not proceed with the examine display
                # until the fetch is complete
                session = None
                    mergemode = "session"
                    session = obj.sessions.get()[0]
                elif self.object_type == "account":
                    mergemode = "account"
                else:
                    mergemode = "object"

                account = None
                objct = None
                if self.object_type == "account":
                    account = obj
                else:
                    account = obj.account
                    objct = obj

                # this is usually handled when a command runs, but when we examine
                # we may have leftover inherited cmdsets directly after a move etc.
                obj.cmdset.update()
                # using callback to print results whenever function returns.

                def _get_cmdset_callback(current_cmdset):
                    self.msg(self.format_output(obj, current_cmdset).strip())

                get_and_merge_cmdsets(
                    obj, session, account, objct, mergemode, self.raw_string
                ).addCallback(_get_cmdset_callback)

            else:
                # for objects without cmdsets we can proceed to examine immediately
                self.msg(self.format_output(obj, None).strip())

class CmdFind(COMMAND_DEFAULT_CLASS):
    """
    search the database for objects

    Usage:
      find[/switches] <name or dbref or *account> [= dbrefmin[-dbrefmax]]
      locate - this is a shorthand for using the /loc switch.

    Switches:
      room       - only look for rooms (location=None)
      exit       - only look for exits (destination!=None)
      char       - only look for characters (BASE_CHARACTER_TYPECLASS)
      exact      - only exact matches are returned.
      loc        - display object location if exists and match has one result
      startswith - search for names starting with the string, rather than containing

    Searches the database for an object of a particular name or exact #dbref.
    Use *accountname to search for an account. The switches allows for
    limiting object matches to certain game entities. Dbrefmin and dbrefmax
    limits matches to within the given dbrefs range, or above/below if only
    one is given.
    """

    key = "@find"
    aliases = ["@search", "@locate"]
    switch_options = ("room", "exit", "char", "exact", "loc", "startswith")
    locks = "cmd:perm(find) or perm(Builder)"

        """Search functionality"""

        if not self.args or (not self.lhs and not self.rhs):
            caller.msg("Usage: find <string> [= low [-high]]")
            return

        if "locate" in self.cmdstring:  # Use option /loc as a default for locate command alias
            switches.append("loc")

        searchstring = self.lhs

        try:
            # Try grabbing the actual min/max id values by database aggregation
            qs = ObjectDB.objects.values("id").aggregate(low=Min("id"), high=Max("id"))
            low, high = sorted(qs.values())
            if not (low and high):
                    f"{self.__class__.__name__}: Min and max ID not returned by aggregation;"
                    " falling back to queryset slicing."
                )
            logger.log_trace(e)
            # If that doesn't work for some reason (empty DB?), guess the lower
            # bound and do a less-efficient query to find the upper.
            low, high = 1, ObjectDB.objects.all().order_by("-id").first().id

            try:
                # Check that rhs is either a valid dbref or dbref range
                bounds = tuple(
                    sorted(dbref(x, False) for x in re.split("[-\s]+", self.rhs.strip()))
                )

                # dbref() will return either a valid int or None
                assert bounds
                # None should not exist in the bounds list
                assert None not in bounds

                low = bounds[0]
                if len(bounds) > 1:
                    high = bounds[-1]

            except AssertionError:
                caller.msg("Invalid dbref range provided (not a number).")
                return
            except IndexError as e:
                logger.log_err(
                    f"{self.__class__.__name__}: Error parsing upper and lower bounds of query."
                )
                logger.log_trace(e)

        low = min(low, high)
        high = max(low, high)

        is_dbref = utils.dbref(searchstring)
        is_account = searchstring.startswith("*")

        restrictions = ""
        if self.switches:
            restrictions = ", %s" % ", ".join(self.switches)

        if is_dbref or is_account:
            if is_dbref:
                # a dbref search
                result = caller.search(searchstring, global_search=True, quiet=True)
                string = "|wExact dbref match|n(#%i-#%i%s):" % (low, high, restrictions)
            else:
                # an account search
                searchstring = searchstring.lstrip("*")
                result = caller.search_account(searchstring, quiet=True)
                string = "|wMatch|n(#%i-#%i%s):" % (low, high, restrictions)

            if "room" in switches:
                result = result if inherits_from(result, ROOM_TYPECLASS) else None
            if "exit" in switches:
                result = result if inherits_from(result, EXIT_TYPECLASS) else None
            if "char" in switches:
                result = result if inherits_from(result, CHAR_TYPECLASS) else None

            if not result:
                string += "\n   |RNo match found.|n"
            elif not low <= int(result[0].id) <= high:
                string += f"\n   |RNo match found for '{searchstring}' in #dbref interval.|n"
            else:
                result = result[0]
                string += f"\n|g   {result.get_display_name(caller)} - {result.path}|n"
                if "loc" in self.switches and not is_account and result.location:
                    string += f" (|wlocation|n: |g{result.location.get_display_name(caller)}|n)"
        else:
            # Not an account/dbref search but a wider search; build a queryset.
            # Searches for key and aliases
            if "exact" in switches:
                keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high)
                aliasquery = Q(
                    db_tags__db_key__iexact=searchstring,
                    db_tags__db_tagtype__iexact="alias",
                    id__gte=low,
                    id__lte=high,
                )
            elif "startswith" in switches:
                keyquery = Q(db_key__istartswith=searchstring, id__gte=low, id__lte=high)
                    db_tags__db_key__istartswith=searchstring,
                )
            else:
                keyquery = Q(db_key__icontains=searchstring, id__gte=low, id__lte=high)
                    db_tags__db_key__icontains=searchstring,
                )

            # Keep the initial queryset handy for later reuse
            result_qs = ObjectDB.objects.filter(keyquery | aliasquery).distinct()
            nresults = result_qs.count()

            # Use iterator to minimize memory ballooning on large result sets
            results = result_qs.iterator()

            # Check and see if type filtering was requested; skip it if not
            if any(x in switches for x in ("room", "exit", "char")):
                obj_ids = set()
                for obj in results:
                    if (
                        ("room" in switches and inherits_from(obj, ROOM_TYPECLASS))
                        or ("exit" in switches and inherits_from(obj, EXIT_TYPECLASS))
                        or ("char" in switches and inherits_from(obj, CHAR_TYPECLASS))
                    ):
                        obj_ids.add(obj.id)

                # Filter previous queryset instead of requesting another
                filtered_qs = result_qs.filter(id__in=obj_ids).distinct()
                nresults = filtered_qs.count()

                # Use iterator again to minimize memory ballooning
                results = filtered_qs.iterator()

            # still results after type filtering?
            if nresults:
                if nresults > 1:
                    header = f"{nresults} Matches"
                else:
                    header = "One Match"

                string = f"|w{header}|n(#{low}-#{high}{restrictions}):"
                res = None
                for res in results:
                    string += f"\n   |g{res.get_display_name(caller)} - {res.path}|n"
                if (
                    "loc" in self.switches
                    and nresults == 1
                    and res
                    and getattr(res, "location", None)
                ):
                    string += f" (|wlocation|n: |g{res.location.get_display_name(caller)}|n)"
            else:
                string = f"|wNo Matches|n(#{low}-#{high}{restrictions}):"
                string += f"\n   |RNo matches found for '{searchstring}'|n"

        # send result

class ScriptEvMore(EvMore):
    """
    Listing 1000+ Scripts can be very slow and memory-consuming. So
    we use this custom EvMore child to build en EvTable only for
    each page of the list.

    """

    def init_pages(self, scripts):
        """Prepare the script list pagination"""
        script_pages = Paginator(scripts, max(1, int(self.height / 2)))
        super().init_pages(script_pages)

    def page_formatter(self, scripts):
        """Takes a page of scripts and formats the output
        into an EvTable."""

        if not scripts:
            return "<No scripts>"

            "|wdbref|n",
            "|wobj|n",
            "|wkey|n",
            "|wintval|n",
            "|wnext|n",
            "|wrept|n",
            "|wtypeclass|n",
            "|wdesc|n",
            align="r",
            border="tablecols",
            width=self.width,
        )

        for script in scripts:
            nextrep = script.time_until_next_repeat()
            if nextrep is None:
                nextrep = script.db._paused_time
                nextrep = f"PAUSED {int(nextrep)}s" if nextrep else "--"
            else:
                nextrep = f"{nextrep}s"

            maxrepeat = script.repeats
            remaining = script.remaining_repeats() or 0
            if maxrepeat:
                rept = "%i/%i" % (maxrepeat - remaining, maxrepeat)
            else:
                rept = "-/-"

                f"#{script.id}",
                f"{script.obj.key}({script.obj.dbref})"
                if (hasattr(script, "obj") and script.obj)
                else "<Global>",
                script.key,
                script.interval if script.interval > 0 else "--",
                nextrep,
                rept,
                script.typeclass_path.rsplit(".", 1)[-1],
                crop(script.desc, width=20),
            )

        return str(table)

class CmdScripts(COMMAND_DEFAULT_CLASS):
    """
    List and manage all running scripts. Allows for creating new global
    scripts.

    Usage:
      script[/switches] [script-#dbref, key, script.path]
      script[/start||stop] <obj> = [<script.path or script-key>]

    Switches:
      start - start/unpause an existing script's timer.
      stop - stops an existing script's timer
      pause - pause a script's timer
      delete - deletes script. This will also stop the timer as needed

    Examples:
        script                            - list all scripts
        script foo.bar.Script             - create a new global Script
        script/pause foo.bar.Script       - pause global script
        script scriptname|#dbref          - examine named existing global script
        script/delete #dbref[-#dbref]     - delete script or range by #dbref

        script myobj =                    - list all scripts on object
        script myobj = foo.bar.Script     - create and assign script to object
        script/stop myobj = name|#dbref   - stop named script on object
        script/delete myobj = name|#dbref - delete script on object
        script/delete myobj =             - delete ALL scripts on object

    When given with an `<obj>` as left-hand-side, this creates and
    assigns a new script to that object. Without an `<obj>`, this
    manages and inspects global scripts.

    If no switches are given, this command just views all active
    scripts. The argument can be either an object, at which point it
    will be searched for all scripts defined on it, or a script name
    or #dbref. For using the /stop switch, a unique script #dbref is
    required since whole classes of scripts often have the same name.

    Use the `script` build-level command for managing scripts attached to
    objects.

    """

    key = "@scripts"
    aliases = ["@script"]
    switch_options = ("create", "start", "stop", "pause", "delete")
    locks = "cmd:perm(scripts) or perm(Builder)"
    help_category = "System"

    excluded_typeclass_paths = ["evennia.prototypes.prototypes.DbPrototype"]

    switch_mapping = {
        "create": "|gCreated|n",
        "start": "|gStarted|n",
        "stop": "|RStopped|n",
        "pause": "|Paused|n",
        "delete": "|rDeleted|n",
    }
    # never show these script types
    hide_script_paths = ("evennia.prototypes.prototypes.DbPrototype",)

    def _search_script(self, args):
        # test first if this is a script match
        scripts = ScriptDB.objects.get_all_scripts(key=args).exclude(
            db_typeclass_path__in=self.hide_script_paths
        )
        if scripts:
            return scripts
        # try typeclass path
        scripts = (
            ScriptDB.objects.filter(db_typeclass_path__iendswith=args)
            .exclude(db_typeclass_path__in=self.hide_script_paths)
            .order_by("id")
        )
        if "-" in args:
            # may be a dbref-range
            val1, val2 = (dbref(part.strip()) for part in args.split("-", 1))
            if val1 and val2:
                scripts = (
                    ScriptDB.objects.filter(id__in=(range(val1, val2 + 1)))
                    .exclude(db_typeclass_path__in=self.hide_script_paths)
                    .order_by("id")
                )
                if scripts:
                    return scripts

        """implement method"""

            # show all scripts
            scripts = ScriptDB.objects.all().exclude(db_typeclass_path__in=self.hide_script_paths)
            if not scripts:
                caller.msg("No scripts found.")
                return
            ScriptEvMore(caller, scripts.order_by("id"), session=self.session)
            return

        # find script or object to operate on
        scripts, obj = None, None
            obj_query = self.lhs
            script_query = self.rhs
        elif self.rhs is not None:
            # an empty "="
            script_query = None
        else:
            obj_query = None
            script_query = self.args

        scripts = self._search_script(script_query) if script_query else None
        objects = caller.search(obj_query, quiet=True) if obj_query else None
        obj = objects[0] if objects else None

        if not self.switches:
            # creation / view mode
            if obj:
                # we have an object
                if self.rhs:
                    # creation mode
                    if obj.scripts.add(self.rhs, autostart=True):
                            f"Script |w{self.rhs}|n successfully added and "
                            f"started on {obj.get_display_name(caller)}."
                        )
                    else:
                            f"Script {self.rhs} could not be added and/or started "
                            f"on {obj.get_display_name(caller)} (or it started and "
                            "immediately shut down)."
                        )
                else:
                    # just show all scripts on object
                    scripts = ScriptDB.objects.filter(db_obj=obj).exclude(
                        db_typeclass_path__in=self.hide_script_paths
                    )
                    if scripts:
                        ScriptEvMore(caller, scripts.order_by("id"), session=self.session)
                    else:
                        caller.msg(f"No scripts defined on {obj}")

            elif scripts:
                # show found script(s)
                ScriptEvMore(caller, scripts.order_by("id"), session=self.session)

            else:
                # create global script
                try:
                    new_script = create.create_script(self.args)
                except ImportError:
                    new_script = None

                if new_script:
                        f"Global Script Created - {new_script.key} ({new_script.typeclass_path})"
                    )
                    ScriptEvMore(caller, [new_script], session=self.session)
                else:
                        f"Global Script |rNOT|n Created |r(see log)|n - arguments: {self.args}"
                    )

        elif scripts or obj:
            # modification switches - must operate on existing scripts

                scripts = ScriptDB.objects.filter(db_obj=obj).exclude(
                    db_typeclass_path__in=self.hide_script_paths
                )

            if scripts.count() > 1:
                ret = yield (
                    f"Multiple scripts found: {scripts}. Are you sure you want to "
                    "operate on all of them? [Y]/N? "
                )
                if ret.lower() in ("n", "no"):
                    return

            for script in scripts:
                script_key = script.key
                script_typeclass_path = script.typeclass_path
                scripttype = f"Script on {obj}" if obj else "Global Script"

                for switch in self.switches:
                    verb = self.switch_mapping[switch]
                    msgs = []
                    try:
                        getattr(script, switch)()
                    except Exception:
                        logger.log_trace()
                        msgs.append(
                            f"{scripttype} |rNOT|n {verb} |r(see log)|n - "
                            f"{script_key} ({script_typeclass_path})|n"
                        )
                    else:
                        msgs.append(f"{scripttype} {verb} - {script_key} ({script_typeclass_path})")
                caller.msg("\n".join(msgs))
                if "delete" not in self.switches:
                    if script and script.pk:
                        ScriptEvMore(caller, [script], session=self.session)
                    else:
                        caller.msg("Script was deleted automatically.")
        else:
            caller.msg("No scripts found.")

class CmdObjects(COMMAND_DEFAULT_CLASS):
    """
    statistics on objects in the database

    Usage:
      objects [<nr>]

    Gives statictics on objects in database as well as
    a list of <nr> latest objects in database. If not
    given, <nr> defaults to 10.
    """

    key = "@objects"
    locks = "cmd:perm(listobjects) or perm(Builder)"

        nlim = int(self.args) if self.args and self.args.isdigit() else 10
        nobjs = ObjectDB.objects.count()
        Character = class_from_module(settings.BASE_CHARACTER_TYPECLASS)
        nchars = Character.objects.all_family().count()
        Room = class_from_module(settings.BASE_ROOM_TYPECLASS)
        nrooms = Room.objects.all_family().count()
        Exit = class_from_module(settings.BASE_EXIT_TYPECLASS)
        nexits = Exit.objects.all_family().count()
        nother = nobjs - nchars - nrooms - nexits
        nobjs = nobjs or 1  # fix zero-div error with empty database

        # total object sum table
        totaltable = self.styled_table(
            "|wtype|n", "|wcomment|n", "|wcount|n", "|w%|n", border="table", align="l"
        )
        totaltable.align = "l"
        totaltable.add_row(
            "Characters",
            "(BASE_CHARACTER_TYPECLASS + children)",
            nchars,
            "%.2f" % ((float(nchars) / nobjs) * 100),
        )
            "Rooms",
            "(BASE_ROOM_TYPECLASS + children)",
            nrooms,
            "%.2f" % ((float(nrooms) / nobjs) * 100),
        )
            "Exits",
            "(BASE_EXIT_TYPECLASS + children)",
            nexits,
            "%.2f" % ((float(nexits) / nobjs) * 100),
        )
        totaltable.add_row("Other", "", nother, "%.2f" % ((float(nother) / nobjs) * 100))

        # typeclass table
        typetable = self.styled_table(
            "|wtypeclass|n", "|wcount|n", "|w%|n", border="table", align="l"
        )
        typetable.align = "l"
        dbtotals = ObjectDB.objects.get_typeclass_totals()
        for stat in dbtotals:
            typetable.add_row(
                stat.get("typeclass", "<error>"),
                stat.get("count", -1),
                "%.2f" % stat.get("percent", -1),
            )

        # last N table
        objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim) :]
        latesttable = self.styled_table(
            "|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="table"
        )
        latesttable.align = "l"
        for obj in objs:
            latesttable.add_row(
                utils.datetime_format(obj.date_created), obj.dbref, obj.key, obj.path
            )

        string = "\n|wObject subtype totals (out of %i Objects):|n\n%s" % (nobjs, totaltable)
        string += "\n|wObject typeclass distribution:|n\n%s" % typetable
        string += "\n|wLast %s Objects created:|n\n%s" % (min(nobjs, nlim), latesttable)

class CmdTeleport(COMMAND_DEFAULT_CLASS):
    """
    teleport object to another location

    Usage:
      tel/switch [<object> to||=] <target location>

    Examples:
      tel Limbo
      tel/quiet box = Limbo
      tel/tonone box

    Switches:
      quiet  - don't echo leave/arrive messages to the source/target
               locations for the move.
      intoexit - if target is an exit, teleport INTO
                 the exit object instead of to its destination
      tonone - if set, teleport the object to a None-location. If this
               switch is set, <target location> is ignored.
               Note that the only way to retrieve
               an object from a None location is by direct #dbref
               reference. A puppeted object cannot be moved to None.
      loc - teleport object to the target's location instead of its contents

    Teleports an object somewhere. If no object is given, you yourself are
    teleported to the target location.

    To lock an object from being teleported, set its `teleport` lock, it will be
    checked with the caller. To block
    a destination from being teleported to, set the destination's `teleport_here`
    lock - it will be checked with the thing being teleported. Admins and
    higher permissions can always teleport.

    """

    key = "@teleport"
    aliases = "@tel"
    switch_options = ("quiet", "intoexit", "tonone", "loc")
    rhs_split = ("=", " to ")  # Prefer = delimiter, but allow " to " usage.
    locks = "cmd:perm(teleport) or perm(Builder)"

        """
        Breaking out searching here to make this easier to override.

        """
        self.obj_to_teleport = self.caller
        self.destination = None
            self.obj_to_teleport = self.caller.search(self.lhs, global_search=True)
            if not self.obj_to_teleport:
                self.msg("Did not find object to teleport.")
            self.destination = self.caller.search(self.rhs, global_search=True)
        elif self.lhs:
            self.destination = self.caller.search(self.lhs, global_search=True)

        """Performs the teleport"""

        obj_to_teleport = self.obj_to_teleport
        destination = self.destination

        if "tonone" in self.switches:
            # teleporting to None

            if destination:
                # in this case lhs is always the object to teleport
                obj_to_teleport = destination

            if obj_to_teleport.has_account:
                    f"Cannot teleport a puppeted object ({obj_to_teleport.key}, puppeted by"
                    f" {obj_to_teleport.account}) to a None-location."
                )
                return
            caller.msg(f"Teleported {obj_to_teleport} -> None-location.")
            if obj_to_teleport.location and "quiet" not in self.switches:
                obj_to_teleport.location.msg_contents(
                    f"{caller} teleported {obj_to_teleport} into nothingness.", exclude=caller
                )
            obj_to_teleport.location = None
            return

            caller.msg("Usage: teleport[/switches] [<obj> =] <target or (X,Y,Z)>||home")
            return

        if not destination:
            caller.msg("Destination not found.")
            return

        if "loc" in self.switches:
            destination = destination.location
            if not destination:
                caller.msg("Destination has no location.")
                return

        if obj_to_teleport == destination:
            caller.msg("You can't teleport an object inside of itself!")
            return

        if obj_to_teleport == destination.location:
            caller.msg("You can't teleport an object inside something it holds!")
            return

        if obj_to_teleport.location and obj_to_teleport.location == destination:
            caller.msg(f"{obj_to_teleport} is already at {destination}.")
            return

        # check any locks
        if not (caller.permissions.check("Admin") or obj_to_teleport.access(caller, "teleport")):
                f"{obj_to_teleport} 'teleport'-lock blocks you from teleporting it anywhere."
            )
            return

        if not (
            caller.permissions.check("Admin")
            or destination.access(obj_to_teleport, "teleport_here")
        ):
                f"{destination} 'teleport_here'-lock blocks {obj_to_teleport} from moving there."
            )
            return

        # try the teleport
        if not obj_to_teleport.location:
            # teleporting from none-location
            obj_to_teleport.location = destination
            caller.msg(f"Teleported {obj_to_teleport} None -> {destination}")
        elif obj_to_teleport.move_to(
            destination,
            quiet="quiet" in self.switches,
            emit_to_obj=caller,
            use_destination="intoexit" not in self.switches,
            move_type="teleport",
        ):
            if obj_to_teleport == caller:
                caller.msg(f"Teleported to {destination}.")
            else:
                caller.msg(f"Teleported {obj_to_teleport} -> {destination}.")
        else:
            caller.msg("Teleportation failed.")

class CmdTag(COMMAND_DEFAULT_CLASS):
    """
    handles the tags of an object

    Usage:
      tag[/del] <obj> [= <tag>[:<category>]]
      tag/search <tag>[:<category]

    Switches:
      search - return all objects with a given Tag
      del - remove the given tag. If no tag is specified,
            clear all tags on object.

    Manipulates and lists tags on objects. Tags allow for quick
    grouping of and searching for objects.  If only <obj> is given,
    list all tags on the object.  If /search is used, list objects
    with the given tag.
    The category can be used for grouping tags themselves, but it
    should be used with restrain - tags on their own are usually
    enough to for most grouping schemes.
    """

    key = "@tag"
    aliases = ["@tags"]
    options = ("search", "del")
    locks = "cmd:perm(tag) or perm(Builder)"

        """Implement the tag functionality"""

            self.msg("Usage: tag[/switches] <obj> [= <tag>[:<category>]]")
            return
        if "search" in self.switches:
            # search by tag
            tag = self.args
            category = None
            if ":" in tag:
                tag, category = [part.strip() for part in tag.split(":", 1)]
            objs = search.search_tag(tag, category=category)
            nobjs = len(objs)
            if nobjs > 0:
                catstr = (
                    " (category: '|w%s|n')" % category
                    if category
                    else ("" if nobjs == 1 else " (may have different tag categories)")
                )
                matchstr = ", ".join(o.get_display_name(self.caller) for o in objs)

                string = "Found |w%i|n object%s with tag '|w%s|n'%s:\n %s" % (
                    nobjs,
                    "s" if nobjs > 1 else "",
                    tag,
                    catstr,
                    matchstr,
                )
            else:
                string = "No objects found with tag '%s%s'." % (
                    tag,
                    " (category: %s)" % category if category else "",
                )
            return
        if "del" in self.switches:
            # remove one or all tags
            obj = self.caller.search(self.lhs, global_search=True)
            if not obj:
                return
            if self.rhs:
                # remove individual tag
                tag = self.rhs
                category = None
                if ":" in tag:
                    tag, category = [part.strip() for part in tag.split(":", 1)]
                if obj.tags.get(tag, category=category):
                    obj.tags.remove(tag, category=category)
                    string = "Removed tag '%s'%s from %s." % (
                        tag,
                        " (category: %s)" % category if category else "",
                        obj,
                    )
                else:
                    string = "No tag '%s'%s to delete on %s." % (
                        tag,
                        obj,
                    )
            else:
                # no tag specified, clear all tags
                old_tags = [
                    "%s%s" % (tag, " (category: %s)" % category if category else "")
                    for tag, category in obj.tags.all(return_key_and_category=True)
                ]
                if old_tags:
                    obj.tags.clear()
                    string = "Cleared all tags from %s: %s" % (obj, ", ".join(sorted(old_tags)))
                else:
                    string = "No Tags to clear on %s." % obj
            return
        # no search/deletion
            # = is found; command args are of the form obj = tag
            # first search locally, then global
            obj = self.caller.search(self.lhs, quiet=True)
            if not obj:
                obj = self.caller.search(self.lhs, global_search=True)
            else:
            if not obj:
                return
            tag = self.rhs
            # create the tag
            obj.tags.add(tag, category=category)
            string = "Added tag '%s'%s to %s." % (
                tag,
                " (category: %s)" % category if category else "",
                obj,
            )
        else:
            # no = found - list tags on object
            obj = self.caller.search(self.args, quiet=True)
            if not obj:
                obj = self.caller.search(self.args, global_search=True)
            else:
            if not obj:
                return
            tagtuples = obj.tags.all(return_key_and_category=True)
            ntags = len(tagtuples)
            tags = [tup[0] for tup in tagtuples]
            categories = [" (category: %s)" % tup[1] if tup[1] else "" for tup in tagtuples]
            if ntags:
                string = "Tag%s on %s: %s" % (
                    "s" if ntags > 1 else "",
                    obj,
                    ", ".join(sorted("'%s'%s" % (tags[i], categories[i]) for i in range(ntags))),
                )
            else:
                string = f"No tags attached to {obj}."

# helper functions for spawn

class CmdSpawn(COMMAND_DEFAULT_CLASS):
    """
    spawn objects from prototype

    Usage:
      spawn[/noloc] <prototype_key>
      spawn[/noloc] <prototype_dict>

      spawn/search [prototype_keykey][;tag[,tag]]
      spawn/list [tag, tag, ...]
      spawn/list modules    - list only module-based prototypes
      spawn/show [<prototype_key>]
      spawn/update <prototype_key>

      spawn/save <prototype_dict>
      spawn/edit [<prototype_key>]
      olc     - equivalent to spawn/edit

    Switches:
      noloc - allow location to be None if not specified explicitly. Otherwise,
              location will default to caller's current location.
      search - search prototype by name or tags.
      list - list available prototypes, optionally limit by tags.
      show, examine - inspect prototype by key. If not given, acts like list.
      raw - show the raw dict of the prototype as a one-line string for manual editing.
      save - save a prototype to the database. It will be listable by /list.
      delete - remove a prototype from database, if allowed to.
      update - find existing objects with the same prototype_key and update
               them with latest version of given prototype. If given with /save,
               will auto-update all objects with the old version of the prototype
               without asking first.
      edit, menu, olc - create/manipulate prototype in a menu interface.

    Example:
      spawn GOBLIN
      spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"}
      spawn/save {"key": "grunt", prototype: "goblin"};;mobs;edit:all()
    \f
    Dictionary keys:
      |wprototype_parent  |n - name of parent prototype to use. Required if typeclass is
                        not set. Can be a path or a list for multiple inheritance (inherits
                        left to right). If set one of the parents must have a typeclass.
      |wtypeclass  |n - string. Required if prototype_parent is not set.
      |wkey        |n - string, the main object identifier
      |wlocation   |n - this should be a valid object or #dbref
      |whome       |n - valid object or #dbref
      |wdestination|n - only valid for exits (object or dbref)
      |wpermissions|n - string or list of permission strings
      |wlocks      |n - a lock-string
      |waliases    |n - string or list of strings.
      |wndb_|n<name>  - value of a nattribute (ndb_ is stripped)

      |wprototype_key|n   - name of this prototype. Unique. Used to store/retrieve from db
                            and update existing prototyped objects if desired.
      |wprototype_desc|n  - desc of this prototype. Used in listings
      |wprototype_locks|n - locks of this prototype. Limits who may use prototype
      |wprototype_tags|n  - tags of this prototype. Used to find prototype

      any other keywords are interpreted as Attributes and their values.

    The available prototypes are defined globally in modules set in
    settings.PROTOTYPE_MODULES. If spawn is used without arguments it
    displays a list of available prototypes.

    """

    key = "@spawn"
    aliases = ["@olc"]
    switch_options = (
        "noloc",
        "search",
        "list",
        "show",
        "raw",
        "examine",
        "save",
        "delete",
        "menu",
        "olc",
        "update",
        "edit",
    )

    def _search_prototype(self, prototype_key, quiet=False):
        """
        Search for prototype and handle no/multi-match and access.

        Returns a single found prototype or None - in the
        case, the caller has already been informed of the
        search error we need not do any further action.

        """
        prototypes = protlib.search_prototype(prototype_key)
        nprots = len(prototypes)

        # handle the search result
        err = None
        if not prototypes:
            err = f"No prototype named '{prototype_key}' was found."
        elif nprots > 1:
            err = "Found {} prototypes matching '{}':\n  {}".format(
                nprots,
                prototype_key,
                ", ".join(proto.get("prototype_key", "") for proto in prototypes),
            )
        else:
            # we have a single prototype, check access
            prototype = prototypes[0]
            if not self.caller.locks.check_lockstring(
                self.caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True
            ):
                err = "You don't have access to use this prototype."

        if err:
            # return None on any error
            if not quiet:
                self.msg(err)
            return
        return prototype

    def _parse_prototype(self, inp, expect=dict):
        """
        Parse a prototype dict or key from the input and convert it safely
        into a dict if appropriate.

        Args:
            inp (str): The input from user.
            expect (type, optional):
        Returns:
            prototype (dict, str or None): The parsed prototype. If None, the error
                was already reported.

        """
        eval_err = None
        try:
            prototype = _LITERAL_EVAL(inp)
        except (SyntaxError, ValueError) as err:
            # treat as string
            eval_err = err
            prototype = utils.to_str(inp)
        finally:
            # it's possible that the input was a prototype-key, in which case
            # it's okay for the LITERAL_EVAL to fail. Only if the result does not
            # match the expected type do we have a problem.
            if not isinstance(prototype, expect):
                if eval_err:
                    string = (
                        f"{inp}\n{eval_err}\n|RCritical Python syntax error in argument. Only"
                        " primitive Python structures are allowed. \nMake sure to use correct"
                        " Python syntax. Remember especially to put quotes around all strings"
                        " inside lists and dicts.|n For more advanced uses, embed funcparser"
                        " callables ($funcs) in the strings."
                    )
                else:
                    string = f"Expected {expect}, got {type(prototype)}."
                self.msg(string)
                return

        if expect == dict:
            # an actual prototype. We need to make sure it's safe,
            # so don't allow exec.
            # TODO: Exec support is deprecated. Remove completely for 1.0.
            if "exec" in prototype and not self.caller.check_permstring("Developer"):
                self.msg("Spawn aborted: You are not allowed to use the 'exec' prototype key.")
                return
            try:
                # we homogenize the prototype first, to be more lenient with free-form
                protlib.validate_prototype(protlib.homogenize_prototype(prototype))
            except RuntimeError as err:
                self.msg(str(err))
                return

    def _get_prototype_detail(self, query=None, prototypes=None):
        """
        Display the detailed specs of one or more prototypes.

        Args:
            query (str, optional): If this is given and `prototypes` is not, search for
                the prototype(s) by this query. This may be a partial query which
                may lead to multiple matches, all being displayed.
            prototypes (list, optional): If given, ignore `query` and only show these
                prototype-details.
        Returns:
            display (str, None): A formatted string of one or more prototype details.
                If None, the caller was already informed of the error.

        """
            # we need to query. Note that if query is None, all prototypes will
            # be returned.
            prototypes = protlib.search_prototype(key=query)
        if prototypes:
            return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes)
        elif query:
            self.msg(f"No prototype named '{query}' was found.")
        else:
            self.msg("No prototypes found.")

    def _list_prototypes(self, key=None, tags=None):
        """Display prototypes as a list, optionally limited by key/tags."""
        protlib.list_prototypes(self.caller, key=key, tags=tags, session=self.session)

    def _update_existing_objects(self, caller, prototype_key, quiet=False):
        """
        Update existing objects (if any) with this prototype-key to the latest
        prototype version.

        Args:
            caller (Object): This is necessary for @interactive to work.
            prototype_key (str): The prototype to update.
            quiet (bool, optional): If set, don't report to user if no
                old objects were found to update.
        Returns:
            n_updated (int): Number of updated objects.

        """
        prototype = self._search_prototype(prototype_key)
        if not prototype:
            return

        existing_objects = protlib.search_objects_with_prototype(prototype_key)
        if not existing_objects:
                caller.msg("No existing objects found with an older version of this prototype.")
            return

        if existing_objects:
            n_existing = len(existing_objects)
            slow = " (note that this may be slow)" if n_existing > 10 else ""
            string = (
                f"There are {n_existing} existing object(s) with an older version "
                f"of prototype '{prototype_key}'. Should it be re-applied to them{slow}? [Y]/N"
            )
            answer = yield (string)
            if answer.lower() in ["n", "no"]:
                    "|rNo update was done of existing objects. "
                    "Use spawn/update <key> to apply later as needed.|n"
                )
                return
            try:
                n_updated = spawner.batch_update_objects_with_prototype(
                    prototype,
                    objects=existing_objects,
                )
            caller.msg(f"{n_updated} objects were updated.")
        return

    def _parse_key_desc_tags(self, argstring, desc=True):
        """
        Parse ;-separated input list.
        """
        key, desc, tags = "", "", []
        if ";" in argstring:
            parts = [part.strip().lower() for part in argstring.split(";")]
            if len(parts) > 1 and desc:
                key = parts[0]
                desc = parts[1]
                tags = parts[2:]
            else:
                tags = parts[1:]
        else:
            key = argstring.strip().lower()
        return key, desc, tags

        """Implements the spawner"""

        noloc = "noloc" in self.switches

        # run the menu/olc
        if (
            self.cmdstring == "olc"
            or "menu" in self.switches
            or "olc" in self.switches
            or "edit" in self.switches
        ):
            # OLC menu mode
            prototype = None
            if self.lhs:
                prototype_key = self.lhs
                prototype = self._search_prototype(prototype_key)
                if not prototype:
                    return
            olc_menus.start_olc(caller, session=self.session, prototype=prototype)
            return

            # query for a key match. The arg is a search query or nothing.

            if not self.args:
                # an empty search returns the full list
                self._list_prototypes()
                return

            # search for key;tag combinations
            key, _, tags = self._parse_key_desc_tags(self.args, desc=False)
            self._list_prototypes(key, tags)
            return

        if "raw" in self.switches:
            # query for key match and return the prototype as a safe one-liner string.
                caller.msg("You need to specify a prototype-key to get the raw data for.")
            prototype = self._search_prototype(self.args)
            if not prototype:
                return
            caller.msg(str(prototype))
            return

            # show a specific prot detail. The argument is a search query or empty.
                # we don't show the list of all details, that's too spammy.
                caller.msg("You need to specify a prototype-key to show.")
                return

            detail_string = self._get_prototype_detail(self.args)
            if not detail_string:
                return
            caller.msg(detail_string)
            return

        if "list" in self.switches:
            # for list, all optional arguments are tags.
            tags = self.lhslist
            err = self._list_prototypes(tags=tags)
            if err:
                    "No prototypes found with prototype-tag(s): {}".format(
                        list_to_string(tags, "or")
                    )
                )
            return

        if "save" in self.switches:
            # store a prototype to the database store
                    "Usage: spawn/save [<key>[;desc[;tag,tag[,...][;lockstring]]]] ="
                    " <prototype_dict>"
                )
                return
                # input on the form key = prototype
                prototype_key, prototype_desc, prototype_tags = self._parse_key_desc_tags(self.lhs)
                prototype_key = None if not prototype_key else prototype_key
                prototype_desc = None if not prototype_desc else prototype_desc
                prototype_tags = None if not prototype_tags else prototype_tags
                prototype_input = self.rhs.strip()
            else:
                prototype_key = prototype_desc = None
                prototype_tags = None
                prototype_input = self.lhs.strip()

            # handle parsing
            prototype = self._parse_prototype(prototype_input)
                return

            prot_prototype_key = prototype.get("prototype_key")

            if not (prototype_key or prot_prototype_key):
                    "A prototype_key must be given, either as `prototype_key = <prototype>` "
                    "or as a key 'prototype_key' inside the prototype structure."
                )
                return

            if prototype_key is None:
                prototype_key = prot_prototype_key

            if prot_prototype_key != prototype_key:
                caller.msg("(Replacing `prototype_key` in prototype with given key.)")
                prototype["prototype_key"] = prototype_key

            if prototype_desc is not None and prot_prototype_key != prototype_desc:
                caller.msg("(Replacing `prototype_desc` in prototype with given desc.)")
                prototype["prototype_desc"] = prototype_desc
            if prototype_tags is not None and prototype.get("prototype_tags") != prototype_tags:
                caller.msg("(Replacing `prototype_tags` in prototype with given tag(s))")
                prototype["prototype_tags"] = prototype_tags

            string = ""
            # check for existing prototype (exact match)
            old_prototype = self._search_prototype(prototype_key, quiet=True)

            diff = spawner.prototype_diff(old_prototype, prototype, homogenize=True)
            diffstr = spawner.format_diff(diff)
            new_prototype_detail = self._get_prototype_detail(prototypes=[prototype])

            if old_prototype:
                if not diffstr:
                    string = f"|yAlready existing Prototype:|n\n{new_prototype_detail}\n"
                    question = (
                        "\nThere seems to be no changes. Do you still want to (re)save? [Y]/N"
                    )
                else:
                    string = (
                        f'|yExisting prototype "{prototype_key}" found. Change:|n\n{diffstr}\n'
                        f"|yNew changed prototype:|n\n{new_prototype_detail}"
                    )
                        "\n|yDo you want to apply the change to the existing prototype?|n [Y]/N"
                    )
            else:
                string = f"|yCreating new prototype:|n\n{new_prototype_detail}"
                question = "\nDo you want to continue saving? [Y]/N"

            answer = yield (string + question)
                caller.msg("|rSave cancelled.|n")
                return

            # all seems ok. Try to save.
            try:
                prot = protlib.save_prototype(prototype)
                if not prot:
                    caller.msg("|rError saving:|R {}.|n".format(prototype_key))
                    return
            except protlib.PermissionError as err:
                caller.msg("|rError saving:|R {}|n".format(err))
                return
            caller.msg("|gSaved prototype:|n {}".format(prototype_key))

            # check if we want to update existing objects

            self._update_existing_objects(self.caller, prototype_key, quiet=True)
            return

            # all switches beyond this point gets a common non-arg return
            ncount = len(protlib.search_prototype())
                "Usage: spawn <prototype-key> or {{key: value, ...}}"
                f"\n ({ncount} existing prototypes. Use /list to inspect)"
            )
            return

        if "delete" in self.switches:
            # remove db-based prototype
            prototype_detail = self._get_prototype_detail(self.args)
            if not prototype_detail:
                return

            string = f"|rDeleting prototype:|n\n{prototype_detail}"
            question = "\nDo you want to continue deleting? [Y]/N"
                caller.msg("|rDeletion cancelled.|n")
                return

            try:
                success = protlib.delete_prototype(self.args)
                retmsg = f"|rError deleting:|R {err}|n"
            else:
                retmsg = (
                    "Deletion successful"
                    if success
                    else "Deletion failed (does the prototype exist?)"
                )
            caller.msg(retmsg)
            return

        if "update" in self.switches:
            # update existing prototypes
            prototype_key = self.args.strip().lower()
            self._update_existing_objects(self.caller, prototype_key)
            return

        # If we get to this point, we use not switches but are trying a
        # direct creation of an object from a given prototype or -key

        prototype = self._parse_prototype(
            self.args, expect=dict if self.args.strip().startswith("{") else str
        )
            # this will only let through dicts or strings
            return

        key = "<unnamed>"
        if isinstance(prototype, str):
            # A prototype key we are looking to apply
            prototype_key = prototype
            prototype = self._search_prototype(prototype_key)

                return

        # proceed to spawning
        try:
            for obj in spawner.spawn(prototype, caller=self.caller):
                self.msg("Spawned %s." % obj.get_display_name(self.caller))
                if not prototype.get("location") and not noloc:
                    # we don't hardcode the location in the prototype (unless the user
                    # did so manually) - that would lead to it having to be 'removed' every
                    # time we try to update objects with this prototype in the future.
                    obj.location = caller.location
        except RuntimeError as err:
            caller.msg(err)

# -*- encoding: utf-8 -*-
"""
General helper functions that don't fit neatly under any given category.

They provide some useful string and conversion methods that might
be of use when designing your own game.

"""
import gc
import importlib
import importlib.machinery
import importlib.util
import ipaddress
import os
import random
import re
import sys
import textwrap
import threading
import traceback
from ast import literal_eval
from collections import OrderedDict, defaultdict
from inspect import getmembers, getmodule, getmro, ismodule, trace
from os.path import join as osjoin
from string import punctuation
from unicodedata import east_asian_width

from django.apps import apps
from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.validators import validate_email as django_validate_email
from django.utils.html import strip_tags
from simpleeval import simple_eval
from twisted.internet import reactor, threads
from twisted.internet.defer import returnValue  # noqa - used as import target

from evennia.utils import logger

_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
_EVENNIA_DIR = settings.EVENNIA_DIR
_GAME_DIR = settings.GAME_DIR
_IS_MAIN_THREAD = threading.current_thread().name == "MainThread"

ENCODINGS = settings.ENCODINGS

_TASK_HANDLER = None
_TICKER_HANDLER = None
_STRIP_UNSAFE_TOKENS = None
_ANSISTRING = None

def is_iter(obj):
    """
    Checks if an object behaves iterably.

    Args:
        obj (any): Entity to check for iterability.

    Returns:
        is_iterable (bool): If `obj` is iterable or not.

    Notes:
        Strings are *not* accepted as iterable (although they are
        actually iterable), since string iterations are usually not
        what we want to do with a string.

    """
    if isinstance(obj, (str, bytes)):

    try:
        return iter(obj) and True
    except TypeError:

def make_iter(obj):
    """
    Makes sure that the object is always iterable.

    Args:
        obj (any): Object to make iterable.

    Returns:
        iterable (list or iterable): The same object
            passed-through or made iterable.

    """
    return not is_iter(obj) and [obj] or obj

def wrap(text, width=None, indent=0):
    """
    Safely wrap text to a certain number of characters.

    Args:
        text (str): The text to wrap.
        width (int, optional): The number of characters to wrap to.
        indent (int): How much to indent each line (with whitespace).

    Returns:
        text (str): Properly wrapped text.

    """
    width = width if width else settings.CLIENT_DEFAULT_WIDTH
    if not text:
        return ""
    indent = " " * indent
    return to_str(textwrap.fill(text, width, initial_indent=indent, subsequent_indent=indent))

# alias - fill
fill = wrap

def pad(text, width=None, align="c", fillchar=" "):
    """
    Pads to a given width.

    Args:
        text (str): Text to pad.
        width (int, optional): The width to pad to, in characters.
        align (str, optional): This is one of 'c', 'l' or 'r' (center,
            left or right).
        fillchar (str, optional): The character to fill with.

    Returns:
        text (str): The padded text.

    """
    align = align if align in ("c", "l", "r") else "c"
    fillchar = fillchar[0] if fillchar else " "
    if align == "l":
        return text.ljust(width, fillchar)
    elif align == "r":
        return text.rjust(width, fillchar)
    else:
        return text.center(width, fillchar)

def crop(text, width=None, suffix="[...]"):
    """
    Crop text to a certain width, throwing away text from too-long
    lines.

    Args:
        text (str): Text to crop.
        width (int, optional): Width of line to crop, in characters.
        suffix (str, optional): This is appended to the end of cropped
            lines to show that the line actually continues. Cropping
            will be done so that the suffix will also fit within the
            given width. If width is too small to fit both crop and
            suffix, the suffix will be dropped.

    Returns:
        text (str): The cropped text.

    """
    ltext = len(text)
    if ltext <= width:
        return text
    else:
        lsuffix = len(suffix)
        text = text[:width] if lsuffix >= width else "%s%s" % (text[: width - lsuffix], suffix)
        return to_str(text)

def dedent(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, 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.

    Notes:
        This is useful for preserving triple-quoted string indentation
        while still shifting it all to be next to the left edge of the
        display.

    """
        return ""
    if indent is not None:
        lines = text.split("\n")
        ind = " " * indent
        indline = "\n" + ind
        return ind + indline.join(line.strip() for line in lines)
    elif baseline_index is None:
        return textwrap.dedent(text)
    else:
        baseline = lines[baseline_index]
        spaceremove = len(baseline) - len(baseline.lstrip(" "))
        return "\n".join(
            line[min(spaceremove, len(line) - len(line.lstrip(" "))) :] for line in lines
        )

def justify(text, width=None, align="l", indent=0, fillchar=" "):
    """
    Fully justify a text so that it fits inside `width`. When using
    full justification (default) this will be done by padding between
    words with extra whitespace where necessary. Paragraphs will
    be retained.

    Args:
        text (str): Text to justify.
        width (int, optional): The length of each line, in characters.
        align (str, optional): The alignment, 'l', 'c', 'r', 'f' or 'a'
            for left, center, right, full justification. The 'a' stands for
            'absolute' and means the text will be returned unmodified.
        indent (int, optional): Number of characters indentation of
            entire justified text block.
        fillchar (str): The character to use to fill. Defaults to empty space.

    Returns:
        justified (str): The justified and indented block of text.

    """
    # we need to retain ansitrings
    global _ANSISTRING
    if not _ANSISTRING:
        from evennia.utils.ansi import ANSIString as _ANSISTRING

    is_ansi = isinstance(text, _ANSISTRING)
    lb = _ANSISTRING("\n") if is_ansi else "\n"

    def _process_line(line):
        """
        helper function that distributes extra spaces between words. The number
        of gaps is nwords - 1 but must be at least 1 for single-word lines. We
        distribute odd spaces to one of the gaps.
        """
        line_rest = width - (wlen + ngaps)

        gap = _ANSISTRING(" ") if is_ansi else " "

        if line_rest > 0:
            if align == "l":
                if line[-1] == "\n\n":
                    line[-1] = sp * (line_rest - 1) + "\n" + sp * width + "\n" + sp * width
                else:
                    line[-1] += sp * line_rest
            elif align == "r":
                line[0] = sp * line_rest + line[0]
            elif align == "c":
                pad = sp * (line_rest // 2)
                line[0] = pad + line[0]
                    line[-1] += (
                        pad + sp * (line_rest % 2 - 1) + "\n" + sp * width + "\n" + sp * width
                    )
                else:
                    line[-1] = line[-1] + pad + sp * (line_rest % 2)
            else:  # align 'f'
                gap += sp * (line_rest // max(1, ngaps))
                rest_gap = line_rest % max(1, ngaps)
                for i in range(rest_gap):
                    line[i] += sp
        elif not any(line):
            return [sp * width]
        return gap.join(line)

    width = width if width is not None else settings.CLIENT_DEFAULT_WIDTH
    sp = fillchar

    if align == "a":
        # absolute mode - just crop or fill to width
        abs_lines = []
        for line in text.split("\n"):
            nlen = m_len(line)
            if m_len(line) < width:
                line += sp * (width - nlen)
            else:
                line = crop(line, width=width, suffix="")
            abs_lines.append(line)
        return lb.join(abs_lines)

    # all other aligns requires splitting into paragraphs and words

    # split into paragraphs and words
    paragraphs = [text]  # re.split("\n\s*?\n", text, re.MULTILINE)
    words = []
    for ip, paragraph in enumerate(paragraphs):
        if ip > 0:
            words.append(("\n", 0))
        words.extend((word, m_len(word)) for word in paragraph.split())

    if not words:
        # Just whitespace!
        return sp * width

    ngaps = 0
    wlen = 0
    line = []
    lines = []

    while words:
        if not line:
            # start a new line
            word = words.pop(0)
            wlen = word[1]
            line.append(word[0])
        elif (words[0][1] + wlen + ngaps) >= width:
            # next word would exceed word length of line + smallest gaps
            lines.append(_process_line(line))
            ngaps, wlen, line = 0, 0, []
        else:
            # put a new word on the line
            if word[1] == 0:
                # a new paragraph, process immediately
                lines.append(_process_line(line))
                ngaps, wlen, line = 0, 0, []
            else:
                wlen += word[1]
                ngaps += 1

    if line:  # catch any line left behind
        lines.append(_process_line(line))
    indentstring = sp * indent
    out = lb.join([indentstring + line for line in lines])
    return lb.join([indentstring + line for line in lines])

def columnize(string, columns=2, spacing=4, align="l", width=None):
    """
    Break a string into a number of columns, using as little
    vertical space as possible.

    Args:
        string (str): The string to columnize.
        columns (int, optional): The number of columns to use.
        spacing (int, optional): How much space to have between columns.
        width (int, optional): The max width of the columns.
            Defaults to client's default width.

    Returns:
        columns (str): Text divided into columns.

    Raises:
        RuntimeError: If given invalid values.

    """
    columns = max(1, columns)
    spacing = max(1, spacing)

    w_spaces = (columns - 1) * spacing
    w_txt = max(1, width - w_spaces)

    if w_spaces + columns > width:  # require at least 1 char per column
        raise RuntimeError("Width too small to fit columns")

    colwidth = int(w_txt / (1.0 * columns))

    # first make a single column which we then split
    onecol = justify(string, width=colwidth, align=align)
    onecol = onecol.split("\n")

    nrows, dangling = divmod(len(onecol), columns)
    nrows = [nrows + 1 if i < dangling else nrows for i in range(columns)]

    height = max(nrows)
    cols = []
    istart = 0
    for irows in nrows:
        cols.append(onecol[istart : istart + irows])
        istart = istart + irows
    for col in cols:
        if len(col) < height:
            col.append(" " * colwidth)

    sep = " " * spacing
    rows = []
    for irow in range(height):
        rows.append(sep.join(col[irow] for col in cols))

    return "\n".join(rows)

def iter_to_str(iterable, sep=",", endsep=", and", addquote=False):
    """
    This pretty-formats an iterable list as string output, adding an optional
    alternative separator to the second to last entry.  If `addquote`
    is `True`, the outgoing strings will be surrounded by quotes.

    Args:
        iterable (any): Usually an iterable to print. Each element must be possible to
            present with a string. Note that if this is a generator, it will be
            consumed by this operation.
        sep (str, optional): The string to use as a separator for each item in the iterable.
        endsep (str, optional): The last item separator will be replaced with this value.
        addquote (bool, optional): This will surround all outgoing
            values with double quotes.

    Returns:
        str: The list represented as a string.

    Notes:
        Default is to use 'Oxford comma', like 1, 2, 3, and 4.

    Examples:

        ```python
        >>> iter_to_string([1,2,3], endsep=',')
        '1, 2, 3'
        >>> iter_to_string([1,2,3], endsep='')
        '1, 2 3'
        >>> iter_to_string([1,2,3], ensdep='and')
        '1, 2 and 3'
        >>> iter_to_string([1,2,3], sep=';', endsep=';')
        '1; 2; 3'
        >>> iter_to_string([1,2,3], addquote=True)
        '"1", "2", and "3"'
        ```

    """
    iterable = list(make_iter(iterable))
    if not iterable:
        return ""
    len_iter = len(iterable)

    if addquote:
        iterable = tuple(f'"{val}"' for val in iterable)
    else:
        iterable = tuple(str(val) for val in iterable)

    if endsep:
        if endsep.startswith(sep) and endsep != sep:
            # oxford comma alternative
            endsep = endsep[1:] if len_iter < 3 else endsep
        elif endsep[0] not in punctuation:
            # add a leading space if endsep is a word
            endsep = " " + str(endsep).strip()

    # also add a leading space if separator is a word
    if sep not in punctuation:
        sep = " " + sep

    if len_iter == 1:
        return str(iterable[0])
    elif len_iter == 2:
        return f"{endsep} ".join(str(v) for v in iterable)
    else:
        return f"{sep} ".join(str(v) for v in iterable[:-1]) + f"{endsep} {iterable[-1]}"

# legacy aliases
list_to_string = iter_to_str
iter_to_string = iter_to_str

def wildcard_to_regexp(instring):
    """
    Converts a player-supplied string that may have wildcards in it to
    regular expressions. This is useful for name matching.

    Args:
        instring (string): A string that may potentially contain
            wildcards (`*` or `?`).

    Returns:
        regex (str): A string where wildcards were replaced with
            regular expressions.

    """
    regexp_string = ""

    # If the string starts with an asterisk, we can't impose the beginning of
    # string (^) limiter.
    if instring[0] != "*":
        regexp_string += "^"

    # Replace any occurances of * or ? with the appropriate groups.
    regexp_string += instring.replace("*", "(.*)").replace("?", "(.{1})")

    # If there's an asterisk at the end of the string, we can't impose the
    # end of string ($) limiter.
    if instring[-1] != "*":
        regexp_string += "$"

    return regexp_string

def time_format(seconds, style=0):
    """
    Function to return a 'prettified' version of a value in seconds.

    Args:
        seconds (int): Number if seconds to format.
        style (int): One of the following styles:
            0. "1d 08:30"
            1. "1d"
            2. "1 day, 8 hours, 30 minutes"
            3. "1 day, 8 hours, 30 minutes, 10 seconds"
            4. highest unit (like "3 years" or "8 months" or "1 second")
    Returns:
        timeformatted (str): A pretty time string.
    """
    if seconds < 0:
        seconds = 0
    else:
        # We'll just use integer math, no need for decimal precision.
        seconds = int(seconds)

    days = seconds // 86400
    seconds -= days * 86400
    hours = seconds // 3600
    seconds -= hours * 3600
    minutes = seconds // 60
    seconds -= minutes * 60

    retval = ""
    if style == 0:
        """
        Standard colon-style output.
        """
        if days > 0:
            retval = "%id %02i:%02i" % (days, hours, minutes)
        else:
            retval = "%02i:%02i" % (hours, minutes)
        return retval

    elif style == 1:
        """
        Simple, abbreviated form that only shows the highest time amount.
        """
        if days > 0:
            return "%id" % (days,)
        elif hours > 0:
            return "%ih" % (hours,)
        elif minutes > 0:
            return "%im" % (minutes,)
        else:
            return "%is" % (seconds,)
    elif style == 2:
        """
        Full-detailed, long-winded format. We ignore seconds.
        """
        days_str = hours_str = ""
        minutes_str = "0 minutes"

        if days > 0:
            if days == 1:
                days_str = "%i day, " % days
            else:
                days_str = "%i days, " % days
        if days or hours > 0:
            if hours == 1:
                hours_str = "%i hour, " % hours
            else:
                hours_str = "%i hours, " % hours
        if hours or minutes > 0:
            if minutes == 1:
                minutes_str = "%i minute " % minutes
            else:
                minutes_str = "%i minutes " % minutes
        retval = "%s%s%s" % (days_str, hours_str, minutes_str)
    elif style == 3:
        """
        Full-detailed, long-winded format. Includes seconds.
        """
        days_str = hours_str = minutes_str = seconds_str = ""
        if days > 0:
            else:
            else:
            else:
        if minutes or seconds > 0:
            if seconds == 1:
                seconds_str = "%i second " % seconds
            else:
                seconds_str = "%i seconds " % seconds
        retval = "%s%s%s%s" % (days_str, hours_str, minutes_str, seconds_str)
    elif style == 4:
        """
        Only return the highest unit.
        """
        if days >= 730:  # Several years
            return "{} years".format(days // 365)
        elif days >= 365:  # One year
            return "a year"
        elif days >= 62:  # Several months
            return "{} months".format(days // 31)
        elif days >= 31:  # One month
            return "a month"
        elif days >= 2:  # Several days
            return "{} days".format(days)
        elif days > 0:
            return "a day"
        elif hours >= 2:  # Several hours
            return "{} hours".format(hours)
        elif hours > 0:  # One hour
            return "an hour"
        elif minutes >= 2:  # Several minutes
            return "{} minutes".format(minutes)
        elif minutes > 0:  # One minute
            return "a minute"
        elif seconds >= 2:  # Several seconds
            return "{} seconds".format(seconds)
        elif seconds == 1:
            return "a second"
        else:
            return "0 seconds"
    else:
        raise ValueError("Unknown style for time format: %s" % style)

    return retval.strip()

def datetime_format(dtobj):
    """
    Pretty-prints the time since a given time.

    Args:
        dtobj (datetime): An datetime object, e.g. from Django's
            `DateTimeField`.

    Returns:
        deltatime (str): A string describing how long ago `dtobj`
            took place.

    """

    now = timezone.now()

    if dtobj.year < now.year:
        # another year (Apr 5, 2019)
        timestring = dtobj.strftime(f"%b {dtobj.day}, %Y")
    elif dtobj.date() < now.date():
        # another date, same year  (Apr 5)
        timestring = dtobj.strftime(f"%b {dtobj.day}")
    elif dtobj.hour < now.hour - 1:
        # same day, more than 1 hour ago (10:45)
        timestring = dtobj.strftime("%H:%M")
    else:
        # same day, less than 1 hour ago (10:45:33)
        timestring = dtobj.strftime("%H:%M:%S")
    return timestring

def host_os_is(osname):
    """
    Check to see if the host OS matches the query.

    Args:
        osname (str): Common names are "posix" (linux/unix/mac) and
            "nt" (windows).

    Args:
        is_os (bool): If the os matches or not.

    """
    return os.name == osname

def get_evennia_version(mode="long"):
    """
    Helper method for getting the current evennia version.

    Args:
        mode (str, optional): One of:
            - long: 0.9.0 rev342453534
            - short: 0.9.0
            - pretty: Evennia 0.9.0

    Returns:
        version (str): The version string.

    """
    import evennia

    vers = evennia.__version__
    if mode == "short":
        return vers.split()[0].strip()
    elif mode == "pretty":
        vers = vers.split()[0].strip()
        return f"Evennia {vers}"
    else:  # mode "long":
        return vers

def pypath_to_realpath(python_path, file_ending=".py", pypath_prefixes=None):
    """
    Converts a dotted Python path to an absolute path under the
    Evennia library directory or under the current game directory.

    Args:
        python_path (str): A dot-python path
        file_ending (str): A file ending, including the period.
        pypath_prefixes (list): A list of paths to test for existence. These
            should be on python.path form. EVENNIA_DIR and GAME_DIR are automatically
            checked, they need not be added to this list.

    Returns:
        abspaths (list): All existing, absolute paths created by
            converting `python_path` to an absolute paths and/or
            prepending `python_path` by `settings.EVENNIA_DIR`,
            `settings.GAME_DIR` and by`pypath_prefixes` respectively.

    Notes:
        This will also try a few combinations of paths to allow cases
        where pypath is given including the "evennia." or "mygame."
        prefixes.

    """
    path = python_path.strip().split(".")
    plong = osjoin(*path) + file_ending
    pshort = (
        osjoin(*path[1:]) + file_ending if len(path) > 1 else plong
    )  # in case we had evennia. or mygame.
    prefixlong = (
        [osjoin(*ppath.strip().split(".")) for ppath in make_iter(pypath_prefixes)]
        if pypath_prefixes
        else []
    )
    prefixshort = (
        [
            osjoin(*ppath.strip().split(".")[1:])
            for ppath in make_iter(pypath_prefixes)
            if len(ppath.strip().split(".")) > 1
        ]
        else []
    )
    paths = (
        [plong]
        + [osjoin(_EVENNIA_DIR, prefix, plong) for prefix in prefixlong]
        + [osjoin(_GAME_DIR, prefix, plong) for prefix in prefixlong]
        + [osjoin(_EVENNIA_DIR, prefix, plong) for prefix in prefixshort]
        + [osjoin(_GAME_DIR, prefix, plong) for prefix in prefixshort]
        + [osjoin(_EVENNIA_DIR, plong), osjoin(_GAME_DIR, plong)]
        + [osjoin(_EVENNIA_DIR, prefix, pshort) for prefix in prefixshort]
        + [osjoin(_GAME_DIR, prefix, pshort) for prefix in prefixshort]
        + [osjoin(_EVENNIA_DIR, prefix, pshort) for prefix in prefixlong]
        + [osjoin(_GAME_DIR, prefix, pshort) for prefix in prefixlong]
        + [osjoin(_EVENNIA_DIR, pshort), osjoin(_GAME_DIR, pshort)]
    )
    # filter out non-existing paths
    return list(set(p for p in paths if os.path.isfile(p)))

def dbref(inp, reqhash=True):
    """
    Converts/checks if input is a valid dbref.

    Args:
        inp (int, str): A database ref on the form N or #N.
        reqhash (bool, optional): Require the #N form to accept
            input as a valid dbref.

    Returns:
        dbref (int or None): The integer part of the dbref or `None`
            if input was not a valid dbref.

    """
    if reqhash:
        num = (
            int(inp.lstrip("#"))
            if (isinstance(inp, str) and inp.startswith("#") and inp.lstrip("#").isdigit())
            else None
        )
        return num if isinstance(num, int) and num > 0 else None
    elif isinstance(inp, str):
        inp = inp.lstrip("#")
        return int(inp) if inp.isdigit() and int(inp) > 0 else None
    else:
        return inp if isinstance(inp, int) else None

def dbref_to_obj(inp, objclass, raise_errors=True):
    """
    Convert a #dbref to a valid object.

    Args:
        inp (str or int): A valid #dbref.
        objclass (class): A valid django model to filter against.
        raise_errors (bool, optional): Whether to raise errors
            or return `None` on errors.

    Returns:
        obj (Object or None): An entity loaded from the dbref.

    Raises:
        Exception: If `raise_errors` is `True` and
            `objclass.objects.get(id=dbref)` did not return a valid
            object.

    """
    dbid = dbref(inp)
    if not dbid:
        # we only convert #dbrefs
        return inp
    try:
        if dbid < 0:
    except ValueError:

    # if we get to this point, inp is an integer dbref; get the matching object
    try:
        return objclass.objects.get(id=dbid)
        if raise_errors:
            raise
        return inp

# legacy alias
dbid_to_obj = dbref_to_obj

# some direct translations for the latinify
_UNICODE_MAP = {
    "EM DASH": "-",
    "FIGURE DASH": "-",
    "EN DASH": "-",
    "HORIZONTAL BAR": "-",
    "HORIZONTAL ELLIPSIS": "...",
    "LEFT SINGLE QUOTATION MARK": "'",
    "RIGHT SINGLE QUOTATION MARK": "'",
    "LEFT DOUBLE QUOTATION MARK": '"',
    "RIGHT DOUBLE QUOTATION MARK": '"',
}

def latinify(string, default="?", pure_ascii=False):
    """
    Convert a unicode string to "safe" ascii/latin-1 characters.
    This is used as a last resort when normal encoding does not work.

        string (str): A string to convert to 'safe characters' convertible
            to an latin-1 bytestring later.
        default (str, optional): Characters resisting mapping will be replaced
            with this character or string. The intent is to apply an encode operation
            on the string soon after.

    Returns:
        string (str): A 'latinified' string where each unicode character has been
            replaced with a 'safe' equivalent available in the ascii/latin-1 charset.
    Notes:
        This is inspired by the gist by Ricardo Murri:
            https://gist.github.com/riccardomurri/3c3ccec30f037be174d3

    """

    from unicodedata import name

    if isinstance(string, bytes):
        string = string.decode("utf8")

    converted = []
    for unich in iter(string):
        try:
            ch = unich.encode("utf8").decode("ascii")
        except UnicodeDecodeError:
            # deduce a latin letter equivalent from the Unicode data
            # point name; e.g., since `name(u'á') == 'LATIN SMALL
            # LETTER A WITH ACUTE'` translate `á` to `a`.  However, in
            # some cases the unicode name is still "LATIN LETTER"
            # although no direct equivalent in the Latin alphabet
            # exists (e.g., Þ, "LATIN CAPITAL LETTER THORN") -- we can
            # avoid these cases by checking that the letter name is
            # composed of one letter only.
            # We also supply some direct-translations for some particular
            # common cases.
            what = name(unich)
            if what in _UNICODE_MAP:
                ch = _UNICODE_MAP[what]
            else:
                what = what.split()
                if what[0] == "LATIN" and what[2] == "LETTER" and len(what[3]) == 1:
                    ch = what[3].lower() if what[1] == "SMALL" else what[3].upper()
                else:
                    ch = default
        converted.append(chr(ord(ch)))
    return "".join(converted)

def to_bytes(text, session=None):
    """
    Try to encode the given text to bytes, using encodings from settings or from Session. Will
    always return a bytes, even if given something that is not str or bytes.

    Args:
        text (any): The text to encode to bytes. If bytes, return unchanged. If not a str, convert
            to str before converting.
        session (Session, optional): A Session to get encoding info from. Will try this before
            falling back to settings.ENCODINGS.

    Returns:
        encoded_text (bytes): the encoded text following the session's protocol flag followed by the
            encodings specified in settings.ENCODINGS.  If all attempt fail, log the error and send
            the text with "?" in place of problematic characters.  If the specified encoding cannot
            be found, the protocol flag is reset to utf-8.  In any case, returns bytes.

    Notes:
        If `text` is already bytes, return it as is.

    """
    if isinstance(text, bytes):
    if not isinstance(text, str):
        # convert to a str representation before encoding
        try:
            text = str(text)
            text = repr(text)

    default_encoding = session.protocol_flags.get("ENCODING", "utf-8") if session else "utf-8"
    try:
        return text.encode(default_encoding)
    except (LookupError, UnicodeEncodeError):
        for encoding in settings.ENCODINGS:
            try:
                return text.encode(encoding)
            except (LookupError, UnicodeEncodeError):
                pass
        # no valid encoding found. Replace unconvertable parts with ?
        return text.encode(default_encoding, errors="replace")

def to_str(text, session=None):
    """
    Try to decode a bytestream to a python str, using encoding schemas from settings
    or from Session. Will always return a str(), also if not given a str/bytes.

    Args:
        text (any): The text to encode to bytes. If a str, return it. If also not bytes, convert
            to str using str() or repr() as a fallback.

    Returns:
        decoded_text (str): The decoded text.

    Notes:
        If `text` is already str, return it as is.
    """
    if isinstance(text, str):
    if not isinstance(text, bytes):
        # not a byte, convert directly to str
        try:
            return str(text)
            return repr(text)

    try:
        return text.decode(default_encoding)
    except (LookupError, UnicodeDecodeError):
            try:
                return text.decode(encoding)
            except (LookupError, UnicodeDecodeError):
                pass
        return text.decode(default_encoding, errors="replace")

def validate_email_address(emailaddress):
    """
    Checks if an email address is syntactically correct. Makes use
    of the django email-validator for consistency.

    Args:
        emailaddress (str): Email address to validate.

    Returns:
        bool: If this is a valid email or not.

    """
    try:
        django_validate_email(str(emailaddress))
    except DjangoValidationError:
    else:

def inherits_from(obj, parent):
    """
    Takes an object and tries to determine if it inherits at *any*
    distance from parent.

    Args:
        obj (any): Object to analyze. This may be either an instance or
            a class.
        parent (any): Can be either an instance, a class or the python
            path to the class.

    Returns:
        inherits_from (bool): If `parent` is a parent to `obj` or not.

    Notes:
        What differentiates this function from Python's `isinstance()` is the
        flexibility in the types allowed for the object and parent being compared.

    """

    if callable(obj):
        # this is a class
        obj_paths = ["%s.%s" % (mod.__module__, mod.__name__) for mod in obj.mro()]
    else:
        obj_paths = ["%s.%s" % (mod.__module__, mod.__name__) for mod in obj.__class__.mro()]

    if isinstance(parent, str):
        # a given string path, for direct matching
        parent_path = parent
    elif callable(parent):
        parent_path = "%s.%s" % (parent.__module__, parent.__name__)
    else:
        parent_path = "%s.%s" % (parent.__class__.__module__, parent.__class__.__name__)
    return any(1 for obj_path in obj_paths if obj_path == parent_path)

def server_services():
    """
    Lists all services active on the Server. Observe that since
    services are launched in memory, this function will only return
    any results if called from inside the game.

    Returns:
        services (dict): A dict of available services.

    """

    if hasattr(evennia.SESSION_HANDLER, "server") and hasattr(
        evennia.SESSION_HANDLER.server, "services"
    ):
        server = evennia.SESSION_HANDLER.server.services.namedServices
    else:
        # This function must be called from inside the evennia process.
        server = {}
    del evennia.SESSION_HANDLER
    return server

def uses_database(name="sqlite3"):
    """
    Checks if the game is currently using a given database. This is a
    shortcut to having to use the full backend name.

    Args:
        name (str): One of 'sqlite3', 'mysql', 'postgresql' or 'oracle'.

    Returns:
        uses (bool): If the given database is used or not.

    """
    try:
        engine = settings.DATABASES["default"]["ENGINE"]
        engine = settings.DATABASE_ENGINE
    return engine == "django.db.backends.%s" % name

def delay(timedelay, callback, *args, **kwargs):
    """
    Delay the calling of a callback (function).

    Args:
        timedelay (int or float): The delay in seconds.
        callback (callable): Will be called as `callback(*args, **kwargs)`
            after `timedelay` seconds.
        *args: Will be used as arguments to callback
        persistent (bool, optional): If True the delay remains after a server restart.
            persistent is False by default.
        any (any): Will be used as keyword arguments to callback.

    Returns:
        task (TaskHandlerTask): An instance of a task.
            Refer to, evennia.scripts.taskhandler.TaskHandlerTask

    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 (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 intended for use with TASK_HANDLER's do_task
        and remove methods.
        All persistent tasks whose time delays have passed will be called on server startup.

    """
    global _TASK_HANDLER
    if _TASK_HANDLER is None:
        from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER

    return _TASK_HANDLER.add(timedelay, callback, *args, **kwargs)

def repeat(
    interval, callback, persistent=True, idstring="", stop=False, store_key=None, *args, **kwargs
):
    """
    Start a repeating task using the TickerHandler.

    Args:
        interval (int): How often to call callback.
        callback (callable): This will be called with `*args, **kwargs` every
            `interval` seconds. This must be possible to pickle regardless
            of if `persistent` is set or not!
        persistent (bool, optional): If ticker survives a server reload.
        idstring (str, optional): Separates multiple tickers. This is useful
            mainly if wanting to set up multiple repeats for the same
            interval/callback but with different args/kwargs.
        stop (bool, optional): If set, use the given parameters to _stop_ a running
            ticker instead of creating a new one.
        store_key (tuple, optional): This is only used in combination with `stop` and
            should be the return given from the original `repeat` call. If this
            is given, all other args except `stop` are ignored.
        *args: Used as arguments to `callback`.
        **kwargs: Keyword-arguments to pass to `callback`.

    Returns:
        tuple or None: The tuple is the `store_key` - the identifier for the
        created ticker.  Store this and pass into unrepat() in order to to stop
        this ticker later. Returns `None` if `stop=True`.

    Raises:
        KeyError: If trying to stop a ticker that was not found.

    """
    global _TICKER_HANDLER
    if _TICKER_HANDLER is None:
        from evennia.scripts.tickerhandler import TICKER_HANDLER as _TICKER_HANDLER

    if stop:
        # we pass all args, but only store_key matters if given
        _TICKER_HANDLER.remove(
            interval=interval,
            callback=callback,
            idstring=idstring,
            persistent=persistent,
            store_key=store_key,
        )
    else:
        return _TICKER_HANDLER.add(
            interval=interval, callback=callback, idstring=idstring, persistent=persistent
        )

def unrepeat(store_key):
    """
    This is used to stop a ticker previously started with `repeat`.

    Args:
        store_key (tuple): This is the return from `repeat`, used to uniquely
            identify the ticker to stop. Without the store_key, the ticker
            must be stopped by passing its parameters to `TICKER_HANDLER.remove`
            directly.

    Returns:
        bool: True if a ticker was stopped, False if not (for example because no
            matching ticker was found or it was already stopped).

    """
    try:
        repeat(None, None, stop=True, store_key=store_key)

_PPOOL = None
_PCMD = None
_PROC_ERR = "A process has ended with a probable error condition: process ended by signal 9."

def run_async(to_execute, *args, **kwargs):
    """
    Runs a function or executes a code snippet asynchronously.

    Args:
        to_execute (callable): If this is a callable, it will be
            executed with `*args` and non-reserved `**kwargs` as arguments.
            The callable will be executed using ProcPool, or in a thread
            if ProcPool is not available.
        at_return (callable): Should point to a callable with one
          argument.  It will be called with the return value from
          to_execute.
        at_return_kwargs (dict): This dictionary will be used as
          keyword arguments to the at_return callback.
        at_err (callable): This will be called with a Failure instance
          if there is an error in to_execute.
        at_err_kwargs (dict): This dictionary will be used as keyword
          arguments to the at_err errback.

    Notes:
        All other `*args` and `**kwargs` will be passed on to
        `to_execute`. Run_async will relay executed code to a thread
        or procpool.

        Use this function with restrain and only for features/commands
        that you know has no influence on the cause-and-effect order of your
        game (commands given after the async function might be executed before
        it has finished). Accessing the same property from different threads
        can lead to unpredicted behaviour if you are not careful (this is called a
        "race condition").

        Also note that some databases, notably sqlite3, don't support access from
        multiple threads simultaneously, so if you do heavy database access from
        your `to_execute` under sqlite3 you will probably run very slow or even get
        tracebacks.

    """

    # handle special reserved input kwargs
    callback = kwargs.pop("at_return", None)
    errback = kwargs.pop("at_err", None)
    callback_kwargs = kwargs.pop("at_return_kwargs", {})
    errback_kwargs = kwargs.pop("at_err_kwargs", {})

    if callable(to_execute):
        # no process pool available, fall back to old deferToThread mechanism.
        deferred = threads.deferToThread(to_execute, *args, **kwargs)
    else:
        # no appropriate input for this server setup
        raise RuntimeError("'%s' could not be handled by run_async" % to_execute)

    # attach callbacks
    if callback:
        deferred.addCallback(callback, **callback_kwargs)
    deferred.addErrback(errback, **errback_kwargs)

def check_evennia_dependencies():
    """
    Checks the versions of Evennia's dependencies including making
    some checks for runtime libraries.

    Returns:
        result (bool): `False` if a show-stopping version mismatch is
            found.

    """

    # check main dependencies
    from evennia.server.evennia_launcher import check_main_evennia_dependencies

    not_error = check_main_evennia_dependencies()

    # South is no longer used ...
    if "south" in settings.INSTALLED_APPS:
        errstring += (
            "\n ERROR: 'south' found in settings.INSTALLED_APPS. "
            "\n   South is no longer used. If this was added manually, remove it."
        )
        not_error = False
    # IRC support
    if settings.IRC_ENABLED:
        try:
            import twisted.words

            twisted.words  # set to avoid debug info about not-used import
            errstring += (
                "\n ERROR: IRC is enabled, but twisted.words is not installed. Please install it."
                "\n   Linux Debian/Ubuntu users should install package 'python-twisted-words', "
                "\n others can get it from http://twistedmatrix.com/trac/wiki/TwistedWords."
            )
            not_error = False
    errstring = errstring.strip()
        mlen = max(len(line) for line in errstring.split("\n"))
        logger.log_err("%s\n%s\n%s" % ("-" * mlen, errstring, "-" * mlen))
    return not_error

def has_parent(basepath, obj):
    """
    Checks if `basepath` is somewhere in obj's parent tree.

    Args:
        basepath (str): Python dotpath to compare against obj path.
        obj (any): Object whose path is to be checked.

    Returns:
        has_parent (bool): If the check was successful or not.

    """
    try:
        return any(
            cls
            for cls in obj.__class__.mro()
            if basepath == "%s.%s" % (cls.__module__, cls.__name__)
        )
    except (TypeError, AttributeError):
        # this can occur if we tried to store a class object, not an
        # instance. Not sure if one should defend against this.

def mod_import_from_path(path):
    """
    Load a Python module at the specified path.

    Args:
        path (str): An absolute path to a Python module to load.

    Returns:
        (module or None): An imported module if the path was a valid
        Python module. Returns `None` if the import failed.

    """
    if not os.path.isabs(path):
        path = os.path.abspath(path)
    dirpath, filename = path.rsplit(os.path.sep, 1)
    modname = filename.rstrip(".py")

    try:
        return importlib.machinery.SourceFileLoader(modname, path).load_module()
    except OSError:
        logger.log_trace(f"Could not find module '{modname}' ({modname}.py) at path '{dirpath}'")

def mod_import(module):
    """
    A generic Python module loader.

    Args:
        module (str, module): This can be either a Python path
            (dot-notation like `evennia.objects.models`), an absolute path
            (e.g. `/home/eve/evennia/evennia/objects/models.py`) or an
            already imported module object (e.g. `models`)
    Returns:
        (module or None): An imported module. If the input argument was
        already a module, this is returned as-is, otherwise the path is
        parsed and imported. Returns `None` and logs error if import failed.

    """
    if not module:

    if isinstance(module, types.ModuleType):
        # if this is already a module, we are done
        return module

    if module.endswith(".py") and os.path.exists(module):
        return mod_import_from_path(module)

    try:
        return importlib.import_module(module)
    except ImportError:

def all_from_module(module):
    """
    Return all global-level variables defined in a module.

    Args:
            (e.g. `/home/eve/evennia/evennia/objects.models.py`) or an

    Returns:
        dict: A dict of {variablename: variable} for all
            variables in the given module.

    Notes:
        Ignores modules and variable names starting with an underscore, as well
        as variables imported into the module from other modules.

    """
    mod = mod_import(module)
    if not mod:
        return {}
    # make sure to only return variables actually defined in this
    # module if available (try to avoid imports)
    members = getmembers(mod, predicate=lambda obj: getmodule(obj) in (mod, None))
    return dict((key, val) for key, val in members if not key.startswith("_"))

def callables_from_module(module):
    """
    Return all global-level callables defined in a module.

    Args:
        module (str, module): A python-path to a module or an actual
            module object.

    Returns:
        callables (dict): A dict of {name: callable, ...} from the module.

    Notes:
        Will ignore callables whose names start with underscore "_".

    """
    if not mod:
        return {}
    # make sure to only return callables actually defined in this module (not imports)
    members = getmembers(mod, predicate=lambda obj: callable(obj) and getmodule(obj) == mod)

def variable_from_module(module, variable=None, default=None):
    """
    Retrieve a variable or list of variables from a module. The
    variable(s) must be defined globally in the module. If no variable
    is given (or a list entry is `None`), all global variables are
    extracted from the module.

    Args:
      module (string or module): Python path, absolute path or a module.
      variable (string or iterable, optional): Single variable name or iterable
          of variable names to extract. If not given, all variables in
          the module will be returned.
      default (string, optional): Default value to use if a variable fails to
          be extracted. Ignored if `variable` is not given.

    Returns:
        variables (value or list): A single value or a list of values
        depending on if `variable` is given or not. Errors in lists
        are replaced by the `default` argument.

    """

        return default

    if not mod:

    if variable:
        result = []
        for var in make_iter(variable):
            if var:
                # try to pick a named variable
                result.append(mod.__dict__.get(var, default))
    else:
        # get all
        result = [
            val for key, val in mod.__dict__.items() if not (key.startswith("_") or ismodule(val))
        ]

    if len(result) == 1:
        return result[0]
    return result

def string_from_module(module, variable=None, default=None):
    """
    This is a wrapper for `variable_from_module` that requires return
    value to be a string to pass. It's primarily used by login screen.

    Args:

    Returns:
        variables (value or list): A single (string) value or a list of values
        depending on if `variable` is given or not. Errors in lists (such
        as the value not being a string) are replaced by the `default` argument.

    """
    val = variable_from_module(module, variable=variable, default=default)
    if val:
        if variable:
            return val
        else:
            result = [v for v in make_iter(val) if isinstance(v, str)]
            return result if result else default
    return default

def random_string_from_module(module):
    """
    Returns a random global string from a module.

    Args:
        module (string or module): Python path, absolute path or a module.

    Returns:
        random (string): A random stribg variable from `module`.
    """
    return random.choice(string_from_module(module))

def fuzzy_import_from_module(path, variable, default=None, defaultpaths=None):
    """
    Import a variable based on a fuzzy path. First the literal
    `path` will be tried, then all given `defaultpaths` will be
    prepended to see a match is found.

    Args:
        path (str): Full or partial python path.
        variable (str): Name of variable to import from module.
        default (string, optional): Default value to use if a variable fails to
        defaultpaths (iterable, options): Python paths to attempt in order if
            importing directly from `path` doesn't work.

    Returns:
        value (any): The variable imported from the module, or `default`, if
            not found.

    """
    paths = [path] + make_iter(defaultpaths)
    for modpath in paths:
        try:
            mod = importlib.import_module(modpath)
        except ImportError as ex:
            if not str(ex).startswith("No module named %s" % modpath):
                # this means the module was found but it
                # triggers an ImportError on import.
                raise ex
            return getattr(mod, variable, default)

def class_from_module(path, defaultpaths=None, fallback=None):
    """
    Return a class from a module, given the class' full python path. This is
    primarily used to convert db_typeclass_path:s to classes.

    Args:
        path (str): Full Python dot-path to module.
        defaultpaths (iterable, optional): If a direct import from `path` fails,
            try subsequent imports by prepending those paths to `path`.
        fallback (str): If all other attempts fail, use this path as a fallback.
            This is intended as a last-resort. In the example of Evennia
            loading, this would be a path to a default parent class in the
            evennia repo itself.

    Returns:
        class (Class): An uninstantiated class recovered from path.

    Raises:
        ImportError: If all loading failed.

    """
    cls = None
    err = ""
    if defaultpaths:
        paths = (
            [path] + ["%s.%s" % (dpath, path) for dpath in make_iter(defaultpaths)]
            if defaultpaths
            else []
        )
    else:
        paths = [path]

    for testpath in paths:
            testpath, clsname = testpath.rsplit(".", 1)
        else:
            raise ImportError("the path '%s' is not on the form modulepath.Classname." % path)

        try:
            if not importlib.util.find_spec(testpath, package="evennia"):
                continue
        except ModuleNotFoundError:
            continue

        try:
            mod = importlib.import_module(testpath, package="evennia")
            err = traceback.format_exc(30)
            break

        try:
            cls = getattr(mod, clsname)
            break
            if len(trace()) > 2:
                # AttributeError within the module, don't hide it
                err = traceback.format_exc(30)
                break
    if not cls:
        err = "\nCould not load typeclass '{}'{}".format(
            path, " with the following traceback:\n" + err if err else ""
        )
        if defaultpaths:
            err += "\nPaths searched:\n    %s" % "\n    ".join(paths)
        else:
            err += "."
        logger.log_err(err)
        if fallback:
            logger.log_warn(f"Falling back to {fallback}.")
            return class_from_module(fallback)
        else:
            # even fallback fails
            raise ImportError(err)
    return cls

# alias
object_from_module = class_from_module

def init_new_account(account):
    """
    Deprecated.
    """
    from evennia.utils import logger

    logger.log_dep("evennia.utils.utils.init_new_account is DEPRECATED and should not be used.")

def string_similarity(string1, string2):
    """
    This implements a "cosine-similarity" algorithm as described for example in
    *Proceedings of the 22nd International Conference on Computation
    Linguistics* (Coling 2008), pages 593-600, Manchester, August 2008.
    The measure-vectors used is simply a "bag of words" type histogram
    (but for letters).

    Args:
        string1 (str): String to compare (may contain any number of words).
        string2 (str): Second string to compare (any number of words).

    Returns:
        similarity (float): A value 0...1 rating how similar the two
            strings are.

    """
    vocabulary = set(list(string1 + string2))
    vec1 = [string1.count(v) for v in vocabulary]
    vec2 = [string2.count(v) for v in vocabulary]
    try:
        return float(sum(vec1[i] * vec2[i] for i in range(len(vocabulary)))) / (
            math.sqrt(sum(v1**2 for v1 in vec1)) * math.sqrt(sum(v2**2 for v2 in vec2))
        )
    except ZeroDivisionError:
        # can happen if empty-string cmdnames appear for some reason.
        # This is a no-match.
        return 0

def string_suggestions(string, vocabulary, cutoff=0.6, maxnum=3):
    """
    Given a `string` and a `vocabulary`, return a match or a list of
    suggestions based on string similarity.

    Args:
        string (str): A string to search for.
        vocabulary (iterable): A list of available strings.
        cutoff (int, 0-1): Limit the similarity matches (the higher
            the value, the more exact a match is required).
        maxnum (int): Maximum number of suggestions to return.

    Returns:
        suggestions (list): Suggestions from `vocabulary` with a
        similarity-rating that higher than or equal to `cutoff`.
        Could be empty if there are no matches.

    """
    return [
        tup[1]
        for tup in sorted(
            [(string_similarity(string, sugg), sugg) for sugg in vocabulary],
            key=lambda tup: tup[0],
            reverse=True,
        )
        if tup[0] >= cutoff
    ][:maxnum]

def string_partial_matching(alternatives, inp, ret_index=True):
    """
    Partially matches a string based on a list of `alternatives`.
    Matching is made from the start of each subword in each
    alternative. Case is not important. So e.g. "bi sh sw" or just
    "big" or "shiny" or "sw" will match "Big shiny sword". Scoring is
    done to allow to separate by most common denominator. You will get
    multiple matches returned if appropriate.

    Args:
        alternatives (list of str): A list of possible strings to
            match.
        inp (str): Search criterion.
        ret_index (bool, optional): Return list of indices (from alternatives
            array) instead of strings.
    Returns:
        matches (list): String-matches or indices if `ret_index` is `True`.

    """
    if not alternatives or not inp:
        return []

    matches = defaultdict(list)
    inp_words = inp.lower().split()
    for altindex, alt in enumerate(alternatives):
        alt_words = alt.lower().split()
        last_index = 0
        score = 0
        for inp_word in inp_words:
            # loop over parts, making sure only to visit each part once
            # (this will invalidate input in the wrong word order)
            submatch = [
                last_index + alt_num
                for alt_num, alt_word in enumerate(alt_words[last_index:])
                if alt_word.startswith(inp_word)
            ]
            if submatch:
                last_index = min(submatch) + 1
                score += 1
            else:
                score = 0
                break
        if score:
            if ret_index:
                matches[score].append(altindex)
            else:
                matches[score].append(alt)
    if matches:
        return matches[max(matches)]
    return []

def format_table(table, extra_space=1):
    """
    Format a 2D array of strings into a multi-column table.

    Args:
        table (list): A list of lists to represent columns in the
            table: `[[val,val,val,...], [val,val,val,...], ...]`, where
            each val will be placed on a separate row in the
            column. All columns must have the same number of rows (some
            positions may be empty though).
        extra_space (int, optional): Sets how much *minimum* extra
            padding (in characters) should  be left between columns.

    Returns:
        list: A list of lists representing the rows to print out one by one.

    Notes:
        The function formats the columns to be as wide as the widest member
        of each column.

        `evennia.utils.evtable` is more powerful than this, but this
        function can be useful when the number of columns and rows are
        unknown and must be calculated on the fly.

    Examples: ::

        ftable = format_table([[1,2,3], [4,5,6]])
        string = ""
        for ir, row in enumerate(ftable):
            if ir == 0:
                # make first row white
                string += "\\n|w" + "".join(row) + "|n"
            else:
                string += "\\n" + "".join(row)
        print(string)

    """

    if not table:
        return [[]]

    max_widths = [max([len(str(val)) for val in col]) for col in table]
    ftable = []
    for irow in range(len(table[0])):
        ftable.append(
            [
                str(col[irow]).ljust(max_widths[icol]) + " " * extra_space
                for icol, col in enumerate(table)
            ]
        )
    return ftable

def percent(value, minval, maxval, formatting="{:3.1f}%"):
    """
    Get a value in an interval as a percentage of its position
    in that interval. This also understands negative numbers.

    Args:
        value (number): This should be a value minval<=value<=maxval.
        minval (number or None): Smallest value in interval. This could be None
            for an open interval (then return will always be 100%)
        maxval (number or None): Biggest value in interval. This could be None
        formatted (str, optional): This is a string that should
            accept one formatting tag. This will receive the
            current value as a percentage. If None, the
            raw float will be returned instead.
    Returns:
        str or float: The formatted value or the raw percentage as a float.
    Notes:
        We try to handle a weird interval gracefully.

        - If either maxval or minval is None (open interval), we (aribtrarily) assume 100%.
        - If minval > maxval, we return 0%.
        - If minval == maxval == value we are looking at a single value match and return 100%.
        - If minval == maxval != value we return 0%.
        - If value not in [minval..maxval], we set value to the  closest
          boundary, so the result will be 0% or 100%, respectively.

    """
    result = None
    if None in (minval, maxval):
        # we have no boundaries, percent calculation makes no sense,
        # we set this to 100% since it
        result = 100.0
    elif minval > maxval:
        # interval has no width so we cannot
        # occupy any position within it.
        result = 0.0
    elif minval == maxval == value:
        # this is a single value that we match
    elif minval == maxval != value:
        # interval has no width so we cannot be in it.

    if result is None:
        # constrain value to interval
        value = min(max(minval, value), maxval)

        # these should both be >0
        dpart = value - minval
        dfull = maxval - minval
        result = (dpart / dfull) * 100.0

    if isinstance(formatting, str):
        return formatting.format(result)

import functools  # noqa

def percentile(iterable, percent, key=lambda x: x):
    """
    Find the percentile of a list of values.

    Args:
        iterable (iterable): A list of values. Note N MUST BE already sorted.
        percent (float): A value from 0.0 to 1.0.
        key (callable, optional). Function to compute value from each element of N.

    Returns:
        float: The percentile of the values

    """
    k = (len(iterable) - 1) * percent
    f = math.floor(k)
    c = math.ceil(k)
    if f == c:
        return key(iterable[int(k)])
    d0 = key(iterable[int(f)]) * (c - k)
    d1 = key(iterable[int(c)]) * (k - f)
    return d0 + d1

def format_grid(elements, width=78, sep="  ", verbatim_elements=None, line_prefix=""):
    """
    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!

    Args:
        elements (iterable): A 1D list of string elements to put in the grid.
        width (int, optional): The width of the grid area to fill.
        sep (str, optional): The extra separator to put between words. If
            set to the empty string, words may run into each other.
        verbatim_elements (list, optional): This is a list of indices pointing to
            specific items in the `elements` list. An element at this index will
            not be included in the calculation of the slot sizes. It will still
            be inserted into the grid at the correct position and may be surrounded
            by padding unless filling the entire line. This is useful for embedding
            decorations in the grid, such as horizontal bars.
        ignore_ansi (bool, optional): Ignore ansi markups when calculating white spacing.
        line_prefix (str, optional): A prefix to add at the beginning of each line.
            This can e.g. be used to preserve line color across line breaks.

    Returns:
        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 = [""]
        for element in elements:
            rowlen = display_len((rows[-1]))
            elen = display_len((element))
            if rowlen + elen <= width:
                rows[-1] += element
            else:
                rows.append(element)
        return rows

    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 = [display_len((elem)) for elem in elements]
        wls_percentile = [wl for iw, wl in enumerate(wls) if iw not in verbatim_elements]

        if wls_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

        if aver_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 - display_len((element.rstrip())))
                for iel, element in enumerate(elements)
            ]

        indices = [averlen * ind for ind in range(aver_per_row - 1)]

        rows = []
        ic = 0
        row = ""
        for ie, element in enumerate(elements):
            wl = wls[ie]
            lrow = display_len((row))
            # debug = row.replace(" ", ".")

            if lrow + wl > width:
                # this slot extends outside grid, move to next line
                row += " " * (width - lrow)
                rows.append(row)
                if wl >= width:
                    # remove sep if this fills the entire line
                    element = element.rstrip()
                row = crop(element, width)
                ic = 0
            elif ic >= aver_per_row - 1:
                # no more slots available on this line
                row += " " * max(0, (width - lrow))
                ic = 0
            else:
                try:
                    while lrow > max(0, indices[ic]):
                        # slot too wide, extend into adjacent slot
                        ic += 1
                    row += " " * max(0, indices[ic] - lrow)
                    # we extended past edge of grid, crop or move to next line
                    if ic == 0:
                        row = crop(element, width)
                    else:
                        row += " " * max(0, width - lrow)
                    rows.append(row)
                    row = ""
                    ic = 0
                else:
                    # add a new slot
                    row += element + " " * max(0, averlen - wl)
                    ic += 1

            if ie >= nelements - 1:
                # last element, make sure to store
                row += " " * max(0, width - display_len((row)))

    if not elements:
        return []
    if not verbatim_elements:
        verbatim_elements = []

    nelements = len(elements)
    # add sep to all but the very last element
    elements = [elements[ie] + sep for ie in range(nelements - 1)] + [elements[-1]]

    if sum(display_len((element)) for element in elements) <= width:
        # grid fits in one line
        rows = _minimal_rows(elements)
    else:
        # full multi-line grid
        rows = _weighted_rows(elements)

    if line_prefix:
        return [line_prefix + row for row in rows]
    return rows

def get_evennia_pids():
    """
    Get the currently valid PIDs (Process IDs) of the Portal and
    Server by trying to access a PID file.

    Returns:
        server, portal (tuple): The PIDs of the respective processes,
            or two `None` values if not found.

    Examples:
        This can be used to determine if we are in a subprocess by

        ```python
        self_pid = os.getpid()
        server_pid, portal_pid = get_evennia_pids()
        is_subprocess = self_pid not in (server_pid, portal_pid)
        ```

    """
    server_pidfile = os.path.join(settings.GAME_DIR, "server.pid")
    portal_pidfile = os.path.join(settings.GAME_DIR, "portal.pid")
    server_pid, portal_pid = None, None
    if os.path.exists(server_pidfile):
        with open(server_pidfile, "r") as f:
            server_pid = f.read()
    if os.path.exists(portal_pidfile):
        with open(portal_pidfile, "r") as f:
            portal_pid = f.read()
    if server_pid and portal_pid:
        return int(server_pid), int(portal_pid)
    return None, None

def deepsize(obj, max_depth=4):
    """
    Get not only size of the given object, but also the size of
    objects referenced by the object, down to `max_depth` distance
    from the object.

    Args:
        obj (object): the object to be measured.
        max_depth (int, optional): maximum referential distance
            from `obj` that `deepsize()` should cover for
            measuring objects referenced by `obj`.

    Returns:
        size (int): deepsize of `obj` in Bytes.

    Notes:
        This measure is necessarily approximate since some
        memory is shared between objects. The `max_depth` of 4 is roughly
        tested to give reasonable size information about database models
        and their handlers.

    """

    def _recurse(o, dct, depth):
        if 0 <= max_depth < depth:
            return
        for ref in gc.get_referents(o):
            idr = id(ref)
            if idr not in dct:
                dct[idr] = (ref, sys.getsizeof(ref, default=0))
                _recurse(ref, dct, depth + 1)

    sizedict = {}
    _recurse(obj, sizedict, 0)
    size = sys.getsizeof(obj) + sum([p[1] for p in sizedict.values()])
    return size

# lazy load handler
_missing = object()

class lazy_property:
    """
    Delays loading of property until first access. Credit goes to the
    Implementation in the werkzeug suite:
    http://werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property

    This should be used as a decorator in a class and in Evennia is
    mainly used to lazy-load handlers:

        ```python
        @lazy_property
        def attributes(self):
            return AttributeHandler(self)
        ```

    Once initialized, the `AttributeHandler` will be available as a
    property "attributes" on the object. This is read-only since
    this functionality is pretty much exclusively used by handlers.

    """

    def __init__(self, func, name=None, doc=None):
        """Store all properties for now"""
        self.__name__ = name or func.__name__
        self.__module__ = func.__module__
        self.__doc__ = doc or func.__doc__

    def __get__(self, obj, type=None):
        """Triggers initialization"""
        if obj is None:
        value = obj.__dict__.get(self.__name__, _missing)
        if value is _missing:
            value = self.func(obj)
        obj.__dict__[self.__name__] = value
        return value

    def __set__(self, obj, value):
        """Protect against setting"""
        handlername = self.__name__
        raise AttributeError(
            _(
                "{obj}.{handlername} is a handler and can't be set directly. "
                "To add values, use `{obj}.{handlername}.add()` instead."
            ).format(obj=obj, handlername=handlername)
        )

    def __delete__(self, obj):
        """Protect against deleting"""
            _(
                "{obj}.{handlername} is a handler and can't be deleted directly. "
                "To remove values, use `{obj}.{handlername}.remove()` instead."
        )

_STRIP_ANSI = None
_RE_CONTROL_CHAR = re.compile(
    "[%s]" % re.escape("".join([chr(c) for c in range(0, 32)]))
)  # + range(127,160)])))

def strip_control_sequences(string):
    """
    Remove non-print text sequences.

    Args:
        string (str): Text to strip.

    Returns.
        text (str): Stripped text.

    """
    global _STRIP_ANSI
    if not _STRIP_ANSI:
        from evennia.utils.ansi import strip_raw_ansi as _STRIP_ANSI
    return _RE_CONTROL_CHAR.sub("", _STRIP_ANSI(string))

def calledby(callerdepth=1):
    """
    Only to be used for debug purposes. Insert this debug function in
    another function; it will print which function called it.

    Args:
        callerdepth (int or None): If None, show entire stack. If int, must be larger than 0.
            When > 1, it will print the sequence to that depth.

    Returns:
        calledby (str): A debug string detailing the code that called us.

    """
    import inspect

    def _stack_display(frame):
        path = os.path.sep.join(frame[1].rsplit(os.path.sep, 2)[-2:])
        return (
            f"> called by '{frame[3]}': {path}:{frame[2]} >>>"
            f" {frame[4][0].strip() if frame[4] else ''}"
        )

    stack = inspect.stack()

    out = []
    if callerdepth is None:
        callerdepth = len(stack) - 1

    # show range
    for idepth in range(1, max(1, callerdepth + 1)):
        # we must step one extra level back in stack since we don't want
        # to include the call of this function itself.
        out.append(_stack_display(stack[min(idepth + 1, len(stack) - 1)]))
    return "\n".join(out[::-1])

def m_len(target):
    """
    Provides length checking for strings with MXP patterns, and falls
    back to normal len for other objects.

    Args:
        target (str): A string with potential MXP components
            to search.

    Returns:
        length (int): The length of `target`, ignoring MXP components.

    """
    # Would create circular import if in module root.
    from evennia.utils.ansi import ANSI_PARSER

    if inherits_from(target, str) and "|lt" in target:
        return len(ANSI_PARSER.strip_mxp(target))
    return len(target)

def display_len(target):
    """
    Calculate the 'visible width' of text. This is not necessarily the same as the
    number of characters in the case of certain asian characters. This will also
    strip MXP patterns.

    Args:
        target (any): Something to measure the length of. If a string, it will be
            measured keeping asian-character and MXP links in mind.

    Return:
        int: The visible width of the target.

    """

    if inherits_from(target, str):
        # str or ANSIString
        target = ANSI_PARSER.strip_mxp(target)
        target = ANSI_PARSER.parse_ansi(target, strip_ansi=True)
        extra_wide = ("F", "W")
        return sum(2 if east_asian_width(char) in extra_wide else 1 for char in target)
    else:
        return len(target)

# -------------------------------------------------------------------
# Search handler function
#
# Replace this hook function by changing settings.SEARCH_AT_RESULT.

def at_search_result(matches, caller, query="", quiet=False, **kwargs):
    """
    This is a generic hook for handling all processing of a search
    result, including error reporting. This is also called by the cmdhandler
    to manage errors in command lookup.

    Args:
        matches (list): This is a list of 0, 1 or more typeclass
            instances or Command instances, the matched result of the
            search. If 0, a nomatch error should be echoed, and if >1,
            multimatch errors should be given. Only if a single match
            should the result pass through.
        caller (Object): The object performing the search and/or which should
        receive error messages.
        query (str, optional): The search query used to produce `matches`.
        quiet (bool, optional): If `True`, no messages will be echoed to caller
            on errors.
        nofound_string (str): Replacement string to echo on a notfound error.
        multimatch_string (str): Replacement string to echo on a multimatch error.

    Returns:
        processed_result (Object or None): This is always a single result
        or `None`. If `None`, any error reporting/handling should
        already have happened. The returned object is of the type we are
        checking multimatches for (e.g. Objects or Commands)

    """

    error = ""
    if not matches:
        # no results.
        error = kwargs.get("nofound_string") or _("Could not find '{query}'.").format(query=query)
        matches = None
    elif len(matches) > 1:
        multimatch_string = kwargs.get("multimatch_string")
        if multimatch_string:
            error = "%s\n" % multimatch_string
        else:
            error = _("More than one match for '{query}' (please narrow target):\n").format(
                query=query
            )

        for num, result in enumerate(matches):
            # we need to consider that result could be a Command, where .aliases
            # is a list of strings
            if hasattr(result.aliases, "all"):
                # result is a typeclassed entity where `.aliases` is an AliasHandler.
                aliases = result.aliases.all(return_objs=True)
                # remove pluralization aliases
                aliases = [
                    alias
                    for alias in aliases
                    if hasattr(alias, "category") and alias.category not in ("plural_key",)
                ]
            else:
                # result is likely a Command, where `.aliases` is a list of strings.
                aliases = result.aliases

            error += _MULTIMATCH_TEMPLATE.format(
                number=num + 1,
                name=result.get_display_name(caller)
                if hasattr(result, "get_display_name")
                else query,
                aliases=" [{alias}]".format(alias=";".join(aliases) if aliases else ""),
                info=result.get_extra_info(caller),
            )
    else:
        # exactly one match
        matches = matches[0]

    if error and not quiet:
        caller.msg(error.strip())

class LimitedSizeOrderedDict(OrderedDict):
    """
    This dictionary subclass is both ordered and limited to a maximum
    number of elements. Its main use is to hold a cache that can never
    grow out of bounds.

    """

    def __init__(self, *args, **kwargs):
        """
        Limited-size ordered dict.

            size_limit (int): Use this to limit the number of elements
              alloweds to be in this list. By default the overshooting elements
              will be removed in FIFO order.
            fifo (bool, optional): Defaults to `True`. Remove overshooting elements
              in FIFO order. If `False`, remove in FILO order.

        """
        super().__init__()
        self.size_limit = kwargs.get("size_limit", None)
        self.filo = not kwargs.get("fifo", True)  # FIFO inverse of FILO
        self._check_size()

        ret = super().__eq__(other)
        if ret:
            return (
                ret
                and hasattr(other, "size_limit")
                and self.size_limit == other.size_limit
                and hasattr(other, "fifo")
                and self.fifo == other.fifo
            )

    def __ne__(self, other):
        return not self.__eq__(other)

    def _check_size(self):
        filo = self.filo
        if self.size_limit is not None:
            while self.size_limit < len(self):
                self.popitem(last=filo)

    def __setitem__(self, key, value):
        super().__setitem__(key, value)

    def update(self, *args, **kwargs):
        super().update(*args, **kwargs)

def get_game_dir_path():
    """
    This is called by settings_default in order to determine the path
    of the game directory.

    Returns:
        path (str): Full OS path to the game dir

    """
    # current working directory, assumed to be somewhere inside gamedir.
    for inum in range(10):
        gpath = os.getcwd()
        if "server" in os.listdir(gpath):
            if os.path.isfile(os.path.join("server", "conf", "settings.py")):
                return gpath
        else:
            os.chdir(os.pardir)
    raise RuntimeError("server/conf/settings.py not found: Must start from inside game dir.")

def get_all_typeclasses(parent=None):
    """
    List available typeclasses from all available modules.

    Args:
        parent (str, optional): If given, only return typeclasses inheriting
            (at any distance) from this parent.

    Returns:
        dict: On the form `{"typeclass.path": typeclass, ...}`

    Notes:
        This will dynamically retrieve all abstract django models inheriting at
        any distance from the TypedObject base (aka a Typeclass) so it will
        work fine with any custom classes being added.

    """
    from evennia.typeclasses.models import TypedObject

    typeclasses = {
        "{}.{}".format(model.__module__, model.__name__): model
        for model in apps.get_models()
        if TypedObject in getmro(model)
    }
    if parent:
        typeclasses = {
            name: typeclass
            for name, typeclass in typeclasses.items()
            if inherits_from(typeclass, parent)
        }
    return typeclasses

def get_all_cmdsets(parent=None):
    """
    List available cmdsets from all available modules.

    Args:
        parent (str, optional): If given, only return cmdsets inheriting (at
            any distance) from this parent.

    Returns:
        dict: On the form {"cmdset.path": cmdset, ...}

    Notes:
        any distance from the CmdSet base so it will work fine with any custom
        classes being added.

    """
    from evennia.commands.cmdset import CmdSet

    base_cmdset = class_from_module(parent) if parent else CmdSet

    cmdsets = {
        "{}.{}".format(subclass.__module__, subclass.__name__): subclass
        for subclass in base_cmdset.__subclasses__()
    }
    return cmdsets

def interactive(func):
    """
    Decorator to make a method pausable with `yield(seconds)`
    and able to ask for user-input with `response=yield(question)`.
    For the question-asking to work, one of the args or kwargs to the
    decorated function must be named 'caller'.

    Raises:
        ValueError: If asking an interactive question but the decorated
            function has no arg or kwarg named 'caller'.
        ValueError: If passing non int/float to yield using for pausing.

    Examples:

        ```python
        @interactive
        def myfunc(caller):
            caller.msg("This is a test")
            # wait five seconds
            yield(5)
            # ask user (caller) a question
            response = yield("Do you want to continue waiting?")
            if response == "yes":
                yield(5)
            else:
                # ...
        ```

    Notes:
       This turns the decorated function or method into a generator.

    """
    from evennia.utils.evmenu import get_input

    def _process_input(caller, prompt, result, generator):
        deferLater(reactor, 0, _iterate, generator, caller, response=result)

    def _iterate(generator, caller=None, response=None):
        try:
            if response is None:
                value = next(generator)
            else:
                value = generator.send(response)
            pass
        else:
            if isinstance(value, (int, float)):
                delay(value, _iterate, generator, caller=caller)
            elif isinstance(value, str):
                if not caller:
                        "To use `result yield('prompt')` in an @interactive method, that "
                        "method must have an argument named `caller`.)"
                    )
                get_input(caller, value, _process_input, generator=generator)
            else:
                    "yield(val) in an @interactive method must have an int/float as arg."
                )

    def decorator(*args, **kwargs):
        argnames = inspect.getfullargspec(func).args
        caller = None
        if "caller" in argnames:
            # we assume this is an object
            caller = args[argnames.index("caller")]

        ret = func(*args, **kwargs)
        if isinstance(ret, types.GeneratorType):
            _iterate(ret, caller)
        else:
            return ret

    return decorator

def safe_convert_to_types(converters, *args, raise_errors=True, **kwargs):
    """
    Helper function to safely convert inputs to expected data types.

    Args:
        converters (tuple): A tuple `((converter, converter,...), {kwarg: converter, ...})` to
            match a converter to each element in `*args` and `**kwargs`.
            Each converter will will be called with the arg/kwarg-value as the only argument.
            If there are too few converters given, the others will simply not be converter. If the
            converter is given as the string 'py', it attempts to run
            `safe_eval`/`literal_eval` on  the input arg or kwarg value. It's possible to
            skip the arg/kwarg part of the tuple, an empty tuple/dict will then be assumed.
        *args: The arguments to convert with `argtypes`.
        raise_errors (bool, optional): If set, raise any errors. This will
            abort the conversion at that arg/kwarg. Otherwise, just skip the
            conversion of the failing arg/kwarg. This will be set by the FuncParser if
            this is used as a part of a FuncParser callable.
        **kwargs: The kwargs to convert with `kwargtypes`

    Returns:
        tuple: `(args, kwargs)` in converted form.

    Raises:
        utils.funcparser.ParsingError: If parsing failed in the `'py'`
            converter. This also makes this compatible with the FuncParser
            interface.
        any: Any other exception raised from other converters, if raise_errors is True.

    Notes:
        This function is often used to validate/convert input from untrusted sources. For
        security, the "py"-converter is deliberately limited and uses `safe_eval`/`literal_eval`
        which  only supports simple expressions or simple containers with literals. NEVER
        use the python `eval` or `exec` methods as a converter for any untrusted input! Allowing
        untrusted sources to execute arbitrary python on your server is a severe security risk,

    Example:
    ::

        $funcname(1, 2, 3.0, c=[1,2,3])

        def _funcname(*args, **kwargs):
            args, kwargs = safe_convert_input(((int, int, float), {'c': 'py'}), *args, **kwargs)
            # ...

    """
    container_end_char = {"(": ")", "[": "]", "{": "}"}  # tuples, lists, sets

    def _manual_parse_containers(inp):
        startchar = inp[0]
        endchar = inp[-1]
        if endchar != container_end_char.get(startchar):
            return
        return [str(part).strip() for part in inp[1:-1].split(",")]

    def _safe_eval(inp):
        if not inp:
            return ""
        if not isinstance(inp, str):
            # already converted
            return inp
        try:
            try:
                return literal_eval(inp)
                parts = _manual_parse_containers(inp)
                if not parts:
                    raise
                return parts

            literal_err = f"{err.__class__.__name__}: {err}"
            try:
                return simple_eval(inp)
                simple_err = f"{str(err.__class__.__name__)}: {err}"

            from evennia.utils.funcparser import ParsingError

            err = (
                f"Errors converting '{inp}' to python:\n"
                f"literal_eval raised {literal_err}\n"
                f"simple_eval raised {simple_err}"
            )
            raise ParsingError(err)
        else:
            # fallback - convert to str
            return str(inp)

    # handle an incomplete/mixed set of input converters
    if not converters:
        return args, kwargs
    arg_converters, *kwarg_converters = converters
    arg_converters = make_iter(arg_converters)
    kwarg_converters = kwarg_converters[0] if kwarg_converters else {}

    # apply the converters
    if args and arg_converters:
        args = list(args)
        arg_converters = make_iter(arg_converters)
        for iarg, arg in enumerate(args[: len(arg_converters)]):
            converter = arg_converters[iarg]
            converter = _safe_eval if converter in ("py", "python") else converter
            try:
                args[iarg] = converter(arg)
                if raise_errors:
                    raise
        args = tuple(args)
    if kwarg_converters and isinstance(kwarg_converters, dict):
        for key, converter in kwarg_converters.items():
            if key in {**kwargs}:
                try:
                    kwargs[key] = converter(kwargs[key])
                    if raise_errors:
                        raise
    return args, kwargs

def strip_unsafe_input(txt, session=None, bypass_perms=None):
    """
    Remove 'unsafe' text codes from text; these are used to elimitate
    exploits in user-provided data, such as html-tags, line breaks etc.

    Args:
        txt (str): The text to clean.
        session (Session, optional): A Session in order to determine if
            the check should be bypassed by permission (will be checked
            with the 'perm' lock, taking permission hierarchies into account).
        bypass_perms (list, optional): Iterable of permission strings
            to check for bypassing the strip. If not given, use
            `settings.INPUT_CLEANUP_BYPASS_PERMISSIONS`.

    Returns:
        str: The cleaned string.

    Notes:
        The `INPUT_CLEANUP_BYPASS_PERMISSIONS` list defines what account
        permissions are required to bypass this strip.

    """
    global _STRIP_UNSAFE_TOKENS
    if not _STRIP_UNSAFE_TOKENS:
        from evennia.utils.ansi import strip_unsafe_tokens as _STRIP_UNSAFE_TOKENS

    if session:
        obj = session.puppet if session.puppet else session.account
        bypass_perms = bypass_perms or settings.INPUT_CLEANUP_BYPASS_PERMISSIONS
        if obj.permissions.check(*bypass_perms):
            return txt

    # remove html codes
    txt = strip_tags(txt)
    txt = _STRIP_UNSAFE_TOKENS(txt)
    return txt

def copy_word_case(base_word, new_word):
    """
    Converts a word to use the same capitalization as a first word.

    Args:
        base_word (str): A word to get the capitalization from.
        new_word (str): A new word to capitalize in the same way as `base_word`.

    Returns:
        str: The `new_word` with capitalization matching the first word.

    Notes:
        This is meant for words. Longer sentences may get unexpected results.

        If the two words have a mix of capital/lower letters _and_ `new_word`
        is longer than `base_word`, the excess will retain its original case.

    """

    # Word
    if base_word.istitle():
        return new_word.title()
    # word
    elif base_word.islower():
        return new_word.lower()
    # WORD
    elif base_word.isupper():
        return new_word.upper()
    else:
        # WorD - a mix. Handle each character
        maxlen = len(base_word)
        shared, excess = new_word[:maxlen], new_word[maxlen - 1 :]
        return (
            "".join(
                char.upper() if base_word[ic].isupper() else char.lower()
                for ic, char in enumerate(new_word)
            )
            + excess
        )

def run_in_main_thread(function_or_method, *args, **kwargs):
    """
    Force a callable to execute in the main Evennia thread. This is only relevant when
    calling code from e.g. web views, which run in a separate threadpool. Use this
    to avoid race conditions.

    Args:
        function_or_method (callable): A function or method to fire.
        *args: Will be passed into the callable.
        **kwargs: Will be passed into the callable.

    """
    if _IS_MAIN_THREAD:
        return function_or_method(*args, **kwargs)
    else:
        return threads.blockingCallFromThread(reactor, function_or_method, *args, **kwargs)

_INT2STR_MAP_NOUN = {
    0: "no",
    1: "one",
    2: "two",
    3: "three",
    4: "four",
    5: "five",
    6: "six",
    7: "seven",
    8: "eight",
    9: "nine",
    10: "ten",
    11: "eleven",
    12: "twelve",
}
_INT2STR_MAP_ADJ = {1: "1st", 2: "2nd", 3: "3rd"}  # rest is Xth.

def int2str(number, adjective=False):
    """
    Convert a number to an English string for better display; so 1 -> one, 2 -> two etc
    up until 12, after which it will be '13', '14' etc.

    Args:
        number (int): The number to convert. Floats will be converted to ints.
        adjective (int): If set, map 1->1st, 2->2nd etc. If unset, map 1->one, 2->two etc.
            up to twelve.
    Return:
        str: The number expressed as a string.

    """
    number = int(number)
    if adjective:
        return _INT2STR_MAP_ADJ.get(number, f"{number}th")
    return _INT2STR_MAP_NOUN.get(number, str(number))

_STR2INT_MAP = {
    "one": 1,
    "two": 2,
    "three": 3,
    "four": 4,
    "five": 5,
    "six": 6,
    "seven": 7,
    "eight": 8,
    "nine": 9,
    "ten": 10,
    "eleven": 11,
    "twelve": 12,
    "thirteen": 13,
    "fourteen": 14,
    "fifteen": 15,
    "sixteen": 16,
    "seventeen": 17,
    "eighteen": 18,
    "nineteen": 19,
    "twenty": 20,
    "thirty": 30,
    "forty": 40,
    "fifty": 50,
    "sixty": 60,
    "seventy": 70,
    "eighty": 80,
    "ninety": 90,
    "hundred": 100,
    "thousand": 1000,
}
_STR2INT_ADJS = {
    "first": 1,
    "second": 2,
    "third": 3,
}

def str2int(number):
    """
    Converts a string to an integer.

    Args:
        number (str): The string to convert. It can be a digit such as "1", or a number word such as "one".

    Returns:
        int: The string represented as an integer.
    """
    number = str(number)
    original_input = number
    try:
        # it's a digit already
        return int(number)
    except:
        # if it's an ordinal number such as "1st", it'll convert to int with the last two characters chopped off
        try:
            return int(number[:-2])
        except:
            pass

    # convert sound changes for generic ordinal numbers
    if number[-2:] == "th":
        # remove "th"
        number = number[:-2]
        if number[-1] == "f":
            # e.g. twelfth, fifth
            number = number[:-1] + "ve"
        elif number[-2:] == "ie":
            # e.g. twentieth, fortieth
            number = number[:-2] + "y"
        # custom case for ninth
        elif number[-3:] == "nin":
            number += "e"

    if i := _STR2INT_MAP.get(number):
        # it's a single number, return it
        return i

    # remove optional "and"s
    number = number.replace(" and ", " ")

    # split number words by spaces, hyphens and commas, to accommodate multiple styles
    numbers = [word.lower() for word in re.split(r"[-\s\,]", number) if word]
    sums = []
    for word in numbers:
        # check if it's a known number-word
        if i := _STR2INT_MAP.get(word):
            if not len(sums):
                # initialize the list with the current value
                sums = [i]
            else:
                # if the previous number was smaller, it's a multiplier
                # e.g. the "two" in "two hundred"
                if sums[-1] < i:
                    sums[-1] = sums[-1] * i
                # otherwise, it's added on, like the "five" in "twenty five"
                else:
                    sums.append(i)
        elif i := _STR2INT_ADJS.get(word):
            # it's a special adj word; ordinal case will never be a multiplier
            sums.append(i)
        else:
            # invalid number-word, raise ValueError
            raise ValueError(f"String {original_input} cannot be converted to int.")
    return sum(sums)

def match_ip(address, pattern) -> bool:
    """
    Check if an IP address matches a given pattern. The pattern can be a single IP address
    such as 8.8.8.8 or a CIDR-formatted subnet like 10.0.0.0/8

    IPv6 is supported to, with CIDR-subnets looking like 2001:db8::/48

    Args:
        address (str): The source address being checked.
        pattern (str): The single IP address or subnet to check against.

    Returns:
        result (bool): Whether it was a match or not.
    """
    try:
        # Convert the given IP address to an IPv4Address or IPv6Address object
        ip_obj = ipaddress.ip_address(address)
        # Invalid IP address format

    try:
        # Check if pattern is a single IP or a subnet
        if "/" in pattern:
            # It's (hopefully) a subnet in CIDR notation
            network = ipaddress.ip_network(pattern, strict=False)
            if ip_obj in network:
        else:
            # It's a single IP address
            if ip_obj == ipaddress.ip_address(pattern):

def ip_from_request(request, exclude=None) -> str:
    """
    Retrieves the IP address from a web Request, while respecting X-Forwarded-For and
    settings.UPSTREAM_IPS.

    Args:
        request (django Request or twisted.web.http.Request): The web request.
        exclude: (list, optional): A list of IP addresses to exclude from the check. If left none,
            then settings.UPSTREAM_IPS will be used.

    Returns:
        ip (str): The IP address the request originated from.
    """
    if exclude is None:
        exclude = settings.UPSTREAM_IPS

    if hasattr(request, "getClientIP"):
        # It's a twisted request.
        remote_addr = request.getClientIP()
        forwarded = request.getHeader("x-forwarded-for")
    else:
        # it's a Django request.
        remote_addr = request.META.get("REMOTE_ADDR")
        forwarded = request.META.get("HTTP_X_FORWARDED_FOR")

    addresses = [remote_addr]

    if forwarded:
        addresses.extend(x.strip() for x in forwarded.split(","))

    for addr in reversed(addresses):
        if all(not match_ip(addr, pattern) for pattern in exclude):
            return addr

    logger.log_warn("ip_from_request: No valid IP address found in request. Using remote_addr.")
    return remote_addr

"""

This is the cmdset for Account (OOC) commands.  These are
stored on the Account object and should thus be able to handle getting
an Account object as caller rather than a Character.

Note - in order for session-rerouting (in MULTISESSION_MODE=2) to
function, all commands in this cmdset should use the self.msg()
command method rather than caller.msg().
"""

from evennia.commands.default import (
    account,
    admin,
    building,
    comms,
    general,
    help,
    system,
)

class AccountCmdSet(CmdSet):
    """
    Implements the account command set.
    """

    key = "DefaultAccount"
    priority = -10

        "Populates the cmdset"

        # Account-specific commands
        self.add(account.CmdOOCLook())
        self.add(account.CmdIC())
        self.add(account.CmdOOC())
        self.add(account.CmdCharCreate())
        self.add(account.CmdCharDelete())
        # self.add(account.CmdSessions())
        self.add(account.CmdWho())
        self.add(account.CmdOption())
        self.add(account.CmdQuit())
        self.add(account.CmdPassword())
        self.add(account.CmdColorTest())
        self.add(account.CmdQuell())
        self.add(account.CmdStyle())

        # nicks
        self.add(general.CmdNick())

        # testing
        self.add(building.CmdExamine())

        # Help command
        self.add(help.CmdHelp())

        # system commands
        self.add(system.CmdReload())
        self.add(system.CmdReset())
        self.add(system.CmdShutdown())
        self.add(system.CmdPy())

        # Admin commands
        self.add(admin.CmdNewPassword())

        # Comm commands
        self.add(comms.CmdChannel())
        self.add(comms.CmdPage())
        self.add(comms.CmdIRC2Chan())
        self.add(comms.CmdIRCStatus())
        self.add(comms.CmdRSS2Chan())
        self.add(comms.CmdGrapevine2Chan())
        self.add(comms.CmdDiscord2Chan())
        # self.add(comms.CmdChannels())
        # self.add(comms.CmdAddCom())
        # self.add(comms.CmdDelCom())
        # self.add(comms.CmdAllCom())
        # self.add(comms.CmdCdestroy())
        # self.add(comms.CmdChannelCreate())
        # self.add(comms.CmdClock())
        # self.add(comms.CmdCBoot())
        # self.add(comms.CmdCWho())
        # self.add(comms.CmdCdesc())

"""
This module ties together all the commands default Character objects have
available (i.e. IC commands). Note that some commands, such as
communication-commands are instead put on the account level, in the
Account cmdset. Account commands remain available also to Characters.
"""
    admin,
    batchprocess,
    building,
    general,
    help,
    system,
)

class CharacterCmdSet(CmdSet):
    """
    Implements the default command set.
    """

    key = "DefaultCharacter"

        # The general commands
        self.add(general.CmdLook())
        self.add(general.CmdHome())
        self.add(general.CmdInventory())
        self.add(general.CmdPose())
        self.add(general.CmdSetDesc())
        self.add(general.CmdGet())
        self.add(general.CmdDrop())
        self.add(general.CmdGive())
        self.add(general.CmdSay())
        self.add(general.CmdWhisper())
        self.add(general.CmdAccess())

        # The help system
        self.add(help.CmdSetHelp())

        # System commands
        self.add(system.CmdAccounts())
        self.add(system.CmdService())
        self.add(system.CmdAbout())
        self.add(system.CmdTime())
        self.add(system.CmdServerLoad())
        # self.add(system.CmdPs())
        self.add(system.CmdTickers())
        self.add(system.CmdTasks())

        self.add(admin.CmdBoot())
        self.add(admin.CmdBan())
        self.add(admin.CmdUnban())
        self.add(admin.CmdEmit())
        self.add(admin.CmdPerm())
        self.add(admin.CmdWall())
        self.add(admin.CmdForce())

        # Building and world manipulation
        self.add(building.CmdTeleport())
        self.add(building.CmdSetObjAlias())
        self.add(building.CmdListCmdSets())
        self.add(building.CmdWipe())
        self.add(building.CmdSetAttribute())
        self.add(building.CmdName())
        self.add(building.CmdDesc())
        self.add(building.CmdCpAttr())
        self.add(building.CmdMvAttr())
        self.add(building.CmdCopy())
        self.add(building.CmdFind())
        self.add(building.CmdOpen())
        self.add(building.CmdLink())
        self.add(building.CmdUnLink())
        self.add(building.CmdCreate())
        self.add(building.CmdDig())
        self.add(building.CmdTunnel())
        self.add(building.CmdDestroy())
        self.add(building.CmdTypeclass())
        self.add(building.CmdLock())
        self.add(building.CmdSetHome())
        self.add(building.CmdTag())
        self.add(building.CmdSpawn())
        self.add(building.CmdScripts())
        self.add(building.CmdObjects())

        # Batchprocessor commands
        self.add(batchprocess.CmdBatchCommands())
        self.add(batchprocess.CmdBatchCode())

"""
This module stores session-level commands.
"""
from evennia.commands.default import account

class SessionCmdSet(CmdSet):
    """
    Sets up the unlogged cmdset.
    """

    key = "DefaultSession"
    priority = -20

        "Populate the cmdset"
        self.add(account.CmdSessions())

"""
This module describes the unlogged state of the default game.
The setting STATE_UNLOGGED should be set to the python path
of the state instance in this module.
"""
from evennia.commands.default import unloggedin

class UnloggedinCmdSet(CmdSet):
    """
    """

    key = "DefaultUnloggedin"

        self.add(unloggedin.CmdUnconnectedConnect())
        self.add(unloggedin.CmdUnconnectedCreate())
        self.add(unloggedin.CmdUnconnectedQuit())
        self.add(unloggedin.CmdUnconnectedLook())
        self.add(unloggedin.CmdUnconnectedHelp())
        self.add(unloggedin.CmdUnconnectedEncoding())
        self.add(unloggedin.CmdUnconnectedScreenreader())
        self.add(unloggedin.CmdUnconnectedInfo())

"""
Communication commands:

- channel
- page
- irc/rss/grapevine/discord linking

"""

from evennia.accounts import bots
from evennia.comms.comms import DefaultChannel
from evennia.comms.models import Msg
from evennia.utils.evmenu import ask_yes_no
from evennia.utils.logger import tail_log_file
from evennia.utils.utils import class_from_module, strip_unsafe_input

CHANNEL_DEFAULT_TYPECLASS = class_from_module(
    settings.BASE_CHANNEL_TYPECLASS, fallback=settings.FALLBACK_CHANNEL_TYPECLASS
)

__all__ = (
    "CmdChannel",
    "CmdObjectChannel",
    "CmdPage",
    "CmdIRC2Chan",
    "CmdIRCStatus",
    "CmdRSS2Chan",
    "CmdGrapevine2Chan",
    "CmdDiscord2Chan",
)

# helper functions to make it easier to override the main CmdChannel
# command and to keep the legacy addcom etc commands around.

class CmdChannel(COMMAND_DEFAULT_CLASS):
    """
    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.

        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.

    """

    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)"
        "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

    def search_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.

        """
        # first see if this is a personal alias
        channelname = caller.nicks.get(key=channelname, category="channel") or channelname

        # always try the exact match first.
        channels = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname, exact=True)

        if not channels and not exact:
            # try fuzzy matching as well
            channels = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname, exact=exact)

        # check permissions
        channels = [
            channel
            for channel in channels
            if channel.access(caller, "listen") or channel.access(caller, "control")
        ]

        if handle_errors:
            if not channels:
                self.msg(
                    f"No channel found matching '{channelname}' "
                    "(could also be due to missing access)."
                )
            elif len(channels) > 1:
                self.msg(
                    f"Multiple possible channel matches/alias for '{channelname}':\n"
                    + ", ".join(chan.key for chan in channels)
                )
            return channels[0]
        else:
                return []
                return list(channels)
            return [channels[0]]

    def msg_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.

        """
        if not channel.access(self.caller, "send"):
            self.msg(f"You are not allowed to send messages to channel {channel}")
            return

        # avoid unsafe tokens in message
        message = strip_unsafe_input(message, self.session)

        channel.msg(message, senders=self.caller, **kwargs)

    def get_channel_history(self, channel, start_index=0):
        """
        View a channel's history.

        Args:
            channel (Channel): The channel to access.

        """
        log_file = channel.get_log_filename()

        def send_msg(lines):
            return self.msg(
                "".join(line.split("[-]", 1)[1] if "[-]" in line else line for line in lines)
            )

        # asynchronously tail the log file
        tail_log_file(log_file, start_index, 20, callback=send_msg)

    def sub_to_channel(self, channel):
        """
        Subscribe to a channel. Note that all permissions should
        be checked before this step.

        Args:

        Returns:
            bool, str: True, None if connection failed. If False,
                the second part is an error string.

        """

        if channel.has_connection(caller):
            return False, f"Already listening to channel {channel.key}."

        # this sets up aliases in post_join_channel by default
        result = channel.connect(caller)

        return result, "" if result else f"Were not allowed to subscribe to channel {channel.key}"

    def unsub_from_channel(self, channel, **kwargs):
        """
        Un-Subscribe to a channel. Note that all permissions should

        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,

        """

        if not channel.has_connection(caller):
            return False, f"Not listening to channel {channel.key}."

        # this will also clean aliases
        result = channel.disconnect(caller)

        return result, "" if result else f"Could not unsubscribe from channel {channel.key}"

    def add_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)

    def remove_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,
            **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.

        """
        if self.caller.nicks.has(alias, category="channel", **kwargs):
            DefaultChannel.remove_user_channel_alias(self.caller, alias)
            return True, ""
        return False, "No such alias was defined."

    def get_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)
        if nicktuples:
            return [tup[2] for tup in nicktuples if tup[3].lower() == chan_key]
        return []

    def mute_channel(self, channel):
        """
        Temporarily mute a channel.

        Args:

        Returns:
            bool, str: True, None if muting successful. If False,
        """
        if channel.mute(self.caller):
        return False, f"Channel {channel.key} was already muted."

    def unmute_channel(self, channel):
        """
        Unmute a channel.

        Args:

        Returns:
            bool, str: True, None if unmuting successful. If False,

        """
        if channel.unmute(self.caller):
        return False, f"Channel {channel.key} was already unmuted."

    def create_channel(self, name, description, typeclass=None, aliases=None):
        """
        Create a new channel. Its name must not previously exist (case agnostic)
        (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,

        """
            typeclass = class_from_module(typeclass)
        else:
            typeclass = CHANNEL_DEFAULT_TYPECLASS

        if typeclass.objects.channel_search(name, exact=True):
            return False, 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)
        return new_chan, ""

    def destroy_channel(self, channel, message=None):
        """
        Destroy an existing channel. Access should be checked before
        calling this function.

        Args:
            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.

           pass

        """

        channel_key = channel.key
        if message is None:
            message = (
                f"|rChannel {channel_key} is being destroyed. "
                "Make sure to clean any channel aliases.|n"
            )
        if message:
            channel.msg(message, senders=caller, bypass_mute=True)
        channel.delete()
        logger.log_sec(f"Channel {channel_key} was deleted by {caller}")

    def set_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,

        """
        try:
            channel.locks.add(lockstring)
        except LockException as err:
            return False, err
        return True, ""

    def unset_lock(self, channel, lockstring):
        """
        Remove locks in a lockstring on a channel. Permissions must have been

        Args:

        Returns:

        """
        try:
            channel.locks.remove(lockstring)

    def set_desc(self, channel, description):
        """
        Set a channel description. This is shown in listings etc.

        Args:
            caller (Object or Account): The entity performing the action.
            description (str): A short description of the channel.

        Returns:

        """
        channel.db.desc = description

    def boot_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:
            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:

        """
        if not channel.subscriptions.has(target):
            return False, f"{target} is not connected to channel {channel.key}."
        # find all of target's nicks linked to this channel and delete them
        for nick in [
            nick
            for nick in target.nicks.get(category="channel") or []
            if nick.value[3].lower() == channel.key
        ]:
            nick.delete()
        channel.disconnect(target)
        reason = f" Reason: {reason}" if reason else ""
        target.msg(f"You were booted from channel {channel.key} by {self.caller.key}.{reason}")
        if not quiet:
            channel.msg(f"{target.key} was booted from channel by {self.caller.key}.{reason}")

            f"Channel Boot: {target} (Channel: {channel}, "
            f"Reason: {reason.strip()}, Caller: {self.caller}"
        )

    def ban_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:
            target (Object or Account): The entity to ban
            reason (str, optional): A reason for the ban

        Returns:
            bool, str: True, None if banning was successful. If False,

        """
        self.boot_user(channel, target, quiet=quiet, reason=reason)
        if channel.ban(target):
        return False, f"{target} is already banned from this channel."

    def unban_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:
            target (Object or Account): The entity to unban

        Returns:
            bool, str: True, None if unbanning was successful. If False,

        """
        if channel.unban(target):
        return False, f"{target} was not previously banned from this channel."

    def channel_list_bans(self, channel):
        """
        Show a channel's bans.

        Args:

        Returns:
            list: A list of strings, each the name of a banned user.

        """
        return [banned.key for banned in channel.banlist]

    def channel_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:

        Returns:
            list: A list of prepared strings, with name + markers for if they are
                muted or offline.

        """
        mute_list = list(channel.mutelist)
        online_list = channel.subscriptions.online()
        if channel.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 = []
        for subscriber in all_subs:
            name = subscriber.get_display_name(caller)
            conditions = (
                "muting" if subscriber in mute_list else "",
                "offline" if subscriber not in online_list else "",
            )
            conditions = [cond for cond in conditions if cond]
            cond_text = "(" + ", ".join(conditions) + ")" if conditions else ""
            who_list.append(f"{name}{cond_text}")

        return who_list

    def list_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.

        """
        subscribed_channels = list(channelcls.objects.get_subscriptions(caller))
        unsubscribed_available_channels = [
            chan
            for chan in channelcls.objects.get_all_channels()
            if chan not in subscribed_channels and chan.access(caller, "listen")
        ]
        return subscribed_channels, unsubscribed_available_channels

    def display_subbed_channels(self, subscribed):
        """
        Display channels subscribed to.

        Args:
            subscribed (list): List of subscribed channels

        Returns:
            EvTable: Table to display.

        """
        comtable = self.styled_table(
            "id",
            "channel",
            "my aliases",
            "locks",
            "description",
            align="l",
            maxwidth=_DEFAULT_WIDTH,
        )
        for chan in subscribed:
            locks = "-"
            chanid = "-"
            if chan.access(self.caller, "control"):
                locks = chan.locks
                chanid = chan.id

            my_aliases = ", ".join(self.get_channel_aliases(chan))
            comtable.add_row(
                *(
                    chanid,
                    "{key}{aliases}".format(
                        key=chan.key,
                        aliases=";" + ";".join(chan.aliases.all()) if chan.aliases.all() else "",
                    ),
                    my_aliases,
                    locks,
                    chan.db.desc,
                )
            )
        return comtable

    def display_all_channels(self, subscribed, available):
        """
        Display all available channels

        Args:
        Returns:

        """

            "sub",
            "aliases",
        )
        channels = subscribed + available

        for chan in channels:
            if chan not in subscribed:
                substatus = "|rNo|n"
            elif caller in chan.mutelist:
                substatus = "|rMuting|n"
            else:
                substatus = "|gYes|n"
                *(
                    substatus,
                    chan.key,
                    ",".join(chan.aliases.all()) if chan.aliases.all() else "",
                )
            )
        comtable.reformat_column(0, width=8)

        """
        Main functionality of command.
        """
        # from evennia import set_trace;set_trace()

        channel_names = [name for name in self.lhslist if name]

        if "all" in switches:
            # 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

        if not channel_names:
            # empty arg show only subscribed channels
            subscribed, _ = self.list_channels()
            table = self.display_subbed_channels(subscribed)

            self.msg(f"\n|wChannel subscriptions|n (use |w/all|n to see all available):\n{table}")
            return

        if not self.switches and not self.args:
            self.msg("Usage[/switches]: channel [= message]")
            return

        if "create" in switches:
            # create a new channel
            if not self.access(caller, "manage"):
                self.msg("You don't have access to use channel/create.")
                return

            config = self.lhs
            if not config:
                self.msg("To create: channel/create name[;aliases][:typeclass] [= description]")
                return
            name, *typeclass = config.rsplit(":", 1)
            typeclass = typeclass[0] if typeclass else None
            name, *aliases = name.rsplit(";")
            description = self.rhs or ""
            chan, err = self.create_channel(name, description, typeclass=typeclass, aliases=aliases)
            if chan:
                self.msg(f"Created (and joined) new channel '{chan.key}'.")
            else:
            return

        if "unalias" in switches:
            # remove a personal alias (no channel needed)
            alias = self.args.strip()
            if not alias:
                self.msg("Specify the alias to remove as channel/unalias <alias>")
                return
            success, err = self.remove_alias(alias)
            if success:
                self.msg(f"Removed your channel alias '{alias}'.")
            else:
            return

        possible_lhs_message = ""
        if not self.rhs and self.args and " " in self.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) :]
            if possible_lhs_message.strip() == "=":
                possible_lhs_message = ""
            channel_names.append(no_rhs_channel_name)

        channels = []
        errors = []
        for channel_name in channel_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)
            if not found_channels:
                    f"No channel found matching '{channel_name}' "
                )
            elif len(found_channels) > 1:
                    f"Multiple possible channel matches/alias for '{channel_name}':\n"
                    + ", ".join(chan.key for chan in found_channels)
                )
            else:
                channels.append(found_channels[0])

        if not channels:
            self.msg("\n".join(errors))
            return

        # we have at least one channel at this point
        channel = channels[0]

        if not switches:
                # send message to channel
                self.msg_channel(channel, self.rhs.strip())
            elif channel and possible_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()
                if channel in subscribed:
                    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) "
                        "to chat and the 'channel' command "
                        f"to customize)\n{table}"
                    )
                elif channel in available:
                    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" in switches or "hist" in switches:
            # view channel history

            index = self.rhs or 0
            try:
                index = max(0, int(index))
                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" in switches:
            # subscribe to a channel
            aliases = []
                aliases = set(alias.strip().lower() for alias in self.rhs.split(";"))
            success, err = self.sub_to_channel(channel)
                for alias in aliases:
                    self.add_alias(channel, alias)
                alias_txt = ", ".join(aliases)
                alias_txt = f" using alias(es) {alias_txt}" if aliases else ""
                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:
            return

        if "unsub" in switches:
            # un-subscribe from a channel
            success, err = self.unsub_from_channel(channel)
                self.msg(f"You un-subscribed from channel {channel.key}. All aliases were cleared.")
            else:
            return

        if "alias" in switches:
            # create a new personal alias for a channel
            alias = self.rhs
                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" in switches:
            # mute a given channel
            success, err = self.mute_channel(channel)
                self.msg(f"Muted channel {channel.key}.")
            else:
            return

        if "unmute" in switches:
            # unmute a given channel
            success, err = self.unmute_channel(channel)
                self.msg(f"Un-muted channel {channel.key}.")
            else:
            return

        if "destroy" in switches or "delete" in switches:
            # destroy a channel we control

                self.msg("You don't have access to use channel/destroy.")
                return

            if not channel.access(caller, "control"):
                self.msg("You can only delete channels you control.")
                return

            reason = self.rhs or None

            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" in switches:
            # set channel description

                self.msg("You don't have access to use channel/desc.")
                return

                self.msg("You can only change description of channels you control.")
                return

            desc = self.rhs.strip()

            if not desc:
                self.msg("Usage: /desc channel = description")
                return

            self.set_desc(channel, desc)
            self.msg("Updated channel description.")

        if "lock" in switches:
            # add a lockstring to channel

            if not self.access(caller, "changelocks"):
                self.msg("You don't have access to use channel/lock.")
                return

                self.msg("You need 'control'-access to change locks on this channel.")
                return

            lockstring = self.rhs.strip()

            if not lockstring:
                self.msg("Usage: channel/lock channelname = lockstring")
                return

            success, err = self.set_lock(channel, self.rhs)
                self.msg("Added/updated lock on channel.")
            else:
                self.msg(f"Could not add/update lock: {err}")
            return

        if "unlock" in switches:
            # remove/update lockstring from channel

                self.msg("You don't have access to use channel/unlock.")
                return

                return

                self.msg("Usage: channel/unlock channelname = lockstring")
                return

            success, err = self.unset_lock(channel, self.rhs)
                self.msg("Removed lock from channel.")
            else:
                self.msg(f"Could not remove lock: {err}")
            return

        if "boot" in switches:
            # boot a user from channel(s)

            if not self.access(caller, "admin"):
                self.msg("You don't have access to use channel/boot.")
                return

            if not self.rhs:
                self.msg("Usage: channel/boot channel[,channel,...] = username [:reason]")
                return

            target_str, *reason = self.rhs.rsplit(":", 1)
            reason = reason[0].strip() if reason else ""

            for chan in channels:
                if not chan.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())
                if not target:
                    self.msg(f"Cannot boot '{target_str}' - not in channel {chan.key}.")
                    return

            def _boot_user(caller, *args, **kwargs):
                for chan in channels:
                    success, err = self.boot_user(chan, target, quiet=False, reason=reason)
                    if success:
                        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.key for chan in channels)
            reasonwarn = (
                ". Also note that your reason will be echoed to the channel" if reason else ""
            )
                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,
                default="Y",
            )
            return

        if "ban" in switches:
            # ban a user from channel(s)

                self.msg("You don't have access to use channel/ban.")
                return

                # view bans for channels

                if not channel.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

                    self.msg(f"You don't have access to ban users on channel {chan.key}")
                    return

                    self.msg(f"Cannot ban '{target_str}' - not in channel {chan.key}.")
                    return

            def _ban_user(caller, *args, **kwargs):
                    success, err = self.ban_user(chan, target, quiet=False, reason=reason)
                        self.msg(f"Banned {target.key} from channel {chan.key}.")
                    else:

            )
                caller,
                (
                    f"Are you sure you want to ban user {target.key} from "
                    f"channel(s) {channames} (make sure name/channels are correct{reasonwarn}) "
                ),
                _ban_user,
                "Aborted.",
            )
            return

        if "unban" in switches:
            # unban a previously banned user from channel

                self.msg("You don't have access to use channel/unban.")
                return

            target_str = self.rhs.strip()

            if not target_str:
                self.msg("Usage: channel[,channel,...] = user")
                return

            banlists = []
                    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)
                self.msg(f"Could not find a banned user '{target_str}' in given channel(s).")
                return

                success, err = self.unban_user(channel, target)
                if success:
                    self.msg(f"Un-banned {target_str} from channel {chan.key}")
                else:
                    self.msg(err)
            return

        if "who" in switches:
            # 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.
class CmdObjectChannel(CmdChannel):
    account_caller = False

class CmdPage(COMMAND_DEFAULT_CLASS):
    """
    send a private message to another account

    Usage:
      page <account> <message>
      page[/switches] [<account>,<account>,... = <message>]
      tell        ''
      page <number>

    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. The equal sign is needed for
    multiple targets or if sending to target with space in the name.

    """

    key = "page"
    aliases = ["tell"]
    switch_options = ("last", "list")
    locks = "cmd:not pperm(page_banned)"

    # this is used by the COMMAND_DEFAULT_CLASS parent

        """Implement function using the Msg methods"""

        # Since account_caller is set above, this will be an Account.

        # get the messages we've sent (not to channels)
        pages_we_sent = Msg.objects.get_messages_by_sender(caller).order_by("-db_date_created")
        # get last messages we've got
        pages_we_got = Msg.objects.get_messages_by_receiver(caller).order_by("-db_date_created")
        targets, message, number = [], None, None

        if "last" in self.switches:
            if pages_we_sent:
                recv = ",".join(obj.key for obj in pages_we_sent[0].receivers)
                self.msg(f"You last paged |c{recv}|n:{pages_we_sent[0].message}")
                return
            else:
                self.msg("You haven't paged anyone yet.")
                return

                for target in self.lhslist:
                    target_obj = self.caller.search(target)
                    if not target_obj:
                        return
                    targets.append(target_obj)
                message = self.rhs.strip()
            else:
                target, *message = self.args.split(" ", 1)
                if target and target.isnumeric():
                    # a number to specify a historic page
                    number = int(target)
                elif target:
                    target_obj = self.caller.search(target, quiet=True)
                    if target_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] if message else "")
                else:
                    # a single-word message
                    message = message[0].strip()

        pages = list(pages_we_sent) + list(pages_we_got)
        pages = sorted(pages, key=lambda page: page.date_created)

            # send a message
            if not targets:
                # no target given - send to last person we paged
                if pages_we_sent:
                    targets = pages_we_sent[0].receivers
                else:
                    self.msg("Who do you want page?")
                    return

            header = f"|wAccount|n |c{caller.key}|n |wpages:|n"
            if message.startswith(":"):
                message = f"{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 = []
            for target in targets:
                if not target.access(caller, "msg"):
                    rstrings.append(f"You are not allowed to page {target}.")
                    continue
                target.msg(f"{header} {message}")
                if hasattr(target, "sessions") and not target.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")
            if rstrings:
                self.msg("\n".join(rstrings))
            self.msg("You paged %s with: '%s'." % (", ".join(received), message))
            return

        else:
            # no message to send
            if number is not None and len(pages) > number:
                lastpages = pages[-number:]
            else:
                lastpages = pages
            to_template = "|w{date}{clr} {sender}|nto{clr}{receiver}|n:> {message}"
            from_template = "|w{date}{clr} {receiver}|nfrom{clr}{sender}|n:< {message}"
            listing = []
            prev_selfsend = False
            for page in lastpages:
                multi_send = len(page.senders) > 1
                multi_recv = len(page.receivers) > 1
                sending = self.caller in page.senders
                # self-messages all look like sends, so we assume they always
                # come in close pairs and treat the second of the pair as the recv.
                selfsend = sending and self.caller in page.receivers
                if selfsend:
                    if prev_selfsend:
                        # this is actually a receive of a self-message
                        sending = False
                        prev_selfsend = False
                    else:
                        prev_selfsend = True

                clr = "|c" if sending else "|g"

                sender = f"|n,{clr}".join(obj.key for obj in page.senders)
                receiver = f"|n,{clr}".join([obj.name for obj in page.receivers])
                if sending:
                    template = to_template
                    sender = f"{sender} " if multi_send else ""
                    receiver = f" {receiver}" if multi_recv else f" {receiver}"
                else:
                    template = from_template
                    receiver = f"{receiver} " if multi_recv else ""
                    sender = f" {sender} " if multi_send else f" {sender}"

                listing.append(
                    template.format(
                        date=utils.datetime_format(page.date_created),
                        clr=clr,
                        sender=sender,
                        receiver=receiver,
                        message=page.message,
                    )
                )
            lastpages = "\n ".join(listing)

            if lastpages:
                string = f"Your latest pages:\n {lastpages}"
            else:
                string = "You haven't paged anyone yet."
            return

def _list_bots(cmd):
    """
    Helper function to produce a list of all IRC bots.

    Args:
        cmd (Command): Instance of the Bot command.
    Returns:
        bots (str): A table of bots or an error message.

    """
    ircbots = [
        bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")
    ]
    if ircbots:
        table = cmd.styled_table(
            "|w#dbref|n",
            "|wbotname|n",
            "|wev-channel|n",
            "|wirc-channel|n",
            "|wSSL|n",
        )
        for ircbot in ircbots:
            ircinfo = "%s (%s:%s)" % (
                ircbot.db.irc_channel,
                ircbot.db.irc_network,
                ircbot.db.irc_port,
            )
                "#%i" % ircbot.id,
                ircbot.db.irc_botname,
                ircbot.db.ev_channel,
                ircinfo,
                ircbot.db.irc_ssl,
            )
    else:
        return "No irc bots found."

class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
    """
    Link an evennia channel to an external IRC channel

    Usage:
      irc2chan[/switches] <evennia_channel> = <ircnetwork> <port> <#irchannel> <botname>[:typeclass]
      irc2chan/delete botname|#dbid

    Switches:
      /delete     - this will delete the bot and remove the irc connection
                    to the channel. Requires the botname or #dbid as input.
      /remove     - alias to /delete
      /disconnect - alias to /delete
      /list       - show all irc<->evennia mappings
      /ssl        - use an SSL-encrypted connection

    Example:
      irc2chan myircchan = irc.dalnet.net 6667 #mychannel evennia-bot
      irc2chan public = irc.freenode.net 6667 #evgaming #evbot:accounts.mybot.MyBot

    This creates an IRC bot that connects to a given IRC network and
    channel. If a custom typeclass path is given, this will be used
    instead of the default bot class.
    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.
    Provide an optional bot class path to use a custom bot.
    """

    key = "irc2chan"
    switch_options = ("delete", "remove", "disconnect", "list", "ssl")
    locks = "cmd:serversetting(IRC_ENABLED) and pperm(Developer)"

        """Setup the irc-channel mapping"""

        if not settings.IRC_ENABLED:
            string = """IRC is not enabled. You need to activate it in game/settings.py."""
            return

            # show all connections
            self.msg(_list_bots(self))
            return

        if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches:
            botname = f"ircbot-{self.lhs}"
            matches = AccountDB.objects.filter(db_is_bot=True, username=botname)
            dbref = utils.dbref(self.lhs)
            if not matches and dbref:
                # try dbref match
                matches = AccountDB.objects.filter(db_is_bot=True, id=dbref)
            if matches:
                matches[0].delete()
                self.msg("IRC connection destroyed.")
            else:
                self.msg("IRC connection/bot could not be removed, does it exist?")
            return

            string = (
                "Usage: irc2chan[/switches] <evennia_channel> ="
                " <ircnetwork> <port> <#irchannel> <botname>[:typeclass]"
            )
            return

        channel = self.lhs
        self.rhs = self.rhs.replace("#", " ")  # to avoid Python comment issues
        try:
            irc_network, irc_port, irc_channel, irc_botname = [
                part.strip() for part in self.rhs.split(None, 4)
            ]
            irc_channel = f"#{irc_channel}"
            string = "IRC bot definition '%s' is not valid." % self.rhs
            return

        botclass = None
        if ":" in irc_botname:
            irc_botname, botclass = [part.strip() for part in irc_botname.split(":", 2)]
        botname = f"ircbot-{irc_botname}"
        # If path given, use custom bot otherwise use default.
        botclass = botclass if botclass else bots.IRCBot
        irc_ssl = "ssl" in self.switches

        # create a new bot
        bot = AccountDB.objects.filter(username__iexact=botname)
        if bot:
            # re-use an existing bot
            bot = bot[0]
            if not bot.is_bot:
                self.msg(f"Account '{botname}' already exists and is not a bot.")
                return
        else:
            try:
                bot = create.create_account(botname, None, None, typeclass=botclass)
                self.msg(f"|rError, could not create the bot:|n '{err}'.")
                return
        bot.start(
            ev_channel=channel,
            irc_botname=irc_botname,
            irc_channel=irc_channel,
            irc_network=irc_network,
            irc_port=irc_port,
            irc_ssl=irc_ssl,
        )
        self.msg("Connection created. Starting IRC bot.")

class CmdIRCStatus(COMMAND_DEFAULT_CLASS):
    """
    Check and reboot IRC bot.

    Usage:
        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
    see if the connection is still responsive. The 'nicklist' argument
    (aliases are 'who' and 'users') will return a list of users on the
    remote IRC channel.  Finally, 'reconnect' will force the client to
    disconnect and reconnect again. This may be a last resort if the
    client has silently lost connection (this may happen if the remote
    network experience network issues). During the reconnection
    messages sent to either channel will be lost.

    """

    key = "ircstatus"
    locks = "cmd:serversetting(IRC_ENABLED) and perm(ircstatus) or perm(Builder))"

        """Handles the functioning of the command."""

            return
        # should always be on the form botname option
        args = self.args.split()
        if len(args) != 2:
            self.msg("Usage: ircstatus [#dbref ping||nicklist||reconnect]")
            return
        botname, option = args
        if option not in ("ping", "users", "reconnect", "nicklist", "who"):
            self.msg("Not a valid option.")
            return
        if utils.dbref(botname):
            matches = AccountDB.objects.filter(db_is_bot=True, id=utils.dbref(botname))
            self.msg(
                "No matching IRC-bot found. Use ircstatus without arguments to list active bots."
            )
            return
        ircbot = matches[0]
        channel = ircbot.db.irc_channel
        network = ircbot.db.irc_network
        port = ircbot.db.irc_port
        chtext = f"IRC bot '{ircbot.db.irc_botname}' on channel {channel} ({network}:{port})"
        if option == "ping":
            # check connection by sending outself a ping through the server.
            self.msg(f"Pinging through {chtext}.")
            ircbot.ping(self.caller)
        elif option in ("users", "nicklist", "who"):
            # retrieve user list. The bot must handles the echo since it's
            # an asynchronous call.
            self.msg(f"Requesting nicklist from {channel} ({network}:{port}).")
            ircbot.get_nicklist(self.caller)
        elif self.caller.locks.check_lockstring(
            self.caller, "dummy:perm(ircstatus) or perm(Developer)"
        ):
            # reboot the client
            self.msg(f"Forcing a disconnect + reconnect of {chtext}.")
            ircbot.reconnect()
        else:
            self.msg("You don't have permission to force-reload the IRC bot.")

# RSS connection
class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
    """
    link an evennia channel to an external RSS feed

    Usage:
      rss2chan[/switches] <evennia_channel> = <rss_url>

    Switches:
      /disconnect - this will stop the feed and remove the connection to the
                    channel.
      /remove     -                                 "
      /list       - show all rss->evennia mappings

    Example:
      rss2chan rsschan = http://code.google.com/feeds/p/evennia/updates/basic

    This creates an RSS reader  that connects to a given RSS feed url. Updates
    will be echoed as a title and news link to the given channel. The rate of
    updating is set with the RSS_UPDATE_INTERVAL variable in settings (default
    is every 10 minutes).

    When disconnecting you need to supply both the channel and url again so as
    to identify the connection uniquely.
    """

    key = "rss2chan"
    switch_options = ("disconnect", "remove", "list")
    locks = "cmd:serversetting(RSS_ENABLED) and pperm(Developer)"

        """Setup the rss-channel mapping"""

        # checking we have all we need
        if not settings.RSS_ENABLED:
            string = """RSS is not enabled. You need to activate it in game/settings.py."""
            return
        try:
            import feedparser

            assert feedparser  # to avoid checker error of not being used
            string = (
                "RSS requires python-feedparser (https://pypi.python.org/pypi/feedparser)."
                " Install before continuing."
            )
            return

            rssbots = [
                bot
                for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="rssbot-")
            ]
            if rssbots:
                table = self.styled_table(
                    "|wdbid|n",
                    "|wupdate rate|n",
                    "|wev-channel",
                    "|wRSS feed URL|n",
                    border="cells",
                    maxwidth=_DEFAULT_WIDTH,
                )
                for rssbot in rssbots:
                    table.add_row(
                        rssbot.id, rssbot.db.rss_rate, rssbot.db.ev_channel, rssbot.db.rss_url
                    )
                self.msg(table)
            else:
                self.msg("No rss bots found.")
            return

            botname = f"rssbot-{self.lhs}"
            matches = AccountDB.objects.filter(db_is_bot=True, db_key=botname)
            if not matches:
                matches = AccountDB.objects.filter(db_is_bot=True, id=self.args.lstrip("#"))
                self.msg("RSS connection destroyed.")
            else:
                self.msg("RSS connection/bot could not be removed, does it exist?")
            return

            string = "Usage: rss2chan[/switches] <evennia_channel> = <rss url>"
            return
        url = self.rhs

        botname = f"rssbot-{url}"
        if bot:
            # re-use existing bot
                return
        else:
            # create a new bot
            bot = create.create_account(botname, None, None, typeclass=bots.RSSBot)
        bot.start(ev_channel=channel, rss_url=url, rss_rate=10)
        self.msg("RSS reporter created. Fetching RSS.")

class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS):
    """
    Link an Evennia channel to an external Grapevine channel

    Usage:
      grapevine2chan[/switches] <evennia_channel> = <grapevine_channel>
      grapevine2chan/disconnect <connection #id>

    Switches:
        /list     - (or no switch): show existing grapevine <-> Evennia
                    mappings and available grapevine chans
        /remove   - alias to disconnect
        /delete   - alias to disconnect

    Example:
        grapevine2chan mygrapevine = gossip

    This creates a link between an in-game Evennia channel and an external
    Grapevine channel. The game must be registered with the Grapevine network
    (register at https://grapevine.haus) and the GRAPEVINE_* auth information
    must be added to game settings.
    """

    key = "grapevine2chan"
    switch_options = ("disconnect", "remove", "delete", "list")
    locks = "cmd:serversetting(GRAPEVINE_ENABLED) and pperm(Developer)"

        """Setup the Grapevine channel mapping"""

        if not settings.GRAPEVINE_ENABLED:
            self.msg("Set GRAPEVINE_ENABLED=True in settings to enable.")
            return

            gwbots = [
                bot
                for bot in AccountDB.objects.filter(
                    db_is_bot=True, username__startswith="grapevinebot-"
                )
            ]
            if gwbots:
                    "|wgw-channel|n",
                )
                for gwbot in gwbots:
                    table.add_row(gwbot.id, gwbot.db.ev_channel, gwbot.db.grapevine_channel)
            else:
                self.msg("No grapevine bots found.")
            return

            botname = f"grapevinebot-{self.lhs}"

                self.msg("Grapevine connection destroyed.")
            else:
                self.msg("Grapevine connection/bot could not be removed, does it exist?")
            return

            string = "Usage: grapevine2chan[/switches] <evennia_channel> = <grapevine_channel>"
            return

        grapevine_channel = self.rhs

        botname = "grapewinebot-%s-%s" % (channel, grapevine_channel)
        if bot:
                return
            else:
                self.msg(f"Reusing bot '{botname}' ({bot.dbref})")
        else:
            bot = create.create_account(botname, None, None, typeclass=bots.GrapevineBot)

        bot.start(ev_channel=channel, grapevine_channel=grapevine_channel)
        self.msg(f"Grapevine connection created {channel} <-> {grapevine_channel}.")

class CmdDiscord2Chan(COMMAND_DEFAULT_CLASS):
    """
    Link an Evennia channel to an external Discord channel

    Usage:
      discord2chan[/switches]
      discord2chan[/switches] <evennia_channel> [= <discord_channel_id>]

    Switches:
        /list    - (or no switch) show existing Evennia <-> Discord links
        /remove  - remove an existing link by link ID
        /delete  - alias to remove
        /guild   - toggle the Discord server tag on/off
        /channel - toggle the Evennia/Discord channel tags on/off
        /start   - tell the bot to start, in case it lost its connection

    Example:
        discord2chan mydiscord = 555555555555555

    Discord channel. You must have a valid Discord bot application
    ( https://discord.com/developers/applications ) and your DISCORD_BOT_TOKEN
    must be added to settings. (Please put it in secret_settings !)
    """

    key = "discord2chan"
    aliases = ("discord",)
        "channel",
        "delete",
        "guild",
        "list",
        "remove",
        "start",
    )
    locks = "cmd:serversetting(DISCORD_ENABLED) and pperm(Developer)"

        """Manage the Evennia<->Discord channel links"""

        if not settings.DISCORD_BOT_TOKEN:
            self.msg(
                "You must add your Discord bot application token to settings as DISCORD_BOT_TOKEN"
            )
            return

        discord_bot = [
            bot for bot in AccountDB.objects.filter(db_is_bot=True, username="DiscordBot")
        ]
        if not discord_bot:
            # create a new discord bot
            bot_class = class_from_module(settings.DISCORD_BOT_CLASS, fallback=bots.DiscordBot)
            discord_bot = create.create_account("DiscordBot", None, None, typeclass=bot_class)
            discord_bot.start()
            self.msg("Created and initialized a new Discord relay bot.")
        else:
            discord_bot = discord_bot[0]

        if not discord_bot.is_typeclass(settings.DISCORD_BOT_CLASS, exact=True):
            self.msg(
                f"WARNING: The Discord bot's typeclass is '{discord_bot.typeclass_path}'. This does"
                f" not match {settings.DISCORD_BOT_CLASS} in settings!"
            )

        if "start" in self.switches:
            if discord_bot.sessions.all():
                self.msg("The Discord bot is already running.")
            else:
                discord_bot.start()
                self.msg("Starting the Discord bot session.")
            return

        if "guild" in self.switches:
            discord_bot.db.tag_guild = not discord_bot.db.tag_guild
            self.msg(
                f"Messages to Evennia |wwill {'' if discord_bot.db.tag_guild else 'not '}|ninclude"
                " the Discord server."
            )
            return
        if "channel" in self.switches:
            discord_bot.db.tag_channel = not discord_bot.db.tag_channel
            self.msg(
                f"Relayed messages |wwill {'' if discord_bot.db.tag_channel else 'not '}|ninclude"
                " the originating channel."
            )
            return

        if "list" in self.switches or not self.args:
            if channel_list := discord_bot.db.channels:
                    "|wLink ID|n",
                    "|wEvennia|n",
                    "|wDiscord|n",
                )
                # iterate through the channel links
                # load in the pretty names for the discord channels from cache
                dc_chan_names = discord_bot.attributes.get("discord_channels", {})
                for i, (evchan, dcchan) in enumerate(channel_list):
                    dc_info = dc_chan_names.get(dcchan, {"name": dcchan, "guild": "unknown"})
                        i, evchan, f"#{dc_info.get('name','?')}@{dc_info.get('guild','?')}"
                    )
            else:
                self.msg("No Discord connections found.")
            return

                try:
                    lid = int(self.args.strip())
                    self.msg("Usage: discord2chan/remove <link id>")
                    return
                if lid < len(channel_list):
                    ev_chan, dc_chan = discord_bot.db.channels.pop(lid)
                    dc_chan_names = discord_bot.attributes.get("discord_channels", {})
                    dc_info = dc_chan_names.get(dc_chan, {"name": "unknown", "guild": "unknown"})
                    self.msg(
                        f"Removed link between {ev_chan} and"
                        f" #{dc_info.get('name','?')}@{dc_info.get('guild','?')}"
                    )
                    return
            else:
                self.msg("There are no active connections to Discord.")
                return

        ev_channel = self.lhs
        dc_channel = self.rhs

        if ev_channel and not dc_channel:
            # show all discord channels linked to self.lhs
                )
                results = False
                    if evchan.lower() == ev_channel.lower():
                        dc_info = dc_chan_names.get(dcchan, {"name": dcchan, "guild": "unknown"})
                        table.add_row(i, evchan, f"#{dc_info['name']}@{dc_info['guild']}")
                        results = True
                if results:
                    self.msg(table)
                else:
                    self.msg(f"There are no Discord channels connected to {ev_channel}.")
            else:
            return

        # check if link already exists
        if channel_list := discord_bot.db.channels:
            if (ev_channel, dc_channel) in channel_list:
                self.msg("Those channels are already linked.")
                return
        else:
            discord_bot.db.channels = []
        # create the new link
        channel_obj = search.search_channel(ev_channel)
        if not channel_obj:
            self.msg(f"There is no channel '{ev_channel}'")
            return
        channel_obj = channel_obj[0]
        discord_bot.db.channels.append((channel_obj.name, dc_channel))
        channel_obj.connect(discord_bot)
        if dc_chans := discord_bot.db.discord_channels:
            dc_channel_name = dc_chans.get(dc_channel, {}).get("name", dc_channel)
        else:
            dc_channel_name = dc_channel
        self.msg(f"Discord connection created: {channel_obj.name} <-> #{dc_channel_name}.")

"""
General Character commands usually available to all characters
"""
import re

from evennia.typeclasses.attributes import NickTemplateInvalid
from evennia.utils import utils

__all__ = (
    "CmdHome",
    "CmdLook",
    "CmdNick",
    "CmdInventory",
    "CmdSetDesc",
    "CmdGet",
    "CmdDrop",
    "CmdGive",
    "CmdSay",
    "CmdWhisper",
    "CmdPose",
    "CmdAccess",
)

class CmdHome(COMMAND_DEFAULT_CLASS):
    """
    move to your character's home location

    Usage:
      home

    Teleports you to your home location.
    """

    key = "home"
    locks = "cmd:perm(home) or perm(Builder)"
    arg_regex = r"$"

        home = caller.home
        if not home:
            caller.msg("You have no home!")
        elif home == caller.location:
            caller.msg("You are already home!")
        else:
            caller.msg("There's no place like home ...")
            caller.move_to(home, move_type="teleport")

class CmdLook(COMMAND_DEFAULT_CLASS):
    """
    look at location or object

    Usage:
      look
      look <obj>
      look *<account>

    Observes your location or objects in your vicinity.
    """

    arg_regex = r"\s|$"

        """
        Handle the looking.
        """
            target = caller.location
                caller.msg("You have no location to look at!")
                return
        else:
            target = caller.search(self.args)
                return
        desc = caller.at_look(target)
        # add the type=look to the outputfunc to make it
        # easy to separate this output in client.
        self.msg(text=(desc, {"type": "look"}), options=None)

class CmdNick(COMMAND_DEFAULT_CLASS):
    """
    define a personal alias/nick by defining a string to
    match and replace it with another on the fly

    Usage:
      nick[/switches] <string> [= [replacement_string]]
      nick[/switches] <template> = <replacement_template>
      nick/delete <string> or number
      nicks

    Switches:
      inputline - replace on the inputline (default)
      object    - replace on object-lookup
      account   - replace on account-lookup
      list      - show all defined aliases (also "nicks" works)
      delete    - remove nick by index in /list
      clearall  - clear all nicks

    Examples:
      nick hi = say Hello, I'm Sarah!
      nick/object tom = the tall man
      nick build $1 $2 = create/drop $1;$2
      nick tell $1 $2=page $1=$2
      nick tm?$1=page tallman=$1
      nick tm\=$1=page tallman=$1

    A 'nick' is a personal string replacement. Use $1, $2, ... to catch arguments.
    Put the last $-marker without an ending space to catch all remaining text. You
    can also use unix-glob matching for the left-hand side <string>:

        * - matches everything
        ? - matches 0 or 1 single characters
        [abcd] - matches these chars in any order
        [!abcd] - matches everything not among these chars
        \= - escape literal '=' you want in your <string>

    Note that no objects are actually renamed or changed by this command - your nicks
    are only available to you. If you want to permanently add keywords to an object
    for everyone to use, you need build privileges and the alias command.

    """

    key = "nick"
    switch_options = ("inputline", "object", "account", "list", "delete", "clearall")
    aliases = ["nickname", "nicks"]

        """
        Support escaping of = with \=
        """
        args = (self.lhs or "") + (" = %s" % self.rhs if self.rhs else "")
        parts = re.split(r"(?<!\\)=", args, 1)
        self.rhs = None
        if len(parts) < 2:
            self.lhs = parts[0].strip()
        else:
            self.lhs, self.rhs = [part.strip() for part in parts]
        self.lhs = self.lhs.replace("\=", "=")

        """Create the nickname"""

        def _cy(string):
            "add color to the special markers"
            return re.sub(r"(\$[0-9]+|\*|\?|\[.+?\])", r"|Y\1|n", string)

        nicktypes = [switch for switch in switches if switch in ("object", "account", "inputline")]
        specified_nicktype = bool(nicktypes)
        nicktypes = nicktypes if specified_nicktype else ["inputline"]

        nicklist = (
            utils.make_iter(caller.nicks.get(category="inputline", return_obj=True) or [])
            + utils.make_iter(caller.nicks.get(category="object", return_obj=True) or [])
            + utils.make_iter(caller.nicks.get(category="account", return_obj=True) or [])
        )

        if "list" in switches or self.cmdstring in ("nicks",):
            if not nicklist:
                string = "|wNo nicks defined.|n"
            else:
                table = self.styled_table("#", "Type", "Nick match", "Replacement")
                for inum, nickobj in enumerate(nicklist):
                    _, _, nickvalue, replacement = nickobj.value
                        str(inum + 1), nickobj.db_category, _cy(nickvalue), _cy(replacement)
                    )
                string = "|wDefined Nicks:|n\n%s" % table
            return

        if "clearall" in switches:
            caller.nicks.clear()
            caller.account.nicks.clear()
            caller.msg("Cleared all nicks.")
            return

        if "delete" in switches or "del" in switches:
            if not self.args or not self.lhs:
                caller.msg("usage nick/delete <nick> or <#num> ('nicks' for list)")
                return
            # see if a number was given
            arg = self.args.lstrip("#")
            oldnicks = []
            if arg.isdigit():
                # we are given a index in nicklist
                delindex = int(arg)
                if 0 < delindex <= len(nicklist):
                    oldnicks.append(nicklist[delindex - 1])
                else:
                    caller.msg("Not a valid nick index. See 'nicks' for a list.")
                    return
            else:
                if not specified_nicktype:
                    nicktypes = ("object", "account", "inputline")
                for nicktype in nicktypes:
                    oldnicks.append(caller.nicks.get(arg, category=nicktype, return_obj=True))

            oldnicks = [oldnick for oldnick in oldnicks if oldnick]
            if oldnicks:
                for oldnick in oldnicks:
                    nicktype = oldnick.category
                    nicktypestr = "%s-nick" % nicktype.capitalize()
                    _, _, old_nickstring, old_replstring = oldnick.value
                    caller.nicks.remove(old_nickstring, category=nicktype)
                        f"{nicktypestr} removed: '|w{old_nickstring}|n' -> |w{old_replstring}|n."
                    )
            else:
                caller.msg("No matching nicks to remove.")
            return

        if not self.rhs and self.lhs:
            # check what a nick is set to
            strings = []
            if not specified_nicktype:
                nicktypes = ("object", "account", "inputline")
            for nicktype in nicktypes:
                nicks = [
                    nick
                    for nick in utils.make_iter(
                        caller.nicks.get(category=nicktype, return_obj=True)
                    )
                    if nick
                ]
                for nick in nicks:
                    _, _, nick, repl = nick.value
                    if nick.startswith(self.lhs):
                        strings.append(f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'")
            if strings:
                caller.msg("\n".join(strings))
            else:
                caller.msg(f"No nicks found matching '{self.lhs}'")
            return

                if nicktype == "account":
                    obj = account
                else:
                    obj = caller
                nicks = utils.make_iter(obj.nicks.get(category=nicktype, return_obj=True))
            else:
            return

                else:
            else:
            return

            caller.msg("Usage: nick[/switches] nickname = [realname]")
            return

        # setting new nicks

        nickstring = self.lhs
        replstring = self.rhs

        if replstring == nickstring:
            caller.msg("No point in setting nick same as the string to replace...")
            return

        # check so we have a suitable nick type
        errstring = ""
        string = ""
        for nicktype in nicktypes:
            nicktypestr = f"{nicktype.capitalize()}-nick"
            old_nickstring = None
            old_replstring = None

            oldnick = caller.nicks.get(key=nickstring, category=nicktype, return_obj=True)
            if oldnick:
                _, _, old_nickstring, old_replstring = oldnick.value
            if replstring:
                # creating new nick
                errstring = ""
                if oldnick:
                    if replstring == old_replstring:
                        string += f"\nIdentical {nicktypestr.lower()} already set."
                    else:
                        string += (
                            f"\n{nicktypestr} '|w{old_nickstring}|n' updated to map to"
                            f" '|w{replstring}|n'."
                        )
                else:
                    string += f"\n{nicktypestr} '|w{nickstring}|n' mapped to '|w{replstring}|n'."
                try:
                    caller.nicks.add(nickstring, replstring, category=nicktype)
                except NickTemplateInvalid:
                        "You must use the same $-markers both in the nick and in the replacement."
                    )
                    return
            elif old_nickstring and old_replstring:
                # just looking at the nick
                string += f"\n{nicktypestr} '|w{old_nickstring}|n' maps to '|w{old_replstring}|n'."
        string = errstring if errstring else string
        caller.msg(_cy(string))

class CmdInventory(COMMAND_DEFAULT_CLASS):
    """
    view inventory

    Usage:
      inventory
      inv

    Shows your inventory.
    """

    key = "inventory"
    aliases = ["inv", "i"]

        """check inventory"""
        items = self.caller.contents
        if not items:
            string = "You are not carrying anything."
        else:
            from evennia.utils.ansi import raw as raw_ansi

            table = self.styled_table(border="header")
            for item in items:
                singular, _ = item.get_numbered_name(1, self.caller)
                    f"|C{singular}|n",
                    "{}|n".format(utils.crop(raw_ansi(item.db.desc or ""), width=50) or ""),
                )
            string = f"|wYou are carrying:\n{table}"
        self.msg(text=(string, {"type": "inventory"}))

class CmdGet(COMMAND_DEFAULT_CLASS):
    """
    pick up something

    Usage:
      get <obj>

    Picks up an object from your location and puts it in
    your inventory.
    """

    key = "get"
    aliases = "grab"
    locks = "cmd:all();view:perm(Developer);read:perm(Developer)"

        """implements the command."""

            caller.msg("Get what?")
            return
        obj = caller.search(self.args, location=caller.location)
        if not obj:
            return
        if caller == obj:
            caller.msg("You can't get yourself.")
            return
        if not obj.access(caller, "get"):
            if obj.db.get_err_msg:
                caller.msg(obj.db.get_err_msg)
            else:
                caller.msg("You can't get that.")
            return

        # calling at_pre_get hook method
        if not obj.at_pre_get(caller):
            return

        success = obj.move_to(caller, quiet=True, move_type="get")
        if not success:
            caller.msg("This can't be picked up.")
        else:
            singular, _ = obj.get_numbered_name(1, caller)
            caller.location.msg_contents(f"$You() $conj(pick) up {singular}.", from_obj=caller)
            # calling at_get hook method
            obj.at_get(caller)

class CmdDrop(COMMAND_DEFAULT_CLASS):
    """
    drop something

    Usage:
      drop <obj>

    Lets you drop an object from your inventory into the
    location you are currently in.
    """

    key = "drop"

        """Implement command"""

            caller.msg("Drop what?")
            return

        # Because the DROP command by definition looks for items
        # in inventory, call the search function using location = caller
        obj = caller.search(
            self.args,
            location=caller,
            nofound_string=f"You aren't carrying {self.args}.",
            multimatch_string=f"You carry more than one {self.args}:",
        )
        if not obj:
            return

        # Call the object script's at_pre_drop() method.
        if not obj.at_pre_drop(caller):
            return

        success = obj.move_to(caller.location, quiet=True, move_type="drop")
            caller.msg("This couldn't be dropped.")
        else:
            caller.location.msg_contents(f"$You() $conj(drop) {singular}.", from_obj=caller)
            # Call the object script's at_drop() method.
            obj.at_drop(caller)

class CmdGive(COMMAND_DEFAULT_CLASS):
    """
    give away something to someone

    Usage:
      give <inventory obj> <to||=> <target>

    Gives an item from your inventory to another person,
    placing it in their inventory.
    """

    key = "give"

        """Implement give"""

            caller.msg("Usage: give <inventory object> = <target>")
            return
        to_give = caller.search(
            self.lhs,
            nofound_string=f"You aren't carrying {self.lhs}.",
            multimatch_string=f"You carry more than one {self.lhs}:",
        )
        target = caller.search(self.rhs)
        if not (to_give and target):
            return

        singular, _ = to_give.get_numbered_name(1, caller)
        if target == caller:
            caller.msg(f"You keep {singular} to yourself.")
            return
        if not to_give.location == caller:
            caller.msg(f"You are not holding {singular}.")
            return

        # calling at_pre_give hook method
        if not to_give.at_pre_give(caller, target):
            return

        # give object
        success = to_give.move_to(target, quiet=True, move_type="give")
            caller.msg(f"You could not give {singular} to {target.key}.")
        else:
            caller.msg(f"You give {singular} to {target.key}.")
            target.msg(f"{caller.key} gives you {singular}.")
            # Call the object script's at_give() method.
            to_give.at_give(caller, target)

class CmdSetDesc(COMMAND_DEFAULT_CLASS):
    """
    describe yourself

    Usage:
      setdesc <description>

    Add a description to yourself. This
    will be visible to people when they
    look at you.
    """

    key = "setdesc"

        """add the description"""

            self.msg("You must add a description.")
            return

        self.caller.db.desc = self.args.strip()
        self.msg("You set your description.")

class CmdSay(COMMAND_DEFAULT_CLASS):
    """
    speak as your character

    Usage:
      say <message>

    Talk to those in your current location.
    """

    key = "say"
    aliases = ['"', "'"]

    # don't require a space after `say/'/"`
    arg_regex = None

        """Run the say command"""

            caller.msg("Say what?")
            return

        speech = self.args

        # Calling the at_pre_say hook on the character
        speech = caller.at_pre_say(speech)

        # If speech is empty, stop here
        if not speech:
            return

        # Call the at_post_say hook on the character
        caller.at_say(speech, msg_self=True)

class CmdWhisper(COMMAND_DEFAULT_CLASS):
    """
    Speak privately as your character to another

    Usage:
      whisper <character> = <message>
      whisper <char1>, <char2> = <message>

    Talk privately to one or more characters in your current location, without
    others in the room being informed.
    """

    key = "whisper"

        """Run the whisper command"""

            caller.msg("Usage: whisper <character> = <message>")
            return

        receivers = [recv.strip() for recv in self.lhs.split(",")]

        receivers = [caller.search(receiver) for receiver in set(receivers)]
        receivers = [recv for recv in receivers if recv]

        speech = self.rhs
        # If the speech is empty, abort the command
        if not speech or not receivers:
            return

        # Call a hook to change the speech before whispering
        speech = caller.at_pre_say(speech, whisper=True, receivers=receivers)

        # no need for self-message if we are whispering to ourselves (for some reason)
        msg_self = None if caller in receivers else True
        caller.at_say(speech, msg_self=msg_self, receivers=receivers, whisper=True)

class CmdPose(COMMAND_DEFAULT_CLASS):
    """
    strike a pose

    Usage:
      pose <pose text>
      pose's <pose text>

    Example:
      pose is standing by the wall, smiling.
       -> others will see:
      Tom is standing by the wall, smiling.

    Describe an action being taken. The pose text will
    automatically begin with your name.
    """

    key = "pose"
    aliases = [":", "emote"]
    arg_regex = ""

    # we want to be able to pose without whitespace between
    # the command/alias and the pose (e.g. :pose)

        """
        Custom parse the cases where the emote
        starts with some special letter, such
        as 's, at which we don't want to separate
        the caller's name and the emote with a
        space.
        """
        if args and not args[0] in ["'", ",", ":"]:
            args = " %s" % args.strip()

        """Hook function"""
            msg = "What do you want to do?"
            self.msg(msg)
        else:
            msg = f"{self.caller.name}{self.args}"
            self.caller.location.msg_contents(text=(msg, {"type": "pose"}), from_obj=self.caller)

class CmdAccess(COMMAND_DEFAULT_CLASS):
    """
    show your current game access

    Usage:
      access

    This command shows you the permission hierarchy and
    which permission groups you are a member of.
    """

    key = "access"
    aliases = ["groups", "hierarchy"]

        """Load the permission groups"""

        hierarchy_full = settings.PERMISSION_HIERARCHY
        string = "\n|wPermission Hierarchy|n (climbing):\n %s" % ", ".join(hierarchy_full)

        if self.caller.account.is_superuser:
            cperms = "<Superuser>"
            pperms = "<Superuser>"
        else:
            cperms = ", ".join(caller.permissions.all())
            pperms = ", ".join(caller.account.permissions.all())

        string += "\n|wYour access|n:"
        string += f"\nCharacter |c{caller.key}|n: {cperms}"
        if utils.inherits_from(caller, evennia.DefaultObject):
            string += f"\nAccount |c{caller.account.key}|n: {pperms}"

"""
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``.

"""

from dataclasses import dataclass

from evennia.help.filehelp import FILE_HELP_ENTRIES
from evennia.help.models import HelpEntry
from evennia.help.utils import help_search_with_index, parse_entry_for_subcategories
from evennia.utils import create, evmore
    dedent,
    pad,
)

HELP_MORE_ENABLED = settings.HELP_MORE_ENABLED
DEFAULT_HELP_CATEGORY = settings.DEFAULT_HELP_CATEGORY
HELP_CLICKABLE_TOPICS = settings.HELP_CLICKABLE_TOPICS

__all__ = ("CmdHelp", "CmdSetHelp")

@dataclass
class HelpCategory:
    """
    Mock 'help entry' to search categories with the same code.

    """

    key: str

    @property
    def search_index_entry(self):
        return {
            "key": self.key,
            "aliases": "",
            "category": self.key,
            "no_prefix": "",
            "tags": "",
            "text": "",
        }

        return hash(id(self))

class CmdHelp(COMMAND_DEFAULT_CLASS):
    """
    Get help.

    Usage:
      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. Some big topics may offer additional sub-topics.

    """

    key = "help"
    aliases = ["?"]

    # this is a special cmdhandler flag that makes the cmdhandler also pack
    # the current cmdset with the call to self.func().
    return_cmdset = True

    # 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_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

    # number of suggestions (set to 0 to remove suggestions from help)
    suggestion_maxnum = 5

    # separator between subtopics:
    subtopic_separator_char = r"/"

    # should topics disply their help entry when clicked
    clickable_topics = HELP_CLICKABLE_TOPICS

    def msg_help(self, text, **kwargs):
        """
        messages text to the caller, adding an extra oob argument to indicate
        that this is a help command result and could be rendered in a separate
        help window.

        """
        if type(self).help_more:
            usemore = True

            if self.session and self.session.protocol_key in (
                "webclient/websocket",
                "webclient/ajax",
            ):
                try:
                    options = self.account.db._saved_webclient_options
                    if options and options["helppopup"]:
                        usemore = False
                except KeyError:
                    pass

            if usemore:
                # adding the 'text_kwargs' keyword means it will be sent with the text outputfunc
                # for every page.
                evmore.msg(
                    self.caller, text, session=self.session, text_kwargs={"type": "help"}, **kwargs
                )
                return

        self.msg(text=(text, {"type": "help"}))

    def format_help_entry(
        self,
        topic="",
        help_text="",
        aliases=None,
        suggested=None,
        subtopics=None,
        click_topics=True,
    ):
        """This visually formats the help entry.
        This method can be overridden to customize the way a help
        entry is displayed.

        Args:
            title (str, optional): The title of the help entry.
            help_text (str, optional): Text of the help entry.
            aliases (list, optional): List of help-aliases (displayed in header).
            suggested (list, optional): Strings suggested reading (based on title).
            subtopics (list, optional): A list of strings - the subcategories available
                for this entry.
            click_topics (bool, optional): Should help topics be clickable. Default is True.

        Returns:
            help_message (str): Help entry formated for console.

        """
        separator = "|C" + "-" * self.client_width() + "|n"
        start = f"{separator}\n"

        title = f"|CHelp for |w{topic}|n" if topic else "|rNo help found|n"

            aliases = " |C(aliases: {}|C)|n".format("|C,|n ".join(f"|w{ali}|n" for ali in aliases))
        else:
            aliases = ""

        help_text = "\n" + dedent(help_text.strip("\n")) if help_text else ""

        if subtopics:
            if click_topics:
                subtopics = [
                    f"|lchelp {topic}/{subtop}|lt|w{topic}/{subtop}|n|le" for subtop in subtopics
                ]
            else:
                subtopics = [f"|w{topic}/{subtop}|n" for subtop in subtopics]
            subtopics = "\n|CSubtopics:|n\n  {}".format(
                "\n  ".join(
                    format_grid(
                        subtopics, width=self.client_width(), line_prefix=self.index_topic_clr
                    )
                )
            )
        else:
            subtopics = ""

        if suggested:
            suggested = sorted(suggested)
                suggested = [f"|lchelp {sug}|lt|w{sug}|n|le" for sug in suggested]
            else:
                suggested = [f"|w{sug}|n" for sug in suggested]
            suggested = "\n|COther topic suggestions:|n\n{}".format(
                        suggested, width=self.client_width(), line_prefix=self.index_topic_clr
                    )
                )
            )
        else:
            suggested = ""

        end = start

        partorder = (start, title + aliases, help_text, subtopics, suggested, end)

        return "\n".join(part.rstrip() for part in partorder if part)

    def format_help_index(
        self, cmd_help_dict=None, db_help_dict=None, title_lone_category=False, click_topics=True
    ):
        """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.
            click_topics (bool, optional): If help-topics are clickable or not
                (for webclient or telnet clients with MXP support).
        Returns:
            str: The help index organized into a grid.

        Notes:
            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.

        """

        def _group_by_category(help_dict):
            grid = []
            verbatim_elements = []

            if len(help_dict) == 1 and not title_lone_category:
                # don't list categories if there is only one
                for category in help_dict:
                    # gather and sort the entries from the help dictionary
                    entries = sorted(set(help_dict.get(category, [])))

                    # make the help topics clickable
                    if click_topics:
                        entries = [f"|lchelp {entry}|lt{entry}|le" for entry in entries]

                    # add the entries to the grid
                    grid.extend(entries)
            else:
                # list the categories
                for category in sorted(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)

            return grid, verbatim_elements

        help_index = ""
        width = self.client_width()
        grid = []
        cmd_grid, db_grid = "", ""

        if any(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,
                line_prefix=self.index_topic_clr,
            )
            cmd_grid = ANSIString("\n").join(gridrows) if gridrows else ""

        if any(db_help_dict.values()):
            # get db-based help entries by-category
            sep2 = (
                + pad("Game & World", width=width, fillchar="-")
            )
            grid, verbatim_elements = _group_by_category(db_help_dict)
                grid,
                width,
                sep="  ",
            )
            db_grid = ANSIString("\n").join(gridrows) if gridrows else ""

        # only show the main separators if there are actually both cmd and db-based help
        if cmd_grid and db_grid:
            help_index = f"{sep1}\n{cmd_grid}\n{sep2}\n{db_grid}"
        else:
            help_index = f"{cmd_grid}{db_grid}"

        return help_index

    def can_read_topic(self, cmd_or_topic, caller):
        """
        Helper method. If this return True, the given help topic
        be viewable in the help listing. Note that even if this returns False,
        the entry will still be visible in the help index unless `should_list_topic`
        is also returning False.

        Args:
            cmd_or_topic (Command, HelpEntry or FileHelpEntry): The topic/command to test.
            caller: the caller checking for access.

        Returns:
            bool: If command can be viewed or not.

        Notes:
            This uses the 'read' lock. If no 'read' lock is defined, the topic is assumed readable
            by all.

        """
        if inherits_from(cmd_or_topic, "evennia.commands.command.Command"):
            return cmd_or_topic.auto_help and cmd_or_topic.access(caller, "read", default=True)
        else:
            return cmd_or_topic.access(caller, "read", default=True)

    def can_list_topic(self, cmd_or_topic, caller):
        """
        Should the specified command appear in the help table?

        This method only checks whether a specified command should appear in the table of
        topics/commands.  The command can be used by the caller (see the 'should_show_help' method)
        and the command will still be available, for instance, if a character type 'help name of the
        command'.  However, if you return False, the specified command will not appear in the table.
        This is sometimes useful to "hide" commands in the table, but still access them through the
        help system.

        Args:

        Returns:
            bool: If command should be listed or not.

        Notes:
            The `.auto_help` propery is checked for commands. For all help entries,
            the 'view' lock will be checked, and if no such lock is defined, the 'read'
            lock will be used. If neither lock is defined, the help entry is assumed to be
            accessible to all.

        """
        if hasattr(cmd_or_topic, "auto_help") and not cmd_or_topic.auto_help:

        has_view = (
            "view:" in cmd_or_topic.locks
            if inherits_from(cmd_or_topic, "evennia.commands.command.Command")
            else cmd_or_topic.locks.get("view")
        )

        if has_view:
            return cmd_or_topic.access(caller, "view", default=True)
        else:
            # no explicit 'view' lock - use the 'read' lock

    def collect_topics(self, caller, mode="list"):
        """
        Collect help topics from all sources (cmd/db/file).

        Args:
            caller (Object or Account): The user of the Command.
            mode (str): One of 'list' or 'query', where the first means we are collecting to view
                the help index and the second because of wanting to search for a specific help
                entry/cmd to read. This determines which access should be checked.

        Returns:
            tuple: A tuple of three dicts containing the different types of help entries
            in the order cmd-help, db-help, file-help:
                `({key: cmd,...}, {key: dbentry,...}, {key: fileentry,...}`

        """
        # start with cmd-help
        cmdset = 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 / file-help topics.
        # also check the 'cmd:' lock here
        cmd_help_topics = [cmd for cmd in cmdset if cmd and cmd.access(caller, "cmd")]
        # get all file-based help entries, checking perms
        file_help_topics = {topic.key.lower().strip(): topic for topic in FILE_HELP_ENTRIES.all()}
        # get db-based help entries, checking perms
        db_help_topics = {topic.key.lower().strip(): topic for topic in HelpEntry.objects.all()}
        if mode == "list":
            # check the view lock for all help entries/commands and determine key
            cmd_help_topics = {
                cmd.auto_help_display_key if hasattr(cmd, "auto_help_display_key") else cmd.key: cmd
                for cmd in cmd_help_topics
                if self.can_list_topic(cmd, caller)
            }
            db_help_topics = {
                key: entry
                for key, entry in db_help_topics.items()
                if self.can_list_topic(entry, caller)
            }
            file_help_topics = {
                key: entry
                for key, entry in file_help_topics.items()
            }
        else:
            # query - check the read lock on entries
                if self.can_read_topic(cmd, caller)
            }
                key: entry
                if self.can_read_topic(entry, caller)
            }
                key: entry
            }

        return cmd_help_topics, db_help_topics, file_help_topics

    def do_search(self, query, entries, search_fields=None):
        """
        Perform a help-query search, default using Lunr search engine.

        Args:
            query (str): The help entry to search for.
            entries (list): All possibilities. A mix of commands, HelpEntries and FileHelpEntries.
            search_fields (list): A list of dicts defining how Lunr will find the
                search data on the elements. If not given, will use a default.

        Returns:
            tuple: A tuple (match, suggestions).

        """
        if not search_fields:
            # lunr search fields/boosts
            search_fields = [
                {"field_name": "key", "boost": 10},
                {"field_name": "aliases", "boost": 9},
                {"field_name": "no_prefix", "boost": 8},
                {"field_name": "category", "boost": 7},
                {"field_name": "tags", "boost": 1},  # tags are not used by default
            ]
        match, suggestions = None, None
        for match_query in (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, fields=search_fields
            )
                match = matches[0]
                break
        return match, suggestions

        """
        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.

        """
        # parse the query

            self.subtopics = [
                part.strip().lower() for part in self.args.split(self.subtopic_separator_char)
            ]
            self.topic = self.subtopics.pop(0)
        else:
            self.topic = ""
            self.subtopics = []

    def strip_cmd_prefix(self, key, all_keys):
        """
        Conditional strip of a command prefix, such as @ in @desc. By default
        this will be hidden unless there is a duplicate without the prefix
        in the full command set (such as @open and open).

        Args:
            key (str): Command key to analyze.
            all_cmds (list): All command-keys (and potentially aliases).

        Returns:
            str: Potentially modified key to use in help display.

        """
        if key and key[0] in CMD_IGNORE_PREFIXES and key[1:] not in all_keys:
            # filter out e.g. `@` prefixes from display if there is duplicate
            # with the prefix in the set (such as @open/open)
            return key[1:]
        return key

        """
        Run the dynamic help entry creator.
        """
        query, subtopics, cmdset = self.topic, self.subtopics, self.cmdset
        clickable_topics = self.clickable_topics

        if not query:
            # list all available help entries, grouped by category. We want to
            # build dictionaries {category: [topic, topic, ...], ...}

            cmd_help_topics, db_help_topics, file_help_topics = self.collect_topics(
                caller, mode="list"
            )

            # db-topics override file-based ones
            file_db_help_topics = {**file_help_topics, **db_help_topics}

            # group by category (cmds are listed separately)
            cmd_help_by_category = defaultdict(list)
            file_db_help_by_category = defaultdict(list)

            # get a collection of all keys + aliases to be able to strip prefixes like @
            key_and_aliases = set(chain(*(cmd._keyaliases for cmd in cmd_help_topics.values())))

            for key, cmd in cmd_help_topics.items():
                key = self.strip_cmd_prefix(key, key_and_aliases)
                cmd_help_by_category[cmd.help_category].append(key)
            for key, entry in file_db_help_topics.items():
                file_db_help_by_category[entry.help_category].append(key)

            # generate the index and display
            output = self.format_help_index(
                cmd_help_by_category, file_db_help_by_category, click_topics=clickable_topics
            )
            self.msg_help(output)

            return

        # search for a specific entry. We need to check for 'read' access here before
        # building the set of possibilities.
        cmd_help_topics, db_help_topics, file_help_topics = self.collect_topics(
            caller, mode="query"
        )

        # get a collection of all keys + aliases to be able to strip prefixes like @
        key_and_aliases = set(chain(*(cmd._keyaliases for cmd in cmd_help_topics.values())))

        # db-help topics takes priority over file-help
        file_db_help_topics = {**file_help_topics, **db_help_topics}

        # commands take priority over the other types
        all_topics = {**file_db_help_topics, **cmd_help_topics}

        # get all categories
        all_categories = list(
            set(HelpCategory(topic.help_category) for topic in all_topics.values())
        )

        # all available help options - will be searched in order. We also check # the
        # read-permission here.
        entries = list(all_topics.values()) + all_categories

        # lunr search fields/boosts
        match, suggestions = self.do_search(query, entries)

            # no topic matches found. Only give suggestions.
            help_text = f"There is no help topic matching '{query}'."

            if not suggestions:
                # 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},
                ]

                for match_query in [query, f"{query}*", f"*{query}"]:
                    _, suggestions = help_search_with_index(
                        match_query,
                        entries,
                        suggestion_maxnum=self.suggestion_maxnum,
                        fields=search_fields,
                    )
                    if suggestions:
                        help_text += (
                            "\n... But matches were found within the help "
                            "texts of the suggestions below."
                        )
                        suggestions = [
                            self.strip_cmd_prefix(sugg, key_and_aliases) for sugg in suggestions
                        ]
                        break

            output = self.format_help_entry(
                topic=None,  # this will give a no-match style title
                help_text=help_text,
                suggested=suggestions,
                click_topics=clickable_topics,
            )

            return

        if isinstance(match, HelpCategory):
            # no subtopics for categories - these are just lists of topics
            category = match.key
            category_lower = category.lower()
            cmds_in_category = [
                key for key, cmd in cmd_help_topics.items() if category_lower == cmd.help_category
            ]
            topics_in_category = [
                key
                for key, topic in file_db_help_topics.items()
                if category_lower == topic.help_category
            ]
                {category: cmds_in_category},
                {category: topics_in_category},
                title_lone_category=True,
            )
            return

        if inherits_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
            help_text = match.entrytext
            aliases = match.aliases if isinstance(match.aliases, list) else match.aliases.all()

        # 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 = [subtopic for subtopic in subtopic_map if subtopic is not None]

            # 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.

            for subtopic_query in subtopics:
                if subtopic_query not in subtopic_map:
                    # exact match failed. Try startswith-match
                    fuzzy_match = False
                    for key in subtopic_map:
                        if key and key.startswith(subtopic_query):
                            subtopic_query = key
                            fuzzy_match = True
                            break

                    if not fuzzy_match:
                        # startswith failed - try an 'in' match
                        for key in subtopic_map:
                            if key and subtopic_query in key:
                                subtopic_query = key
                                fuzzy_match = True
                                break

                        # 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,
                            click_topics=clickable_topics,
                        )
                        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 = [subtopic for subtopic in subtopic_map if subtopic is not None]
                # 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]

        topic = self.strip_cmd_prefix(topic, key_and_aliases)
        else:
            aliases = [self.strip_cmd_prefix(alias, key_and_aliases) for alias in aliases]
        suggested = [self.strip_cmd_prefix(sugg, key_and_aliases) for sugg in suggested]

        output = self.format_help_entry(
            topic=topic,
            help_text=help_text,
            aliases=aliases,
            subtopics=subtopic_index,
            suggested=suggested,
            click_topics=clickable_topics,
        )

        self.msg_help(output)

def _loadhelp(caller):
    entry = caller.db._editing_help
    if entry:
        return entry.entrytext
    else:
        return ""

def _savehelp(caller, buffer):
    caller.msg("Saved help entry.")
    if entry:
        entry.entrytext = buffer

def _quithelp(caller):
    caller.msg("Closing the editor.")
    del caller.db._editing_help

class CmdSetHelp(CmdHelp):
    """
    Edit the help database.

    Usage:
      sethelp[/switches] <topic>[[;alias;alias][,category[,locks]] [= <text>]

    Switches:
      edit - open a line editor to edit the topic's help text.
      replace - overwrite existing help topic.
      append - add text to the end of existing topic with a newline between.
      extend - as append, but don't add a newline.
      delete - remove help topic.

    Examples:
      sethelp lore = In the beginning was ...
      sethelp/append pickpocketing,Thievery = This steals ...
      sethelp/replace pickpocketing, ,attr(is_thief) = This steals ...
      sethelp/edit thievery

    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.

    | 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

    """

    key = "sethelp"
    switch_options = ("edit", "replace", "append", "extend", "delete")
    locks = "cmd:perm(Helper)"

        """We want to use the default parser rather than the CmdHelp.parse"""
        return COMMAND_DEFAULT_CLASS.parse(self)

        """Implement the function"""

        lhslist = self.lhslist

            self.msg(
                "Usage: sethelp[/switches] <topic>[;alias;alias][,category[,locks,..] = <text>"
            )
            return

        nlist = len(lhslist)
        topicstr = lhslist[0] if nlist > 0 else ""
        if not topicstr:
            self.msg("You have to define a topic!")
            return
        topicstrlist = topicstr.split(";")
        topicstr, aliases = (
            topicstrlist[0],
            topicstrlist[1:] if len(topicstr) > 1 else [],
        )
        aliastxt = ("(aliases: %s)" % ", ".join(aliases)) if aliases else ""
        old_entry = None

        # check if we have an old entry with the same name

            self.caller, mode="query"
        )
        )

        # default setup
        category = lhslist[1] if nlist > 1 else DEFAULT_HELP_CATEGORY
        lockstring = ",".join(lhslist[2:]) if nlist > 2 else "read:all()"

        # search for existing entries of this or other types
        for querystr in topicstrlist:
            match, _ = self.do_search(querystr, entries)
            if match:
                warning = None
                if isinstance(match, HelpCategory):
                    warning = (
                        f"'{querystr}' matches (or partially matches) the name of "
                        f"help-category '{match.key}'. If you continue, your help entry will "
                        "take precedence and the category (or part of its name) *may* not "
                        "be usable for grouping help entries anymore."
                    )
                elif inherits_from(match, "evennia.commands.command.Command"):
                    warning = (
                        f"'{querystr}' matches (or partially matches) the key/alias of "
                        f"Command '{match.key}'. Command-help take precedence over other "
                        "help entries so your help *may* be impossible to reach for those "
                        "with access to that command."
                    )
                elif inherits_from(match, "evennia.help.filehelp.FileHelpEntry"):
                    warning = (
                        f"'{querystr}' matches (or partially matches) the name/alias of the "
                        f"file-based help topic '{match.key}'. File-help entries cannot be "
                        "modified from in-game (they are files on-disk). If you continue, "
                        "your help entry may shadow the file-based one's name partly or "
                        "completely."
                    )
                if warning:
                    # show a warning for a clashing help-entry type. Even if user accepts this
                    # we don't break here since we may need to show warnings for other inputs.
                    # We don't count this as an old-entry hit because we can't edit these
                    # types of entries.
                    self.msg(f"|rWarning:\n|r{warning}|n")
                    repl = yield ("|wDo you still want to continue? Y/[N]?|n")
                    if repl.lower() not in ("y", "yes"):
                        self.msg("Aborted.")
                        return
                else:
                    # a db-based help entry - this is OK
                    old_entry = match
                    category = lhslist[1] if nlist > 1 else old_entry.help_category
                    lockstring = ",".join(lhslist[2:]) if nlist > 2 else old_entry.locks.get()
                    break

        category = category.lower()

        if "edit" in switches:
            # open the line editor to edit the helptext. No = is needed.
            if old_entry:
                topicstr = old_entry.key
                    # we assume append here.
                    old_entry.entrytext += "\n%s" % self.rhs
                helpentry = old_entry
            else:
                helpentry = create.create_help_entry(
                    topicstr,
                    self.rhs,
                    category=category,
                    locks=lockstring,
                    aliases=aliases,
                )
            self.caller.db._editing_help = helpentry

            EvEditor(
                self.caller,
                loadfunc=_loadhelp,
                savefunc=_savehelp,
                quitfunc=_quithelp,
                key="topic {}".format(topicstr),
                persistent=True,
            )
            return

        if "append" in switches or "merge" in switches or "extend" in switches:
            # merge/append operations
            if not old_entry:
                self.msg(f"Could not find topic '{topicstr}'. You must give an exact name.")
                return
                self.msg("You must supply text to append/merge.")
                return
            if "merge" in switches:
                old_entry.entrytext += " " + self.rhs
            else:
                old_entry.entrytext += "\n%s" % self.rhs
            old_entry.aliases.add(aliases)
            self.msg(f"Entry updated:\n{old_entry.entrytext}{aliastxt}")
            return

            # delete the help entry
                self.msg(f"Could not find topic '{topicstr}'{aliastxt}.")
                return
            old_entry.delete()
            self.msg(f"Deleted help entry '{topicstr}'{aliastxt}.")
            return

        # at this point it means we want to add a new help entry.
            self.msg("You must supply a help text to add.")
            return
        if old_entry:
            if "replace" in switches:
                # overwrite old entry
                old_entry.key = topicstr
                old_entry.entrytext = self.rhs
                old_entry.help_category = category
                old_entry.locks.clear()
                old_entry.locks.add(lockstring)
                old_entry.aliases.add(aliases)
                old_entry.save()
                self.msg(f"Overwrote the old topic '{topicstr}'{aliastxt}.")
            else:
                self.msg(
                    f"Topic '{topicstr}'{aliastxt} already exists. Use /edit to open in editor, or "
                    "/replace, /append and /merge to modify it directly."
                )
        else:
            # no old entry. Create a new one.
            new_entry = create.create_help_entry(
                topicstr, self.rhs, category=category, locks=lockstring, aliases=aliases
            )
            if new_entry:
                self.msg(f"Topic '{topicstr}'{aliastxt} was successfully created.")
                if "edit" in switches:
                    # open the line editor to edit the helptext
                    self.caller.db._editing_help = new_entry
                    EvEditor(
                        self.caller,
                        loadfunc=_loadhelp,
                        savefunc=_savehelp,
                        quitfunc=_quithelp,
                        key="topic {}".format(new_entry.key),
                        persistent=True,
                    )
                    return
            else:
                self.msg(f"Error when creating topic '{topicstr}'{aliastxt}! Contact an admin.")

"""
The command template for the default MUX-style command set. There
is also an Account/OOC version that makes sure caller is an Account object.
"""

from evennia.commands.command import Command

__all__ = ("MuxCommand", "MuxAccountCommand")

class MuxCommand(Command):
    """
    This sets up the basis for a MUX command. The idea
    is that most other Mux-related commands should just
    inherit from this and don't have to implement much
    parsing of their own unless they do something particularly
    advanced.

    Note that the class's __doc__ string (this text) is
    used by Evennia to create the automatic help entry for
    the command, so make sure to document consistently here.
    """

    def has_perm(self, srcobj):
        """
        This is called by the cmdhandler to determine
        if srcobj is allowed to execute this command.
        We just show it here for completeness - we
        are satisfied using the default check in Command.
        """
        return super().has_perm(srcobj)

        """
        This hook is called before self.parse() on all commands
        """
        pass

        """
        """
        pass

        """
        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.
        """
        raw = self.args
        args = raw.strip()
        # Without explicitly setting these attributes, they assume default values:
        if not hasattr(self, "switch_options"):
            self.switch_options = None
        if not hasattr(self, "rhs_split"):
            self.rhs_split = "="
        if not hasattr(self, "account_caller"):
            self.account_caller = False

        # split out switches
        switches, delimiters = [], self.rhs_split
        if self.switch_options:
            self.switch_options = [opt.lower() for opt in self.switch_options]
        if args and len(args) > 1 and raw[0] == "/":
            # we have a switch, or a set of switches. These end with a space.
            switches = args[1:].split(None, 1)
            if len(switches) > 1:
                switches, args = switches
                switches = switches.split("/")
            else:
                args = ""
                switches = switches[0].split("/")
            # If user-provides switches, parse them with parser switch options.
            if switches and self.switch_options:
                valid_switches, unused_switches, extra_switches = [], [], []
                for element in switches:
                    option_check = [opt for opt in self.switch_options if opt == element]
                    if not option_check:
                        option_check = [
                            opt for opt in self.switch_options if opt.startswith(element)
                        ]
                    match_count = len(option_check)
                    if match_count > 1:
                        extra_switches.extend(
                            option_check
                        )  # Either the option provided is ambiguous,
                    elif match_count == 1:
                        valid_switches.extend(option_check)  # or it is a valid option abbreviation,
                    elif match_count == 0:
                        unused_switches.append(element)  # or an extraneous option to be ignored.
                if extra_switches:  # User provided switches
                    self.msg(
                        "|g%s|n: |wAmbiguous switch supplied: Did you mean /|C%s|w?"
                        % (self.cmdstring, " |nor /|C".join(extra_switches))
                    )
                if unused_switches:
                    plural = "" if len(unused_switches) == 1 else "es"
                    self.msg(
                        '|g%s|n: |wExtra switch%s "/|C%s|w" ignored.'
                        % (self.cmdstring, plural, "|n, /|C".join(unused_switches))
                    )
                switches = valid_switches  # Only include valid_switches in command function call
        arglist = [arg.strip() for arg in args.split()]

        # check for arg1, arg2, ... = argA, argB, ... constructs
        lhs, rhs = args.strip(), None
        if lhs:
            if delimiters and hasattr(delimiters, "__iter__"):  # If delimiter is iterable,
                best_split = delimiters[0]  # (default to first delimiter)
                for this_split in delimiters:  # try each delimiter
                    if this_split in lhs:  # to find first successful split
                        best_split = this_split  # to be the best split.
                        break
            else:
                best_split = delimiters
            # Parse to separate left into left/right sides using best_split delimiter string
            if best_split in lhs:
                lhs, rhs = lhs.split(best_split, 1)
        # Trim user-injected whitespace
        rhs = rhs.strip() if rhs is not None else None
        lhs = lhs.strip()
        # Further split left/right sides by comma delimiter
        lhslist = [arg.strip() for arg in lhs.split(",")] if lhs is not None else []
        rhslist = [arg.strip() for arg in rhs.split(",")] if rhs is not None else []
        # save to object properties:
        self.raw = raw
        self.switches = switches
        self.args = args.strip()
        self.arglist = arglist
        self.lhs = lhs
        self.lhslist = lhslist
        self.rhs = rhs
        self.rhslist = rhslist

        # if the class has the account_caller property set on itself, we make
        # sure that self.caller is always the account if possible. We also create
        # a special property "character" for the puppeted object, if any. This
        # is convenient for commands defined on the Account only.
        if self.account_caller:
            if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"):
                # caller is an Object/Character
                self.character = self.caller
                self.caller = self.caller.account
            elif utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount"):
                # caller was already an Account
                self.character = self.caller.get_puppet(self.session)
            else:
                self.character = None

        """
        Update of parent class's get_command_info() for MuxCommand.
        """
        )
Command {self} has no defined `func()` - showing on-command variables: No child func() defined for {self} - available variables:
        """
        string = "-" * 50
        string += f"\n|w{self.key}|n - Command variables from evennia:\n"
        string += f"\nname of cmd (self.key): |w{self.key}|n\n"
        string += f"cmd aliases (self.aliases): |w{self.aliases}|n\n"
        string += f"cmd locks (self.locks): |w{self.locks}|n\n"
        string += f"help category (self.help_category): |w{self.help_category}|n\n"
        string += f"object calling (self.caller): |w{self.caller}|n\n"
        string += f"object storing cmdset (self.obj): |w{self.obj}|n\n"
        string += f"command string given (self.cmdstring): |w{self.cmdstring}|n\n"
        string += utils.fill(f"current cmdset (self.cmdset): |w{self.cmdset}|n\n")
        string += "\n" + "-" * 50
        string += "\nVariables from MuxCommand baseclass\n"
        string += f"\nraw argument (self.raw): |w{self.raw}|n \n"
        string += f"cmd args (self.args): |w{self.args}|n\n"
        string += f"cmd switches (self.switches): |w{self.switches}|n\n"
        string += f"cmd options (self.switch_options): |w{self.switch_options}|n\n"
        string += f"cmd parse left/right using (self.rhs_split): |w{self.rhs_split}|n\n"
        string += f"space-separated arg list (self.arglist): |w{self.arglist}|n\n"
        string += f"lhs, left-hand side of '=' (self.lhs): |w{self.lhs}|n\n"
        string += f"lhs, comma separated (self.lhslist): |w{self.lhslist}|n\n"
        string += f"rhs, right-hand side of '=' (self.rhs): |w{self.rhs}|n\n"
        string += f"rhs, comma separated (self.rhslist): |w{self.rhslist}|n\n"

        """
        This is the hook function that actually does all the work. It is called
         by the cmdhandler right after self.parser() finishes, and so has access
         to all the variables defined therein.
        """

class MuxAccountCommand(MuxCommand):
    """
    This is an on-Account version of the MuxCommand. Since these commands sit
    on Accounts rather than on Characters/Objects, we need to check
    this in the parser.

    Account commands are available also when puppeting a Character, it's
    just that they are applied with a lower priority and are always
    available, also when disconnected from a character (i.e. "ooc").

    This class makes sure that caller is always an Account object, while
    creating a new property "character" that is set only if a
    character is actually attached to this Account and Session.
    """

    account_caller = True  # Using MuxAccountCommand explicitly defaults the caller to an account

"""
System commands

These are the default commands called by the system commandhandler
when various exceptions occur. If one of these commands are not
implemented and part of the current cmdset, the engine falls back
to a default solution instead.

Some system commands are shown in this module
as a REFERENCE only (they are not all added to Evennia's
default cmdset since they don't currently do anything differently from the
default backup systems hard-wired in the engine).

Overloading these commands in a cmdset can be used to create
interesting effects. An example is using the NoMatch system command
to implement a line-editor where you don't have to start each
line with a command (if there is no match to a known command,
the line is just added to the editor buffer).
"""

# The command keys the engine is calling
# (the actual names all start with __)
from evennia.commands.cmdhandler import CMD_MULTIMATCH, CMD_NOINPUT, CMD_NOMATCH
from evennia.utils import create, utils
from evennia.utils.utils import at_search_result

# Command called when there is no input at line
# (i.e. an lone return key)

class SystemNoInput(COMMAND_DEFAULT_CLASS):
    """
    This is called when there is no input given
    """

    key = CMD_NOINPUT

        "Do nothing."
        pass

#
# Command called when there was no match to the
# command name
#
class SystemNoMatch(COMMAND_DEFAULT_CLASS):
    """
    No command was found matching the given input.
    """

    key = CMD_NOMATCH

        """
        This is given the failed raw string as input.
        """
        self.msg("Huh?")

#
# Command called when there were multiple matches to the command.
#
class SystemMultimatch(COMMAND_DEFAULT_CLASS):
    """
    Multiple command matches.

    The cmdhandler adds a special attribute 'matches' to this
    system command.

      matches = [(cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname) , (cmdname, ...), ...]

    Here, `cmdname` is the command's name and `args` the rest of the incoming string,
    without said command name. `cmdobj` is the Command instance, the cmdlen is
    the same as len(cmdname) and mratio is a measure of how big a part of the
    full input string the cmdname takes up - an exact match would be 1.0. Finally,
    the `raw_cmdname` is the cmdname unmodified by eventual prefix-stripping.

    """

    key = CMD_MULTIMATCH

        """
        Handle multiple-matches by using the at_search_result default handler.

        """
        # this was set by the cmdparser and is a tuple
        #    (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname). See
        # evennia.commands.cmdparse.create_match for more details.
        matches = self.matches
        # at_search_result will itself msg the multimatch options to the caller.
        at_search_result([match[2] for match in matches], self.caller, query=matches[0][0])

"""

"""

import code
import os
import sys

import django
import twisted

from evennia.scripts.taskhandler import TaskHandlerTask
from evennia.utils import gametime, logger, search, utils
from evennia.utils.utils import class_from_module, iter_to_str

_BROADCAST_SERVER_RESTART_MESSAGES = settings.BROADCAST_SERVER_RESTART_MESSAGES

_RESOURCE = None
_IDMAPPER = None

__all__ = (
    "CmdAccounts",
    "CmdReload",
    "CmdReset",
    "CmdShutdown",
    "CmdPy",
    "CmdService",
    "CmdAbout",
    "CmdTime",
    "CmdServerLoad",
    "CmdTasks",
    "CmdTickers",
)

class CmdReload(COMMAND_DEFAULT_CLASS):
    """
    reload the server

    Usage:
      reload [reason]

    This restarts the server. The Portal is not
    affected. Non-persistent scripts will survive a reload (use
    reset to purge) and at_reload() hooks will be called.
    """

    key = "@reload"
    aliases = ["@restart"]
    locks = "cmd:perm(reload) or perm(Developer)"

        """
        Reload the system.
        """
        reason = ""
            reason = "(Reason: %s) " % self.args.rstrip(".")
        if _BROADCAST_SERVER_RESTART_MESSAGES:
            evennia.SESSION_HANDLER.announce_all(f" Server restart initiated {reason}...")
        evennia.SESSION_HANDLER.portal_restart_server()

class CmdReset(COMMAND_DEFAULT_CLASS):
    """
    reset and reboot the server

    Usage:
      reset

    Notes:
      For normal updating you are recommended to use reload rather
      than this command. Use shutdown for a complete stop of
      everything.

    This emulates a cold reboot of the Server component of Evennia.
    The difference to shutdown is that the Server will auto-reboot
    and that it does not affect the Portal, so no users will be
    disconnected. Contrary to reload however, all shutdown hooks will
    be called and any non-database saved scripts, ndb-attributes,
    cmdsets etc will be wiped.

    """

    key = "@reset"
    aliases = ["@reboot"]

        """
        """
        evennia.SESSION_HANDLER.announce_all(" Server resetting/restarting ...")
        evennia.SESSION_HANDLER.portal_reset_server()

class CmdShutdown(COMMAND_DEFAULT_CLASS):

    """
    stop the server completely

    Usage:
      shutdown [announcement]

    Gracefully shut down both Server and Portal.
    """

    key = "@shutdown"
    locks = "cmd:perm(shutdown) or perm(Developer)"

        """Define function"""
        # Only allow shutdown if caller has session
        if not self.caller.sessions.get():
            return
        self.msg("Shutting down server ...")
        announcement = "\nServer is being SHUT DOWN!\n"
            announcement += "%s\n" % self.args
        logger.log_info(f"Server shutdown by {self.caller.name}.")
        evennia.SESSION_HANDLER.announce_all(announcement)
        evennia.SESSION_HANDLER.portal_shutdown()

def _py_load(caller):
    return ""

def _py_code(caller, buf):
    """
    Execute the buffer.
    """
    measure_time = caller.db._py_measure_time
    client_raw = caller.db._py_clientraw
    string = "Executing code%s ..." % (" (measure timing)" if measure_time else "")
    _run_code_snippet(
        caller, buf, mode="exec", measure_time=measure_time, client_raw=client_raw, show_input=False
    )

def _py_quit(caller):
    del caller.db._py_measure_time
    caller.msg("Exited the code editor.")

def _run_code_snippet(
    caller, pycode, mode="eval", measure_time=False, client_raw=False, show_input=True
):
    """
    Run code and try to display information to the caller.

    Args:
        caller (Object): The caller.
        pycode (str): The Python code to run.
        measure_time (bool, optional): Should we measure the time of execution?
        client_raw (bool, optional): Should we turn off all client-specific escaping?
        show_input (bookl, optional): Should we display the input?

    """
    # Try to retrieve the session
    if hasattr(caller, "sessions"):
        sessions = caller.sessions.all()

    available_vars = evennia_local_vars(caller)

    if show_input:
            data = {
                # TODO: 'highlight' is not used yet
                "text": (f">>> {pycode}", {"type": "py_input"}),
                "options": {"raw": True, "highlight": True},
            }
            try:
                caller.msg(session=session, **data)
                caller.msg(**data)

    try:
        # reroute standard output to game client console
        old_stdout = sys.stdout
        old_stderr = sys.stderr

        class FakeStd:
            def __init__(self, caller):
                self.caller = caller

            def write(self, string):
                self.caller.msg(text=(string.rstrip("\n"), {"type": "py_output"}))

        fake_std = FakeStd(caller)
        sys.stdout = fake_std
        sys.stderr = fake_std

        try:
            pycode_compiled = compile(pycode, "", mode)
            mode = "exec"

        duration = ""
        if measure_time:
            t0 = time.time()
            ret = eval(pycode_compiled, {}, available_vars)
            t1 = time.time()
            duration = f" (runtime ~ {(t1 - t0) * 1000:.4f} ms)"
            caller.msg(duration)
        else:

        errlist = traceback.format_exc().split("\n")
        if len(errlist) > 4:
            errlist = errlist[4:]
        ret = "\n".join("%s" % line for line in errlist if line)
    finally:
        # return to old stdout
        sys.stdout = old_stdout
        sys.stderr = old_stderr

    if ret is None:
        return

    if not client_raw:
        ret = str(ret)

    if isinstance(ret, tuple):
        # we must convert here to allow msg to pass it (a tuple is confused
        # with a outputfunc structure)

    for session in sessions:
        try:
                (ret, {"type": "py_output"}),
                session=session,
                options={"raw": True, "client_raw": client_raw, "highlight": True},
            )
        except TypeError:
            )

def evennia_local_vars(caller):
    """Return Evennia local variables usable in the py command as a dictionary."""

    return {
        "self": caller,
        "me": caller,
        "here": getattr(caller, "location", None),
        "evennia": evennia,
        "ev": evennia,
        "inherits_from": utils.inherits_from,
    }

class EvenniaPythonConsole(code.InteractiveConsole):

    """Evennia wrapper around a Python interactive console."""

    def __init__(self, caller):
        super().__init__(evennia_local_vars(caller))
        self.caller = caller

    def write(self, string):
        """Don't send to stderr, send to self.caller."""
        self.caller.msg(string)

    def push(self, line):
        """Push some code, whether complete or not."""

                self.caller.msg(string.split("\n", 1)[0])

        fake_std = FakeStd(self.caller)
        try:
            result = super().push(line)
        finally:
            sys.stdout = old_stdout
            sys.stderr = old_stderr

class CmdPy(COMMAND_DEFAULT_CLASS):
    """
    execute a snippet of python code

    Usage:
      py [cmd]
      py/edit
      py/time <cmd>
      py/clientraw <cmd>
      py/noecho

    Switches:
      time - output an approximate execution time for <cmd>
      edit - open a code editor for multi-line code experimentation
      clientraw - turn off all client-specific escaping. Note that this may
        lead to different output depending on prototocol (such as angular brackets
        being parsed as HTML in the webclient but not in telnet clients)
      noecho - in Python console mode, turn off the input echo (e.g. if your client
        does this for you already)

    Without argument, open a Python console in-game. This is a full console,
    accepting multi-line Python code for testing and debugging. Type `exit()` to
    return to the game. If Evennia is reloaded, the console will be closed.

    Enter a line of instruction after the 'py' command to execute it
    immediately.  Separate multiple commands by ';' or open the code editor
    using the /edit switch (all lines added in editor will be executed
    immediately when closing or using the execute command in the editor).

    A few variables are made available for convenience in order to offer access
    to the system (you can import more at execution time).

    Available variables in py environment:
      self, me                   : caller
      here                       : caller.location
      evennia                    : the evennia API
      inherits_from(obj, parent) : check object inheritance

    You can explore The evennia API from inside the game by calling
    the `__doc__` property on entities:
        py evennia.__doc__
        py evennia.managers.__doc__

    |rNote: In the wrong hands this command is a severe security risk.  It
    should only be accessible by trusted server admins/superusers.|n

    """

    key = "@py"
    aliases = ["@!"]
    switch_options = ("time", "edit", "clientraw", "noecho")
    locks = "cmd:perm(py) or perm(Developer)"

        pycode = self.args

        noecho = "noecho" in self.switches

            caller.db._py_measure_time = "time" in self.switches
            caller.db._py_clientraw = "clientraw" in self.switches
            EvEditor(
                loadfunc=_py_load,
                savefunc=_py_code,
                quitfunc=_py_quit,
                key="Python exec: :w  or :!",
                codefunc=_py_code,
            )
            return

        if not pycode:
            # Run in interactive mode
            console = EvenniaPythonConsole(self.caller)
            banner = (
                "|gEvennia Interactive Python mode{echomode}\n"
                "Python {version} on {platform}".format(
                    echomode=" (no echoing of prompts)" if noecho else "",
                    version=sys.version,
                    platform=sys.platform,
                )
            )
            self.msg(banner)
            line = ""
            main_prompt = "|x[py mode - quit() to exit]|n"
            prompt = main_prompt
            while line.lower() not in ("exit", "exit()"):
                try:
                    line = yield (prompt)
                    if noecho:
                        prompt = "..." if console.push(line) else main_prompt
                    else:
                        if line:
                            self.caller.msg(f">>> {line}")
                        prompt = line if console.push(line) else main_prompt
                except SystemExit:
                    break
            self.msg("|gClosing the Python console.|n")
            return

        _run_code_snippet(
            caller,
            measure_time="time" in self.switches,
            client_raw="clientraw" in self.switches,
        )

class CmdAccounts(COMMAND_DEFAULT_CLASS):
    """
    Manage registered accounts

    Usage:
      accounts [nr]
      accounts/delete <name or #id> [: reason]

    Switches:
      delete    - delete an account from the server

    By default, lists statistics about the Accounts registered with the game.
    It will list the <nr> amount of latest registered accounts
    If not given, <nr> defaults to 10.
    """

    key = "@accounts"
    aliases = ["@account"]
    switch_options = ("delete",)
    locks = "cmd:perm(listaccounts) or perm(Admin)"

        """List the accounts"""

            account = getattr(caller, "account")
            if not account or not account.check_permstring("Developer"):
                caller.msg("You are not allowed to delete accounts.")
                return
            if not args:
                caller.msg("Usage: accounts/delete <name or #id> [: reason]")
                return
            reason = ""
            if ":" in args:
                args, reason = [arg.strip() for arg in args.split(":", 1)]
            # We use account_search since we want to be sure to find also accounts
            # that lack characters.
            accounts = search.account_search(args)
            if not accounts:
                self.msg("Could not find an account by that name.")
                return
            if len(accounts) > 1:
                string = "There were multiple matches:\n"
                string += "\n".join(" %s %s" % (account.id, account.key) for account in accounts)
                return
            account = accounts.first()
            if not account.access(caller, "delete"):
                self.msg("You don't have the permissions to delete that account.")
                return
            username = account.username
            # ask for confirmation
            confirm = (
                "It is often better to block access to an account rather than to delete it. "
                "|yAre you sure you want to permanently delete "
                "account '|n{}|y'|n yes/[no]?".format(username)
            )
            if answer.lower() not in ("y", "yes"):
                caller.msg("Canceled deletion.")
                return

            # Boot the account then delete it.
            self.msg("Informing and disconnecting account ...")
            string = f"\nYour account '{username}' is being *permanently* deleted.\n"
            if reason:
                string += " Reason given:\n  '%s'" % reason
            account.msg(string)
                f"Account Deleted: {account} (Reason: {reason}, Caller: {caller}, IP:"
            )
            account.delete()
            self.msg("Account %s was successfully deleted." % username)
            return

        # No switches, default to displaying a list of accounts.
        if self.args and self.args.isdigit():
            nlim = int(self.args)
        else:
            nlim = 10

        naccounts = AccountDB.objects.count()

        dbtotals = AccountDB.objects.object_totals()
            "|wtypeclass|n", "|wcount|n", "|w%%|n", border="cells", align="l"
        )
        for path, count in dbtotals.items():
            typetable.add_row(path, count, "%.2f" % ((float(count) / naccounts) * 100))
        plyrs = AccountDB.objects.all().order_by("db_date_created")[max(0, naccounts - nlim) :]
            "|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", border="cells", align="l"
        )
        for ply in plyrs:
                utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.path
            )

        string = f"\n|wAccount typeclass distribution:|n\n{typetable}"
        string += f"\n|wLast {min(naccounts, nlim)} Accounts created:|n\n{latesttable}"

class CmdService(COMMAND_DEFAULT_CLASS):
    """
    manage system services

    Usage:
      service[/switch] <service>

    Switches:
      list   - shows all available services (default)
      start  - activates or reactivate a service
      stop   - stops/inactivate a service (can often be restarted)
      delete - tries to permanently remove a service

    Service management system. Allows for the listing,
    starting, and stopping of services. If no switches
    are given, services will be listed. Note that to operate on the
    service you have to supply the full (green or red) name as given
    in the list.
    """

    key = "@service"
    aliases = ["@services"]
    switch_options = ("list", "start", "stop", "delete")
    locks = "cmd:perm(service) or perm(Developer)"

        if switches and switches[0] not in ("list", "start", "stop", "delete"):
            caller.msg("Usage: service/<list|start|stop|delete> [servicename]")
            return

        # get all services
        service_collection = evennia.SESSION_HANDLER.server.services

        if not switches or switches[0] == "list":
            # Just display the list of installed services and their
            # status, then exit.
                "|wService|n (use services/start|stop|delete)", "|wstatus", align="l"
            )
            for service in service_collection.services:
                table.add_row(service.name, service.running and "|gRunning" or "|rNot Running")
            caller.msg(str(table))
            return

        # Get the service to start / stop

        try:
            service = service_collection.getServiceNamed(self.args)
            string = "Invalid service name. This command is case-sensitive. "
            string += "See service/list for valid service name (enter the full name exactly)."
            return

        if switches[0] in ("stop", "delete"):
            # Stopping/killing a service gracefully closes it and disconnects
            # any connections (if applicable).

            delmode = switches[0] == "delete"
            if not service.running:
                caller.msg("That service is not currently running.")
                return
            if service.name[:7] == "Evennia":
                if delmode:
                    caller.msg("You cannot remove a core Evennia service (named 'Evennia*').")
                    return
                string = (
                    "|RYou seem to be shutting down a core Evennia "
                    "service (named 'Evennia*').\nNote that stopping "
                    "some TCP port services will *not* disconnect users "
                    "*already* connected on those ports, but *may* "
                    "instead cause spurious errors for them.\nTo safely "
                    "and permanently remove ports, change settings file "
                    "and restart the server.|n\n"
                )
                caller.msg(string)

            if delmode:
                service.stopService()
                service_collection.removeService(service)
                caller.msg(f"|gStopped and removed service '{self.args}'.|n")
            else:
                caller.msg(f"Stopping service '{self.args}'...")
                try:
                    service.stopService()
                except Exception as err:
                        f"|rErrors were reported when stopping this service{err}.\n"
                        "If there are remaining problems, try reloading "
                        "or rebooting the server."
                    )
                caller.msg(f"|g... Stopped service '{self.args}'.|n")
            return

        if switches[0] == "start":
            # Attempt to start a service.
            if service.running:
                caller.msg("That service is already running.")
                return
            caller.msg(f"Starting service '{self.args}' ...")
            try:
                service.startService()
                    f"|rErrors were reported when starting this service{err}.\n"
                    "If there are remaining problems, try reloading the server, changing the "
                    "settings if it's a non-standard service.|n"
                )
            caller.msg("|gService started.|n")

class CmdAbout(COMMAND_DEFAULT_CLASS):
    """
    show Evennia info

    Usage:
      about

    Display info about the game engine.
    """

    key = "@about"
    aliases = "@version"

        """Display information about server or target"""

         |cEvennia|n MU* development system

         |wEvennia version|n: {version}
         |wOS|n: {os}
         |wPython|n: {python}
         |wTwisted|n: {twisted}
         |wDjango|n: {django}

         |wHomepage|n https://evennia.com
         |wCode|n https://github.com/evennia/evennia
         |wDemo|n https://demo.evennia.com
         |wGame listing|n https://games.evennia.com
         |wChat|n https://discord.gg/AJJpcRUhtF
         |wForum|n https://github.com/evennia/evennia/discussions
         |wLicence|n https://opensource.org/licenses/BSD-3-Clause
         |wMaintainer|n (2010-)   Griatch (griatch AT gmail DOT com)
         |wMaintainer|n (2006-10) Greg Taylor

        """.format(
            version=utils.get_evennia_version(),
            os=os.name,
            python=sys.version.split()[0],
            twisted=twisted.version.short(),
            django=django.get_version(),
        )

class CmdTime(COMMAND_DEFAULT_CLASS):
    """
    show server time statistics

    Usage:
      time

    List Server time statistics such as uptime
    and the current time stamp.
    """

    key = "@time"
    aliases = "@uptime"
    locks = "cmd:perm(time) or perm(Player)"

        """Show server time data in a table."""
        table1 = self.styled_table("|wServer time", "", align="l", width=78)
        table1.add_row("Current uptime", utils.time_format(gametime.uptime(), 3))
        table1.add_row("Portal uptime", utils.time_format(gametime.portal_uptime(), 3))
        table1.add_row("Total runtime", utils.time_format(gametime.runtime(), 2))
        table1.add_row("First start", datetime.datetime.fromtimestamp(gametime.server_epoch()))
        table1.add_row("Current time", datetime.datetime.now())
        table1.reformat_column(0, width=30)
        table2 = self.styled_table(
            "|wIn-Game time",
            "|wReal time x %g" % gametime.TIMEFACTOR,
            width=78,
            border_top=0,
        )
        epochtxt = "Epoch (%s)" % ("from settings" if settings.TIME_GAME_EPOCH else "server start")
        table2.add_row(epochtxt, datetime.datetime.fromtimestamp(gametime.game_epoch()))
        table2.add_row("Total time passed:", utils.time_format(gametime.gametime(), 2))
        table2.add_row(
            "Current time ", datetime.datetime.fromtimestamp(gametime.gametime(absolute=True))
        )
        table2.reformat_column(0, width=30)
        self.msg(str(table1) + "\n" + str(table2))

class CmdServerLoad(COMMAND_DEFAULT_CLASS):
    """
    show server load and memory statistics

    Usage:
       server[/mem]

    Switches:
        mem - return only a string of the current memory usage
        flushmem - flush the idmapper cache

    This command shows server load statistics and dynamic memory
    usage. It also allows to flush the cache of accessed database
    objects.

    Some Important statistics in the table:

    |wServer load|n is an average of processor usage. It's usually
    between 0 (no usage) and 1 (100% usage), but may also be
    temporarily higher if your computer has multiple CPU cores.

    The |wResident/Virtual memory|n displays the total memory used by
    the server process.

    Evennia |wcaches|n all retrieved database entities when they are
    loaded by use of the idmapper functionality. This allows Evennia
    to maintain the same instances of an entity and allowing
    non-persistent storage schemes. The total amount of cached objects
    are displayed plus a breakdown of database object types.

    The |wflushmem|n switch allows to flush the object cache. Please
    note that due to how Python's memory management works, releasing
    caches may not show you a lower Residual/Virtual memory footprint,
    the released memory will instead be re-used by the program.

    """

    key = "@server"
    aliases = ["@serverload"]
    switch_options = ("mem", "flushmem")
    locks = "cmd:perm(list) or perm(Developer)"

        """Show list."""

        global _IDMAPPER
        if not _IDMAPPER:
            from evennia.utils.idmapper import models as _IDMAPPER

        if "flushmem" in self.switches:
            # flush the cache
            prev, _ = _IDMAPPER.cache_size()
            nflushed = _IDMAPPER.flush_cache()
            now, _ = _IDMAPPER.cache_size()
            string = (
                "The Idmapper cache freed |w{idmapper}|n database objects.\n"
                "The Python garbage collector freed |w{gc}|n Python instances total."
            )
            self.msg(string.format(idmapper=(prev - now), gc=nflushed))
            return

        # display active processes

        os_windows = os.name == "nt"
        pid = os.getpid()

        if os_windows:
            # Windows requires the psutil module to even get paltry
            # statistics like this (it's pretty much worthless,
            # unfortunately, since it's not specific to the process) /rant
            try:
                import psutil

                has_psutil = True
                has_psutil = False

            if has_psutil:
                loadavg = psutil.cpu_percent()
                _mem = psutil.virtual_memory()
                rmem = _mem.used / (1000.0 * 1000)
                pmem = _mem.percent

                if "mem" in self.switches:
                    string = "Total computer memory usage: |w%g|n MB (%g%%)"
                    self.msg(string % (rmem, pmem))
                    return
                # Display table
                loadtable = self.styled_table("property", "statistic", align="l")
                loadtable.add_row("Total CPU load", "%g %%" % loadavg)
                loadtable.add_row("Total computer memory usage", "%g MB (%g%%)" % (rmem, pmem))
                loadtable.add_row("Process ID", "%g" % pid),
            else:
                loadtable = (
                    "Not available on Windows without 'psutil' library "
                    "(install with |wpip install psutil|n)."
                )

        else:
            # Linux / BSD (OSX) - proper pid-based statistics

            global _RESOURCE
            if not _RESOURCE:
                import resource as _RESOURCE

            loadavg = os.getloadavg()[0]
            rmem = (
                float(os.popen("ps -p %d -o %s | tail -1" % (pid, "rss")).read()) / 1000.0
            )  # resident memory
            vmem = (
                float(os.popen("ps -p %d -o %s | tail -1" % (pid, "vsz")).read()) / 1000.0
            )  # virtual memory
            pmem = float(
                os.popen("ps -p %d -o %s | tail -1" % (pid, "%mem")).read()
            )  # % of resident memory to total
            rusage = _RESOURCE.getrusage(_RESOURCE.RUSAGE_SELF)

            if "mem" in self.switches:
                string = "Memory usage: RMEM: |w%g|n MB (%g%%), VMEM (res+swap+cache): |w%g|n MB."
                self.msg(string % (rmem, pmem, vmem))
                return

            loadtable = self.styled_table("property", "statistic", align="l")
            loadtable.add_row("Server load (1 min)", "%g" % loadavg)
            loadtable.add_row("Process ID", "%g" % pid),
            loadtable.add_row("Memory usage", "%g MB (%g%%)" % (rmem, pmem))
            loadtable.add_row("Virtual address space", "")
            loadtable.add_row("|x(resident+swap+caching)|n", "%g MB" % vmem)
            loadtable.add_row(
                "CPU time used (total)",
                "%s (%gs)" % (utils.time_format(rusage.ru_utime), rusage.ru_utime),
            )
                "CPU time used (user)",
                "%s (%gs)" % (utils.time_format(rusage.ru_stime), rusage.ru_stime),
            )
                "Page faults",
                "%g hard,  %g soft, %g swapouts"
                % (rusage.ru_majflt, rusage.ru_minflt, rusage.ru_nswap),
            )
                "Disk I/O", "%g reads, %g writes" % (rusage.ru_inblock, rusage.ru_oublock)
            )
            loadtable.add_row("Network I/O", "%g in, %g out" % (rusage.ru_msgrcv, rusage.ru_msgsnd))
                "Context switching",
                "%g vol, %g forced, %g signals"
                % (rusage.ru_nvcsw, rusage.ru_nivcsw, rusage.ru_nsignals),
            )

        # os-generic

        string = "|wServer CPU and Memory load:|n\n%s" % loadtable

        # object cache count (note that sys.getsiseof is not called so this works for pypy too.
        total_num, cachedict = _IDMAPPER.cache_size()
        sorted_cache = sorted(
            [(key, num) for key, num in cachedict.items() if num > 0],
            key=lambda tup: tup[1],
        )
        memtable = self.styled_table("entity name", "number", "idmapper %", align="l")
        for tup in sorted_cache:
            memtable.add_row(tup[0], "%i" % tup[1], "%.2f" % (float(tup[1]) / total_num * 100))

        string += "\n|w Entity idmapper cache:|n %i items\n%s" % (total_num, memtable)

        # return to caller

class CmdTickers(COMMAND_DEFAULT_CLASS):
    """
    View running tickers

    Usage:
      tickers

    Note: Tickers are created, stopped and manipulated in Python code
    using the TickerHandler. This is merely a convenience function for
    inspecting the current status.

    """

    key = "@tickers"
    locks = "cmd:perm(tickers) or perm(Builder)"

        from evennia import TICKER_HANDLER

        all_subs = TICKER_HANDLER.all_display()
        if not all_subs:
            self.msg("No tickers are currently active.")
            return
        table = self.styled_table("interval (s)", "object", "path/methodname", "idstring", "db")
        for sub in all_subs:
                sub[3],
                "%s%s"
                % (
                    sub[0] or "[None]",
                    sub[0] and " (#%s)" % (sub[0].id if hasattr(sub[0], "id") else "") or "",
                ),
                sub[1] if sub[1] else sub[2],
                sub[4] or "[Unset]",
                "*" if sub[5] else "-",
            )
        self.msg("|wActive tickers|n:\n" + str(table))

class CmdTasks(COMMAND_DEFAULT_CLASS):
    """
    Display or terminate active tasks (delays).

    Usage:
        tasks[/switch] [task_id or function_name]

    Switches:
        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.

    Notes:
        A task is a single use method of delaying the call of a function. Calls are created
        in code, using `evennia.utils.delay`.
        See |luhttps://www.evennia.com/docs/latest/Command-Duration.html|ltthe docs|le for help.

        By default, tasks that are canceled and never called are cleaned up after one minute.

    Examples:
        - `tasks/cancel move_callback` - Cancels all movement delays from the slow_exit contrib.
            In this example slow exits creates it's tasks with
            `utils.delay(move_delay, move_callback)`
        - `tasks/cancel 2` - Cancel task id 2.

    """

    key = "@tasks"
    aliases = ["@delays", "@task"]
    switch_options = ("pause", "unpause", "do_task", "call", "remove", "cancel")
    locks = "perm(Developer)"

    def coll_date_func(task):
        """Replace regex characters in date string and collect deferred function name."""
        t_comp_date = str(task[0]).replace("-", "/")
        t_func_name = str(task[1]).split(" ")
        t_func_mem_ref = t_func_name[3] if len(t_func_name) >= 4 else None
        return t_comp_date, t_func_mem_ref

    def do_task_action(self, *args, **kwargs):
        """
        Process the action of a tasks command.

        This exists to gain support with yes or no function from EvMenu.
        """
        task_id = self.task_id

        # get a reference of the global task handler
        global _TASK_HANDLER
        if _TASK_HANDLER is None:
            from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER

        # verify manipulating the correct task
        task_args = _TASK_HANDLER.tasks.get(task_id, False)
        if not task_args:  # check if the task is still active
            self.msg("Task completed while waiting for input.")
            return
        else:
            # make certain a task with matching IDs has not been created
            t_comp_date, t_func_mem_ref = self.coll_date_func(task_args)
            if self.t_comp_date != t_comp_date or self.t_func_mem_ref != t_func_mem_ref:
                self.msg("Task completed while waiting for input.")
                return

        # Do the action requested by command caller
        action_return = self.task_action()
        self.msg(f"{self.action_request} request completed.")
        self.msg(f"The task function {self.action_request} returned: {action_return}")

        # handle no tasks active.
        if not _TASK_HANDLER.tasks:
            self.msg("There are no active tasks.")
            if self.switches or self.args:
                self.msg("Likely the task has completed and been removed.")
            return

        # handle caller's request to manipulate a task(s)
        if self.switches and self.lhs:
            # find if the argument is a task id or function name
            action_request = self.switches[0]
            try:
                arg_is_id = int(self.lhslist[0])
                arg_is_id = False

            # if the argument is a task id, proccess the action on a single task
            if arg_is_id:
                err_arg_msg = "Switch and task ID are required when manipulating a task."
                task_comp_msg = "Task completed while processing request."

                # handle missing arguments or switches
                if not self.switches and self.lhs:
                    self.msg(err_arg_msg)
                    return

                # create a handle for the task
                task_id = arg_is_id
                task = TaskHandlerTask(task_id)

                # handle task no longer existing
                if not task.exists():
                    self.msg(f"Task {task_id} does not exist.")
                    return

                # get a reference of the function caller requested
                switch_action = getattr(task, action_request, False)
                if not switch_action:
                    self.msg(
                        f"{self.switches[0]}, is not an acceptable task action or "
                        f"{task_comp_msg.lower()}"
                    )

                # verify manipulating the correct task
                if task_id in _TASK_HANDLER.tasks:
                    task_args = _TASK_HANDLER.tasks.get(task_id, False)
                    if not task_args:  # check if the task is still active
                        self.msg(task_comp_msg)
                        return
                    else:
                        t_comp_date, t_func_mem_ref = self.coll_date_func(task_args)
                        t_func_name = str(task_args[1]).split(" ")
                        t_func_name = t_func_name[1] if len(t_func_name) >= 2 else None

                if task.exists():  # make certain the task has not been called yet.
                    prompt = (
                        f"{action_request.capitalize()} task {task_id} with completion date "
                        f"{t_comp_date} ({t_func_name}) {{options}}?"
                    )
                    no_msg = f"No {action_request} processed."
                    # record variables for use in do_task_action method
                    self.task_id = task_id
                    self.t_comp_date = t_comp_date
                    self.t_func_mem_ref = t_func_mem_ref
                    self.task_action = switch_action
                    self.action_request = action_request
                    ask_yes_no(
                        prompt=prompt,
                        yes_action=self.do_task_action,
                        no_action=no_msg,
                        default="Y",
                        allow_abort=True,
                    )
                    return True
                else:
                    self.msg(task_comp_msg)
                    return

            # the argument is not a task id, process the action on all task deferring the function
            # specified as an argument
            else:
                name_match_found = False
                arg_func_name = self.lhslist[0].lower()

                # repack tasks into a new dictionary
                current_tasks = {}
                for task_id, task_args in _TASK_HANDLER.tasks.items():
                    current_tasks.update({task_id: task_args})

                # call requested action on all tasks with the function name
                for task_id, task_args in current_tasks.items():
                    t_func_name = str(task_args[1]).split(" ")
                    t_func_name = t_func_name[1] if len(t_func_name) >= 2 else None
                    # skip this task if it is not for the function desired
                    if arg_func_name != t_func_name:
                        continue
                    name_match_found = True
                    task = TaskHandlerTask(task_id)
                    switch_action = getattr(task, action_request, False)
                    if switch_action:
                        action_return = switch_action()
                        self.msg(f"Task action {action_request} completed on task ID {task_id}.")
                        self.msg(f"The task function {action_request} returned: {action_return}")

                # provide a message if not tasks of the function name was found
                if not name_match_found:
                    self.msg(f"No tasks deferring function name {arg_func_name} found.")
                    return

        # check if an maleformed request was created
        elif self.switches or self.lhs:
            self.msg("Task command misformed.")
            self.msg("Proper format tasks[/switch] [function name or task id]")
            return

        # No task manupilation requested, build a table of tasks and display it
        # get the width of screen in characters
        # create table header and list to hold tasks data and actions
        tasks_header = (
            "Task ID",
            "Completion Date",
            "Function",
            "Arguments",
            "KWARGS",
            "persistent",
        )
        # empty list of lists, the size of the header
        tasks_list = [list() for i in range(len(tasks_header))]
        for task_id, task in _TASK_HANDLER.tasks.items():
            # collect data from the task
            t_comp_date, t_func_mem_ref = self.coll_date_func(task)
            t_func_name = str(task[1]).split(" ")
            t_func_name = t_func_name[1] if len(t_func_name) >= 2 else None
            t_args = str(task[2])
            t_kwargs = str(task[3])
            t_pers = str(task[4])
            # add task data to the tasks list
            task_data = (task_id, t_comp_date, t_func_name, t_args, t_kwargs, t_pers)
            for i in range(len(tasks_header)):
                tasks_list[i].append(task_data[i])
        # create and display the table
        tasks_table = EvTable(
            *tasks_header, table=tasks_list, maxwidth=width, border="cells", align="c"
        )
        actions = (f"/{switch}" for switch in self.switch_options)
        helptxt = f"\nActions: {iter_to_str(actions)}"
        self.msg(str(tasks_table) + helptxt)

# -*- coding: utf-8 -*-
"""
 ** OBS - this is not a normal command module! **
 ** You cannot import anything in this module as a command! **

This is part of the Evennia unittest framework, for testing the
stability and integrity of the codebase during updates. This module
test the default command set. It is instantiated by the
evennia/objects/tests.py module, which in turn is run by as part of the
main test suite started with
 > python game/manage.py test.

"""
from unittest.mock import MagicMock, Mock, patch

from anything import Anything
from django.test import override_settings
from parameterized import parameterized
from twisted.internet import task

from evennia import (
    DefaultCharacter,
    DefaultExit,
    DefaultObject,
    DefaultRoom,
    ObjectDB,
    search_object,
)
from evennia.commands import cmdparser
from evennia.commands.command import Command, InterruptCommand
    account,
    admin,
    building,
    comms,
    general,
)
from evennia.commands.default import help as help_module
from evennia.commands.default import syscommands, system, unloggedin
from evennia.commands.default.cmdset_character import CharacterCmdSet
from evennia.commands.default.muxcommand import MuxCommand
from evennia.utils import create, gametime, utils
from evennia.utils.test_resources import BaseEvenniaCommandTest  # noqa
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaCommandTest

# Command testing

class TestGeneral(BaseEvenniaCommandTest):
    def test_look(self):
        rid = self.room1.id
        self.call(general.CmdLook(), "here", "Room(#{})\nroom_desc".format(rid))

    def test_look_no_location(self):
        self.char1.location = None
        self.call(general.CmdLook(), "", "You have no location to look at!")

    def test_look_nonexisting(self):
        self.call(general.CmdLook(), "yellow sign", "Could not find 'yellow sign'.")

    def test_home(self):
        self.call(general.CmdHome(), "", "You are already home")

    def test_go_home(self):
        self.call(building.CmdTeleport(), "/quiet Room2")
        self.call(general.CmdHome(), "", "There's no place like home")

    def test_no_home(self):
        self.char1.home = None
        self.call(general.CmdHome(), "", "You have no home")

    def test_inventory(self):
        self.call(general.CmdInventory(), "", "You are not carrying anything.")

    def test_pose(self):
        self.char2.msg = Mock()
        self.call(general.CmdPose(), "looks around", "Char looks around")
        self.char2.msg.assert_called_with(
            text=("Char looks around", {"type": "pose"}), from_obj=self.char1
        )

    def test_nick(self):
        self.call(
            general.CmdNick(),
            "testalias = testaliasedstring1",
            "Inputline-nick 'testalias' mapped to 'testaliasedstring1'.",
        )
            "/account testalias = testaliasedstring2",
            "Account-nick 'testalias' mapped to 'testaliasedstring2'.",
        )
            "/object testalias = testaliasedstring3",
            "Object-nick 'testalias' mapped to 'testaliasedstring3'.",
        )
        self.assertEqual("testaliasedstring1", self.char1.nicks.get("testalias"))
        self.assertEqual(
            "testaliasedstring2", self.char1.nicks.get("testalias", category="account")
        )
        self.assertEqual(None, self.char1.account.nicks.get("testalias", category="account"))
        self.assertEqual("testaliasedstring3", self.char1.nicks.get("testalias", category="object"))

    def test_nick_list(self):
        self.call(general.CmdNick(), "/list", "No nicks defined.")
        self.call(general.CmdNick(), "test1 = Hello", "Inputline-nick 'test1' mapped to 'Hello'.")
        self.call(general.CmdNick(), "/list", "Defined Nicks:")

    def test_get_and_drop(self):
        self.call(general.CmdGet(), "Obj", "You pick up an Obj.")
        self.call(general.CmdDrop(), "Obj", "You drop an Obj.")

    def test_give(self):
        self.call(general.CmdGive(), "Obj to Char2", "You aren't carrying Obj.")
        self.call(general.CmdGive(), "Obj = Char2", "You aren't carrying Obj.")
        self.call(general.CmdGive(), "Obj to Char2", "You give")
        self.call(general.CmdGive(), "Obj = Char", "You give", caller=self.char2)

    def test_mux_command(self):
        class CmdTest(MuxCommand):
            key = "test"
            switch_options = ("test", "testswitch", "testswitch2")

            def func(self):
                self.msg("Switches matched: {}".format(self.switches))

            CmdTest(),
            "/test/testswitch/testswitch2",
            "Switches matched: ['test', 'testswitch', 'testswitch2']",
        )
        self.call(CmdTest(), "/test", "Switches matched: ['test']")
        self.call(CmdTest(), "/test/testswitch", "Switches matched: ['test', 'testswitch']")
            CmdTest(), "/testswitch/testswitch2", "Switches matched: ['testswitch', 'testswitch2']"
        )
        self.call(CmdTest(), "/testswitch", "Switches matched: ['testswitch']")
        self.call(CmdTest(), "/testswitch2", "Switches matched: ['testswitch2']")
            "/t",
            "test: Ambiguous switch supplied: "
            "Did you mean /test or /testswitch or /testswitch2?|Switches matched: []",
        )
            "/tests",
            "Did you mean /testswitch or /testswitch2?|Switches matched: []",
        )

    def test_say(self):
        self.call(general.CmdSay(), "Testing", 'You say, "Testing"')

    def test_whisper(self):
            general.CmdWhisper(),
            "Obj = Testing",
            'You whisper to Obj, "Testing"',
            caller=self.char2,
        )

    def test_access(self):
        self.call(general.CmdAccess(), "", "Permission Hierarchy (climbing):")

class TestHelp(BaseEvenniaCommandTest):
    maxDiff = None

    def setUp(self):
        super().setUp()
        # we need to set up a logger here since lunr takes over the logger otherwise
        import logging

        logging.basicConfig(level=logging.ERROR)

    def tearDown(self):
        super().tearDown()

        logging.disable(level=logging.ERROR)

    def test_help(self):
        self.call(help_module.CmdHelp(), "", "Commands", cmdset=CharacterCmdSet())

    def test_set_help(self):
            help_module.CmdSetHelp(),
            "testhelp, General = This is a test",
            "Topic 'testhelp' was successfully created.",
            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"
                    "  test/creating extra stuff/subsubtopic\n"
                ),
            ),
            (
                "test/creating",  # startswith-match
                (
                ),
            ),
            (
                "test/extra",  # partial match
                (
                ),
            ),
            (
                "test/extra/subsubtopic",  # partial subsub-match
                "Help for test/creating extra stuff/subsubtopic\n\nA subsubtopic text",
            ),
            (
                "test/creating extra/subsub",  # partial subsub-match
            ),
            ("test/Something else", "Help for test/something else\n\nSomething else"),  # case
            (
                "test/More",  # case
                "Help for test/more\n\nAnother text\n\nSubtopics:\n  test/more/second-more",
            ),
            (
                "test/More/Second-more",
                (
                    "Help for test/more/second-more\n\n"
                    "The Second More text.\n\n"
                    "  test/more/second-more/more again"
                    "  test/more/second-more/third more"
                ),
            ),
            (
                "test/More/-more",  # partial match
                (
                ),
            ),
            (
                "test/more/second/more again",
                "Help for test/more/second-more/more again\n\nEven more text.\n",
            ),
            (
                "test/more/second/third",
                "Help for test/more/second-more/third more\n\nThird more text\n",
            ),
        ]
    )
    def test_subtopic_fetch(self, helparg, expected):
        """
        Check retrieval of subtopics.

        """

        class TestCmd(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

            """

        class TestCmdSet(CmdSet):
            def at_cmdset_creation(self):
                self.add(TestCmd())
                self.add(help_module.CmdHelp())

        self.call(help_module.CmdHelp(), helparg, expected, cmdset=TestCmdSet())

class TestSystem(BaseEvenniaCommandTest):
    def test_py(self):
        # we are not testing CmdReload, CmdReset and CmdShutdown, CmdService or CmdTime
        # since the server is not running during these tests.
        self.call(system.CmdPy(), "1+2", ">>> 1+2|3")
        self.call(system.CmdPy(), "/clientraw 1+2", ">>> 1+2|3")

    def test_scripts(self):
        self.call(building.CmdScripts(), "", "dbref ")

    def test_objects(self):
        self.call(building.CmdObjects(), "", "Object subtype totals")

    def test_about(self):
        self.call(system.CmdAbout(), "", None)

    def test_server_load(self):
        self.call(system.CmdServerLoad(), "", "Server CPU and Memory load:")

def func_test_cmd_tasks():
    return "success"

class TestCmdTasks(BaseEvenniaCommandTest):
        # get a reference of TASK_HANDLER
        self.timedelay = 5
        _TASK_HANDLER.clock = task.Clock()
        self.task_handler = _TASK_HANDLER
        self.task_handler.clear()
        self.task = self.task_handler.add(self.timedelay, func_test_cmd_tasks)
        task_args = self.task_handler.tasks.get(self.task.get_id(), False)

    def test_no_tasks(self):
        self.call(system.CmdTasks(), "", "There are no active tasks.")

    def test_active_task(self):
        cmd_result = self.call(system.CmdTasks(), "")
        for ptrn in (
            "Task ID",
            "Completion",
            "Date",
            "KWARGS",
            "persisten",
            "1",
            r"\d+/\d+/\d+",
            r"\d+\:",
            r"\d+\:\d+",
            r"\:\d+",
            "func_test",
            "{}",
            "False",
        ):
            self.assertRegex(cmd_result, ptrn)

    def test_persistent_task(self):
        self.task_handler.add(self.timedelay, func_test_cmd_tasks, persistent=True)
        self.assertRegex(cmd_result, "True")

    def test_pause_unpause(self):
        # test pause
        args = f"/pause {self.task.get_id()}"
        wanted_msg = "Pause task 1 with completion date"
        cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
        self.assertRegex(cmd_result, " \(func_test_cmd_tasks\) ")
        self.char1.execute_cmd("y")
        self.assertTrue(self.task.paused)
        self.task_handler.clock.advance(self.timedelay + 1)
        # test unpause
        args = f"/unpause {self.task.get_id()}"
        self.assertTrue(self.task.exists())
        wanted_msg = "Unpause task 1 with completion date"
        # verify task continues after unpause
        self.task_handler.clock.advance(1)
        self.assertFalse(self.task.exists())

    def test_do_task(self):
        args = f"/do_task {self.task.get_id()}"
        wanted_msg = "Do_task task 1 with completion date"

    def test_remove(self):
        args = f"/remove {self.task.get_id()}"
        wanted_msg = "Remove task 1 with completion date"

    def test_call(self):
        args = f"/call {self.task.get_id()}"
        wanted_msg = "Call task 1 with completion date"
        # make certain the task is still active
        self.assertTrue(self.task.active())
        # go past delay time, the task should call do_task and remove itself after calling.

    def test_cancel(self):
        args = f"/cancel {self.task.get_id()}"
        wanted_msg = "Cancel task 1 with completion date"
        self.assertFalse(self.task.active())

    def test_func_name_manipulation(self):
        self.task_handler.add(self.timedelay, func_test_cmd_tasks)  # add an extra task
        args = f"/remove func_test_cmd_tasks"
        wanted_msg = (
            "Task action remove completed on task ID 1.|The task function remove returned: True|"
            "Task action remove completed on task ID 2.|The task function remove returned: True"
        )
        self.call(system.CmdTasks(), args, wanted_msg)
        self.assertFalse(self.task_handler.tasks)  # no tasks should exist.

    def test_wrong_func_name(self):
        args = f"/remove intentional_fail"
        wanted_msg = "No tasks deferring function name intentional_fail found."

    def test_no_input(self):
        self.call(system.CmdTasks(), args)
        # task should complete since no input was received

    def test_responce_of_yes(self):
        self.call(system.CmdTasks(), f"/cancel {self.task.get_id()}")
        self.char1.msg = Mock()
        text = ""
        for _, _, kwargs in self.char1.msg.mock_calls:
            text += kwargs.get("text", "")
        self.assertEqual(text, "cancel request completed.The task function cancel returned: True")

    def test_task_complete_waiting_input(self):
        """Test for task completing while waiting for input."""
        text = ""
        self.assertEqual(text, "Task completed while waiting for input.")

    def test_new_task_waiting_input(self):
        """
        Test task completing than a new task with the same ID being made while waitinf for input.
        """
        self.assertTrue(self.task.get_id(), 1)
        text = ""

    def test_misformed_command(self):
            "Task command misformed.|Proper format tasks[/switch] [function name or task id]"
        )
        self.call(system.CmdTasks(), f"/cancel", wanted_msg)

class TestAdmin(BaseEvenniaCommandTest):
    def test_emit(self):
        self.call(admin.CmdEmit(), "Char2 = Test", "Emitted to Char2:\nTest")

    def test_perm(self):
            admin.CmdPerm(),
            "Obj = Builder",
            "Permission 'Builder' given to Obj (the Object/Character).",
        )
            "Char2 = Builder",
            "Permission 'Builder' given to Char2 (the Object/Character).",
        )

    def test_wall(self):
        self.call(admin.CmdWall(), "Test", "Announcing to all connected sessions ...")

    def test_ban(self):
        self.call(admin.CmdBan(), "Char", "Name-ban 'char' was added. Use unban to reinstate.")

    def test_force(self):
        cid = self.char2.id
            admin.CmdForce(),
            "Char2=say test",
            'Char2(#{}) says, "test"|You have forced Char2 to: say test'.format(cid),
        )

class TestAccount(BaseEvenniaCommandTest):
    """
    Test different account-specific modes

    """

        # multisession-mode, auto-puppet, max_nr_characters
        [
            (0, True, 1, "You are out-of-character"),
            (1, True, 1, "You are out-of-character"),
            (2, True, 1, "You are out-of-character"),
            (3, True, 1, "You are out-of-character"),
            (0, False, 1, "Account TestAccount"),
            (1, False, 1, "Account TestAccount"),
            (2, False, 1, "Account TestAccount"),
            (3, False, 1, "Account TestAccount"),
            (0, True, 2, "Account TestAccount"),
            (1, True, 2, "Account TestAccount"),
            (2, True, 2, "Account TestAccount"),
            (3, True, 2, "Account TestAccount"),
            (0, False, 2, "Account TestAccount"),
            (1, False, 2, "Account TestAccount"),
            (2, False, 2, "Account TestAccount"),
            (3, False, 2, "Account TestAccount"),
        ]
    )
    def test_ooc_look(self, multisession_mode, auto_puppet, max_nr_chars, expected_result):
        self.account.characters.add(self.char1)
        self.account.unpuppet_all()

        with self.settings(MULTISESSION=multisession_mode):
            # we need to patch the module header instead of settings
            with patch("evennia.commands.default.account._MAX_NR_CHARACTERS", new=max_nr_chars):
                with patch(
                    "evennia.commands.default.account._AUTO_PUPPET_ON_LOGIN", new=auto_puppet
                ):
                    self.call(
                        account.CmdOOCLook(),
                        "",
                        expected_result,
                        caller=self.account,
                    )

    def test_ooc(self):
        self.call(account.CmdOOC(), "", "You go OOC.", caller=self.account)

    def test_ic(self):
        self.account.unpuppet_object(self.session)
            account.CmdIC(), "Char", "You become Char.", caller=self.account, receiver=self.char1
        )

    def test_ic__other_object(self):
        self.account.characters.add(self.obj1)
            account.CmdIC(), "Obj", "You become Obj.", caller=self.account, receiver=self.obj1
        )

    def test_ic__nonaccess(self):
            account.CmdIC(),
            "Nonexistent",
            "That is not a valid character choice.",
            caller=self.account,
            receiver=self.account,
        )

    def test_password(self):
            account.CmdPassword(),
            "testpassword = testpassword",
            "Password changed.",
        )

    def test_option(self):
        self.call(account.CmdOption(), "", "Client settings", caller=self.account)

    def test_who(self):
        self.call(account.CmdWho(), "", "Accounts:", caller=self.account)

    def test_quit(self):
            account.CmdQuit(), "", "Quitting. Hope to see you again, soon.", caller=self.account
        )

    def test_sessions(self):
        self.call(account.CmdSessions(), "", "Your current session(s):", caller=self.account)

    def test_color_test(self):
        self.call(account.CmdColorTest(), "ansi", "ANSI colors:", caller=self.account)

    def test_char_create(self):
            account.CmdCharCreate(),
            "Test1=Test char",
            "Created new character Test1. Use ic Test1 to enter the game",
        )

    def test_char_delete(self):
        # Chardelete requires user input; this test is mainly to confirm
        # whether permissions are being checked

        # Add char to account playable characters

        # Try deleting as Developer
            account.CmdCharDelete(),
            "Char",
            "This will permanently destroy 'Char'. This cannot be undone. Continue yes/[no]?",
        )

        # Downgrade permissions on account
        self.account.permissions.add("Player")
        self.account.permissions.remove("Developer")

        # Set lock on character object to prevent deletion
        self.char1.locks.add("delete:none()")

        # Try deleting as Player
            "Char",
            "You do not have permission to delete this character.",
        )

        # Set lock on character object to allow self-delete
        self.char1.locks.add("delete:pid(%i)" % self.account.id)

        # Try deleting as Player again
            "Char",
        )

    def test_quell(self):
            account.CmdQuell(),
            "",
            "Quelling to current puppet's permissions (developer).",
        )

class TestBuilding(BaseEvenniaCommandTest):
    def test_create(self):
        typeclass = settings.BASE_OBJECT_TYPECLASS
        name = typeclass.rsplit(".", 1)[1]
            building.CmdCreate(),
            f"/d TestObj1:{typeclass}",  # /d switch is abbreviated form of /drop
            "You create a new %s: TestObj1." % name,
        )
        self.call(building.CmdCreate(), "", "Usage: ")
            f"TestObj1;foo;bar:{typeclass}",
            "You create a new %s: TestObj1 (aliases: foo, bar)." % name,
        )

    def test_examine(self):
        self.call(building.CmdExamine(), "", "Name/key: Room")
        self.call(building.CmdExamine(), "Obj", "Name/key: Obj")
        self.call(building.CmdExamine(), "*TestAccount", "Name/key: TestAccount")

        self.char1.db.test = "testval"
            building.CmdExamine(), "self/test", "Attribute Char/test [category=None]:\n\ntestval"
        )
        self.call(building.CmdExamine(), "NotFound", "Could not find 'NotFound'.")
        self.call(building.CmdExamine(), "out", "Name/key: out")

        # escape inlinefuncs
        self.char1.db.test2 = "this is a $random() value."
            building.CmdExamine(),
            "self/test2",
            "Attribute Char/test2 [category=None]:\n\nthis is a \$random() value.",
        )

        self.room1.scripts.add(self.script.__class__)
        self.call(building.CmdExamine(), "")
        self.account.scripts.add(self.script.__class__)
        self.call(building.CmdExamine(), "*TestAccount")

    def test_set_obj_alias(self):
        oid = self.obj1.id
        self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj")
            building.CmdSetObjAlias(),
            "Obj = TestObj1b",
            "Alias(es) for 'Obj(#{})' set to 'testobj1b'.".format(oid),
        )
        self.call(building.CmdSetObjAlias(), "", "Usage: ")
        self.call(building.CmdSetObjAlias(), "NotFound =", "Could not find 'NotFound'.")

        self.call(building.CmdSetObjAlias(), "Obj", "Aliases for Obj(#{}): 'testobj1b'".format(oid))
        self.call(building.CmdSetObjAlias(), "Obj2 =", "Cleared aliases from Obj2")
        self.call(building.CmdSetObjAlias(), "Obj2 =", "No aliases to clear.")

    def test_copy(self):
            building.CmdCopy(),
            "Obj = TestObj2;TestObj2b, TestObj3;TestObj3b",
            "Copied Obj to 'TestObj3' (aliases: ['TestObj3b']",
        )
        self.call(building.CmdCopy(), "", "Usage: ")
        self.call(building.CmdCopy(), "Obj", "Identical copy of Obj, named 'Obj_copy' was created.")
        self.call(building.CmdCopy(), "NotFound = Foo", "Could not find 'NotFound'.")

    def test_attribute_commands(self):
        self.call(building.CmdSetAttribute(), "", "Usage: ")
            building.CmdSetAttribute(),
            'Obj/test1="value1"',
            "Created attribute Obj/test1 [category:None] = value1",
        )
            'Obj2/test2="value2"',
            "Created attribute Obj2/test2 [category:None] = value2",
        )
            "Obj2/test2",
            "Attribute Obj2/test2 [category:None] = value2",
        )
            "Obj2/NotFound",
            "Attribute Obj2/notfound [category:None] does not exist.",
        )

        with patch("evennia.commands.default.building.EvEditor") as mock_ed:
            self.call(building.CmdSetAttribute(), "/edit Obj2/test3")
            mock_ed.assert_called_with(self.char1, Anything, Anything, key="Obj2/test3")

            'Obj2/test3="value3"',
            "Created attribute Obj2/test3 [category:None] = value3",
        )
            "Obj2/test3 = ",
            "Deleted attribute Obj2/test3 [category:None].",
        )
            "Obj2/test4:Foo = 'Bar'",
            "Created attribute Obj2/test4 [category:Foo] = Bar",
        )
            building.CmdCpAttr(),
            "/copy Obj2/test2 = Obj2/test3",
            '@cpattr: Extra switch "/copy" ignored.|\nCopied Obj2.test2 -> Obj2.test3. '
            "(value: 'value2')",
        )
        self.call(building.CmdMvAttr(), "", "Usage: ")
        self.call(building.CmdMvAttr(), "Obj2/test2 = Obj/test3", "Moved Obj2.test2 -> Obj.test3")
        self.call(building.CmdCpAttr(), "", "Usage: ")
        self.call(building.CmdCpAttr(), "Obj/test1 = Obj2/test3", "Copied Obj.test1 -> Obj2.test3")

        self.call(building.CmdWipe(), "", "Usage: ")
        self.call(building.CmdWipe(), "Obj2/test2/test3", "Wiped attributes test2,test3 on Obj2.")
        self.call(building.CmdWipe(), "Obj2", "Wiped all attributes on Obj2.")

    def test_nested_attribute_commands(self):
        # list - adding white space proves real parsing
            "Obj/test1=[1,2]",
            "Created attribute Obj/test1 [category:None] = [1, 2]",
        )
            building.CmdSetAttribute(), "Obj/test1", "Attribute Obj/test1 [category:None] = [1, 2]"
        )
            building.CmdSetAttribute(), "Obj/test1[0]", "Attribute Obj/test1[0] [category:None] = 1"
        )
            building.CmdSetAttribute(), "Obj/test1[1]", "Attribute Obj/test1[1] [category:None] = 2"
        )
            "Obj/test1[0] = 99",
            "Modified attribute Obj/test1 [category:None] = [99, 2]",
        )
            "Obj/test1[0]",
            "Attribute Obj/test1[0] [category:None] = 99",
        )
        # list delete
            "Obj/test1[0] =",
            "Deleted attribute Obj/test1[0] [category:None].",
        )
            building.CmdSetAttribute(), "Obj/test1[0]", "Attribute Obj/test1[0] [category:None] = 2"
        )
            "Obj/test1[1]",
            "Attribute Obj/test1[1] [category:None] does not exist. (Nested lookups attempted)",
        )
        # Delete non-existent
            "Obj/test1[5] =",
            "No attribute Obj/test1[5] [category: None] was found to "
            "delete. (Nested lookups attempted)",
        )
        # Append
            "Obj/test1[+] = 42",
            "Modified attribute Obj/test1 [category:None] = [2, 42]",
        )
            "Obj/test1[+0] = -1",
            "Modified attribute Obj/test1 [category:None] = [-1, 2, 42]",
        )

        # dict - removing white space proves real parsing
            "Obj/test2={ 'one': 1, 'two': 2 }",
            "Created attribute Obj/test2 [category:None] = {'one': 1, 'two': 2}",
        )
            "Obj/test2",
            "Attribute Obj/test2 [category:None] = {'one': 1, 'two': 2}",
        )
            "Obj/test2['one']",
            "Attribute Obj/test2['one'] [category:None] = 1",
        )
            "Obj/test2['one]",
            "Attribute Obj/test2['one] [category:None] = 1",
        )
            "Obj/test2['one']=99",
            "Modified attribute Obj/test2 [category:None] = {'one': 99, 'two': 2}",
        )
            "Attribute Obj/test2['one'] [category:None] = 99",
        )
            "Obj/test2['two']",
            "Attribute Obj/test2['two'] [category:None] = 2",
        )
            "Obj/test2[+'three']",
            "Attribute Obj/test2[+'three'] [category:None] does not exist. (Nested lookups"
            " attempted)",
        )
            "Obj/test2[+'three'] = 3",
            "Modified attribute Obj/test2 [category:None] = {'one': 99, 'two': 2, \"+'three'\": 3}",
        )
            "Obj/test2[+'three'] =",
            "Deleted attribute Obj/test2[+'three'] [category:None].",
        )
            "Obj/test2['three']=3",
            "Modified attribute Obj/test2 [category:None] = {'one': 99, 'two': 2, 'three': 3}",
        )
        # Dict delete
            "Obj/test2['two'] =",
            "Deleted attribute Obj/test2['two'] [category:None].",
        )
            "Attribute Obj/test2['two'] [category:None] does not exist. (Nested lookups attempted)",
        )
            "Attribute Obj/test2 [category:None] = {'one': 99, 'three': 3}",
        )
            "Obj/test2[0]",
            "Attribute Obj/test2[0] [category:None] does not exist. (Nested lookups attempted)",
        )
            "Obj/test2['five'] =",
            "No attribute Obj/test2['five'] [category: None] "
            "was found to delete. (Nested lookups attempted)",
        )
            "Obj/test2[+]=42",
            "Modified attribute Obj/test2 [category:None] = {'one': 99, 'three': 3, '+': 42}",
        )
            "Obj/test2[+1]=33",
            "Modified attribute Obj/test2 [category:None] = "
            "{'one': 99, 'three': 3, '+': 42, '+1': 33}",
        )

        # dict - case sensitive keys

            "Obj/test_case = {'FooBar': 1}",
            "Created attribute Obj/test_case [category:None] = {'FooBar': 1}",
        )
            "Obj/test_case['FooBar'] = 2",
            "Modified attribute Obj/test_case [category:None] = {'FooBar': 2}",
        )
            "Obj/test_case",
            "Attribute Obj/test_case [category:None] = {'FooBar': 2}",
        )
            "Obj/test_case['FooBar'] = {'BarBaz': 1}",
            "Modified attribute Obj/test_case [category:None] = {'FooBar': {'BarBaz': 1}}",
        )
            "Obj/test_case['FooBar']['BarBaz'] = 2",
            "Modified attribute Obj/test_case [category:None] = {'FooBar': {'BarBaz': 2}}",
        )
            "Attribute Obj/test_case [category:None] = {'FooBar': {'BarBaz': 2}}",
        )

        # tuple
            "Obj/tup = (1,2)",
            "Created attribute Obj/tup [category:None] = (1, 2)",
        )
            "Obj/tup[1] = 99",
            "'tuple' object does not support item assignment - (1, 2)",
        )
            "Obj/tup[+] = 99",
        )
            "Obj/tup[+1] = 99",
        )
            # Special case for tuple, could have a better message
            "Obj/tup[1] = ",
            "No attribute Obj/tup[1] [category: None] "
        )

        # Deaper nesting
            "Obj/test3=[{'one': 1}]",
            "Created attribute Obj/test3 [category:None] = [{'one': 1}]",
        )
            "Obj/test3[0]['one']",
            "Attribute Obj/test3[0]['one'] [category:None] = 1",
        )
            "Obj/test3[0]",
            "Attribute Obj/test3[0] [category:None] = {'one': 1}",
        )
            "Obj/test3[0]['one'] =",
            "Deleted attribute Obj/test3[0]['one'] [category:None].",
        )
            "Attribute Obj/test3[0] [category:None] = {}",
        )
            building.CmdSetAttribute(), "Obj/test3", "Attribute Obj/test3 [category:None] = [{}]"
        )

        # Naughty keys
            "Obj/test4[0]='foo'",
            "Created attribute Obj/test4[0] [category:None] = foo",
        )
            "Obj/test4[0]",
            "Attribute Obj/test4[0] [category:None] = foo",
        )
            "Obj/test4=[{'one': 1}]",
            "Created attribute Obj/test4 [category:None] = [{'one': 1}]",
        )
            "Obj/test4[0]['one']",
            "Attribute Obj/test4[0]['one'] [category:None] = 1",
        )
        # Prefer nested items
            "Attribute Obj/test4[0] [category:None] = {'one': 1}",
        )
        )
        # Restored access
        self.call(building.CmdWipe(), "Obj/test4", "Wiped attributes test4 on Obj.")
        )
            "Attribute Obj/test4[0]['one'] [category:None] does not exist. (Nested lookups"
        )

    def test_split_nested_attr(self):
        split_nested_attr = building.CmdSetAttribute().split_nested_attr
        test_cases = {
            "test1": [("test1", [])],
            'test2["dict"]': [("test2", ["dict"]), ('test2["dict"]', [])],
            # Quotes not actually required
            "test3[dict]": [("test3", ["dict"]), ("test3[dict]", [])],
            'test4["dict]': [("test4", ["dict"]), ('test4["dict]', [])],
            # duplicate keys don't cause issues
            "test5[0][0]": [("test5", [0, 0]), ("test5[0]", [0]), ("test5[0][0]", [])],
            # String ints preserved
            'test6["0"][0]': [("test6", ["0", 0]), ('test6["0"]', [0]), ('test6["0"][0]', [])],
            # Unmatched []
            "test7[dict": [("test7[dict", [])],
        }

        for attr, result in test_cases.items():
            self.assertEqual(list(split_nested_attr(attr)), result)

    def test_do_nested_lookup(self):
        do_nested_lookup = building.CmdSetAttribute().do_nested_lookup
        not_found = building.CmdSetAttribute.not_found

        def do_test_single(value, key, result):
            self.assertEqual(do_nested_lookup(value, key), result)

        def do_test_multi(value, keys, result):
            self.assertEqual(do_nested_lookup(value, *keys), result)

        do_test_single([], "test1", not_found)
        do_test_single([1], "test2", not_found)
        do_test_single([], 0, not_found)
        do_test_single([], "0", not_found)
        do_test_single([1], 2, not_found)
        do_test_single([1], 0, 1)
        do_test_single([1], "0", not_found)  # str key is str not int
        do_test_single({}, "test3", not_found)
        do_test_single({}, 0, not_found)
        do_test_single({"foo": "bar"}, "foo", "bar")

        do_test_multi({"one": [1, 2, 3]}, ("one", 0), 1)
        do_test_multi([{}, {"two": 2}, 3], (1, "two"), 2)

    def test_name(self):
        self.call(building.CmdName(), "", "Usage: ")
        self.call(building.CmdName(), "Obj2=Obj3", "Object's name changed to 'Obj3'.")
            building.CmdName(),
            "*TestAccount=TestAccountRenamed",
            "Account's name changed to 'TestAccountRenamed'.",
        )
        self.call(building.CmdName(), "*NotFound=TestAccountRenamed", "Could not find '*NotFound'")
            building.CmdName(), "Obj3=Obj4;foo;bar", "Object's name changed to 'Obj4' (foo, bar)."
        )
        self.call(building.CmdName(), "Obj4=", "No names or aliases defined!")

    def test_desc(self):
        oid = self.obj2.id
            building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#{}).".format(oid)
        )
        self.call(building.CmdDesc(), "", "Usage: ")

            self.call(building.CmdDesc(), "/edit")
            mock_ed.assert_called_with(
                self.char1,
                key="desc",
                loadfunc=building._desc_load,
                quitfunc=building._desc_quit,
                savefunc=building._desc_save,
            )

    def test_empty_desc(self):
        """
        empty desc sets desc as ''
        """
        o2d = self.obj2.db.desc
        r1d = self.room1.db.desc
        self.call(building.CmdDesc(), "Obj2=", "The description was set on Obj2(#{}).".format(oid))
        assert self.obj2.db.desc == "" and self.obj2.db.desc != o2d
        assert self.room1.db.desc == r1d

    def test_desc_default_to_room(self):
        """no rhs changes room's desc"""
        self.call(building.CmdDesc(), "Obj2", "The description was set on Room(#{}).".format(rid))
        assert self.obj2.db.desc == o2d
        assert self.room1.db.desc == "Obj2" and self.room1.db.desc != r1d

    def test_destroy(self):
        confirm = building.CmdDestroy.confirm
        building.CmdDestroy.confirm = False
        self.call(building.CmdDestroy(), "", "Usage: ")
        self.call(building.CmdDestroy(), "Obj", "Obj was destroyed.")
        self.call(building.CmdDestroy(), "Obj", "Obj2 was destroyed.")
            building.CmdDestroy(),
            "Obj",
            "Could not find 'Obj'.| (Objects to destroy "
            "must either be local or specified with a unique #dbref.)",
        )
        settings.DEFAULT_HOME = f"#{self.room1.dbid}"
            building.CmdDestroy(), settings.DEFAULT_HOME, "You are trying to delete"
        )  # DEFAULT_HOME should not be deleted
        self.char2.location = self.room2
        charid = self.char2.id
        room1id = self.room1.id
        room2id = self.room2.id
            self.room2.dbref,
            "Char2(#{}) arrives to Room(#{}) from Room2(#{}).|Room2 was destroyed.".format(
                charid, room1id, room2id
            ),
        )
        building.CmdDestroy.confirm = confirm

    def test_destroy_sequence(self):
            "{}-{}".format(self.obj1.dbref, self.obj2.dbref),
            "Obj was destroyed.\nObj2 was destroyed.",
        )

    def test_dig(self):
        self.call(building.CmdDig(), "TestRoom1=testroom;tr,back;b", "Created room TestRoom1")
        self.call(building.CmdDig(), "", "Usage: ")

    def test_tunnel(self):
        self.call(building.CmdTunnel(), "n = TestRoom2;test2", "Created room TestRoom2")
        self.call(building.CmdTunnel(), "", "Usage: ")
        self.call(building.CmdTunnel(), "foo = TestRoom2;test2", "tunnel can only understand the")
        self.call(building.CmdTunnel(), "/tel e = TestRoom3;test3", "Created room TestRoom3")
        DefaultRoom.objects.get_family(db_key="TestRoom3")
        exits = DefaultExit.objects.filter_family(db_key__in=("east", "west"))
        self.assertEqual(len(exits), 2)

    def test_tunnel_exit_typeclass(self):
            building.CmdTunnel(),
            "n:evennia.objects.objects.DefaultExit = TestRoom3",
            "Created room TestRoom3",
        )

    def test_exit_commands(self):
            building.CmdOpen(), "TestExit1=Room2", "Created new Exit 'TestExit1' from Room to Room2"
        )
        self.call(building.CmdLink(), "TestExit1=Room", "Link created TestExit1 -> Room (one way).")
        self.call(building.CmdUnLink(), "", "Usage: ")
        self.call(building.CmdLink(), "NotFound", "Could not find 'NotFound'.")
        self.call(building.CmdLink(), "TestExit", "TestExit1 is an exit to Room.")
        self.call(building.CmdLink(), "Obj", "Obj is not an exit. Its home location is Room.")
            building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere."
        )

        self.char1.location = self.room2
            building.CmdOpen(), "TestExit2=Room", "Created new Exit 'TestExit2' from Room2 to Room."
        )
            building.CmdOpen(),
            "TestExit2=Room",
            "Exit TestExit2 already exists. It already points to the correct place.",
        )

        # ensure it matches locally first
            building.CmdLink(), "TestExit=Room2", "Link created TestExit2 -> Room2 (one way)."
        )
            building.CmdLink(),
            "/twoway TestExit={}".format(self.exit.dbref),
            "Link created TestExit2 (in Room2) <-> out (in Room) (two-way).",
        )
            "/twoway TestExit={}".format(self.room1.dbref),
            "To create a two-way link, TestExit2 and Room must both have a location ",
        )
            "/twoway {}={}".format(self.exit.dbref, self.exit.dbref),
            "Cannot link an object to itself.",
        )
        self.call(building.CmdLink(), "", "Usage: ")
        # ensure can still match globally when not a local name
        self.call(building.CmdLink(), "TestExit1=Room2", "Note: TestExit1")
            building.CmdLink(), "TestExit1=", "Former exit TestExit1 no longer links anywhere."
        )

    def test_set_home(self):
            building.CmdSetHome(), "Obj = Room2", "Home location of Obj was changed from Room"
        )
        self.call(building.CmdSetHome(), "", "Usage: ")
        self.call(building.CmdSetHome(), "self", "Char's current home is Room")
        self.call(building.CmdSetHome(), "Obj", "Obj's current home is Room2")
        self.obj1.home = None
        self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was set to Room")

    def test_list_cmdsets(self):
            building.CmdListCmdSets(),
            "",
            "<CmdSetHandler> stack:\n <CmdSet DefaultCharacter, Union, perm, prio 0>:",
        )
        self.call(building.CmdListCmdSets(), "NotFound", "Could not find 'NotFound'")

    def test_typeclass(self):
        self.call(building.CmdTypeclass(), "", "Usage: ")
            building.CmdTypeclass(),
            "Obj = evennia.objects.objects.DefaultExit",
            "Obj changed typeclass from evennia.objects.objects.DefaultObject "
            "to evennia.objects.objects.DefaultExit.",
        )
            "Obj2 = evennia.objects.objects.DefaultExit",
            "Obj2 changed typeclass from evennia.objects.objects.DefaultObject "
            cmdstring="swap",
            inputs=["yes"],
        )
        self.call(building.CmdTypeclass(), "/list Obj", "Core typeclasses")
            "/show Obj",
            "Obj's current typeclass is 'evennia.objects.objects.DefaultExit'",
        )
            "Obj already has the typeclass 'evennia.objects.objects.DefaultExit'. Use /force to"
            " override.",
        )
            "/force Obj = evennia.objects.objects.DefaultExit",
            "Obj updated its existing typeclass ",
        )
        self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultObject")
            "Obj's current typeclass is 'evennia.objects.objects.DefaultObject'",
        )
            "Obj",
            "Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\nOnly the"
            " at_object_creation hook was run (update mode). Attributes set before swap were not"
            " removed\n(use `swap` or `type/reset` to clear all).",
            cmdstring="update",
        )
            "/reset/force Obj=evennia.objects.objects.DefaultObject",
            "Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n"
            "All object creation hooks were run. All old attributes where deleted before the swap.",
        )

        from evennia.prototypes.prototypes import homogenize_prototype

        test_prototype = [
            homogenize_prototype(
                {
                    "prototype_key": "testkey",
                    "prototype_tags": [],
                    "typeclass": "typeclasses.objects.Object",
                    "key": "replaced_obj",
                    "attrs": [("foo", "bar", None, ""), ("desc", "protdesc", None, "")],
                }
            )
        ]
        with patch(
            "evennia.commands.default.building.protlib.search_prototype",
            new=MagicMock(return_value=test_prototype),
        ) as mprot:
            self.call(
                building.CmdTypeclass(),
                "/prototype Obj=testkey",
                "replaced_obj changed typeclass from evennia.objects.objects.DefaultObject to "
                "typeclasses.objects.Object.\nOnly the at_object_creation hook was run "
                "(update mode). Attributes set before swap were not removed\n"
                "(use `swap` or `type/reset` to clear all). Prototype 'replaced_obj' was "
                "successfully applied over the object type.",
            )
            assert self.obj1.db.desc == "protdesc"

    def test_lock(self):
        self.call(building.CmdLock(), "", "Usage: ")
        self.call(building.CmdLock(), "Obj = test:all()", "Added lock 'test:all()' to Obj.")
            building.CmdLock(),
            "*TestAccount = test:all()",
            "Added lock 'test:all()' to TestAccount",
        )
        self.call(building.CmdLock(), "Obj/notfound", "Obj has no lock of access type 'notfound'.")
        self.call(building.CmdLock(), "Obj/test", "test:all()")
            "/view Obj = edit:false()",
            "Switch(es) view can not be used with a lock assignment. "
            "Use e.g. lock/del objname/locktype instead.",
        )
        self.call(building.CmdLock(), "Obj = control:false()")
        self.call(building.CmdLock(), "Obj = edit:false()")
        self.call(building.CmdLock(), "Obj/test", "You are not allowed to do that.")
        self.obj1.locks.add("control:true()")
        self.call(building.CmdLock(), "Obj", "call:true()")  # etc
        self.call(building.CmdLock(), "*TestAccount", "boot:perm(Admin)")  # etc

    def test_find(self):
        rid2 = self.room2.id
        rmax = rid2 + 100
        self.call(building.CmdFind(), "", "Usage: ")
        self.call(building.CmdFind(), "oom2", "One Match")
        self.call(building.CmdFind(), "oom2 = 1-{}".format(rmax), "One Match")
        self.call(building.CmdFind(), "oom2 = 1 {}".format(rmax), "One Match")  # space works too
        self.call(building.CmdFind(), "Char2", "One Match", cmdstring="locate")
            building.CmdFind(),
            "/ex Char2",  # /ex is an ambiguous switch
            "locate: Ambiguous switch supplied: Did you mean /exit or /exact?|",
            cmdstring="locate",
        )
            building.CmdFind(), "/l Char2", "One Match", cmdstring="find"
        )  # /l switch is abbreviated form of /loc
        self.call(building.CmdFind(), "Char2", "One Match", cmdstring="find")
        self.call(building.CmdFind(), "/startswith Room2", "One Match")

        self.call(building.CmdFind(), self.char1.dbref, "Exact dbref match")
        self.call(building.CmdFind(), "*TestAccount", "Match")

        self.call(building.CmdFind(), "/char Obj", "No Matches")
        self.call(building.CmdFind(), "/room Obj", "No Matches")
        self.call(building.CmdFind(), "/exit Obj", "No Matches")
        self.call(building.CmdFind(), "/exact Obj", "One Match")

        # Test multitype filtering
            "evennia.commands.default.building.CHAR_TYPECLASS",
            "evennia.objects.objects.DefaultCharacter",
        ):
            self.call(building.CmdFind(), "/char/room Obj", "No Matches")
            self.call(building.CmdFind(), "/char/room/exit Char", "2 Matches")
            self.call(building.CmdFind(), "/char/room/exit/startswith Cha", "2 Matches")

        # Test null search
        self.call(building.CmdFind(), "=", "Usage: ")

        # Test bogus dbref range with no search term
        self.call(building.CmdFind(), "= obj", "Invalid dbref range provided (not a number).")
        self.call(building.CmdFind(), "= #1a", "Invalid dbref range provided (not a number).")

        # Test valid dbref ranges with no search term
        id1 = self.obj1.id
        id2 = self.obj2.id
        maxid = ObjectDB.objects.latest("id").id
        maxdiff = maxid - id1 + 1
        mdiff = id2 - id1 + 1

        self.call(building.CmdFind(), f"=#{id1}", f"{maxdiff} Matches(#{id1}-#{maxid}")
        self.call(building.CmdFind(), f"={id1}-{id2}", f"{mdiff} Matches(#{id1}-#{id2}):")
        self.call(building.CmdFind(), f"={id1} - {id2}", f"{mdiff} Matches(#{id1}-#{id2}):")
        self.call(building.CmdFind(), f"={id1}- #{id2}", f"{mdiff} Matches(#{id1}-#{id2}):")
        self.call(building.CmdFind(), f"={id1}-#{id2}", f"{mdiff} Matches(#{id1}-#{id2}):")
        self.call(building.CmdFind(), f"=#{id1}-{id2}", f"{mdiff} Matches(#{id1}-#{id2}):")

    def test_script(self):
        self.call(building.CmdScripts(), "Obj =", "No scripts defined on Obj")
            building.CmdScripts(),
            "Obj = scripts.scripts.DefaultScript",
            "Script scripts.scripts.DefaultScript successfully added",
        )
        self.call(building.CmdScripts(), "evennia.Dummy", "Global Script NOT Created ")
            "evennia.scripts.scripts.DoNothing",
            "Global Script Created - sys_do_nothing ",
        )
        self.call(building.CmdScripts(), "Obj =", "dbref ")

            building.CmdScripts(), "/start Obj = ", "Script on Obj Started "
        )  # we allow running start again; this should still happen
        self.call(building.CmdScripts(), "/stop Obj =", "Script on Obj Stopped - ")

            inputs=["Y"],
        )
            "/start Obj = scripts.scripts.DefaultScript",
            "Script on Obj Started ",
        )
            "/stop Obj = scripts.scripts.DefaultScript",
            "Script on Obj Stopped ",
        )
            "/delete Obj = scripts.scripts.DefaultScript",
            "Script on Obj Deleted ",
        )
            "/delete evennia.scripts.scripts.DoNothing",
            "Global Script Deleted -",
        )

    def test_script_multi_delete(self):
        script1 = create.create_script()
        script2 = create.create_script()
        script3 = create.create_script()

            "/delete #{}-#{}".format(script1.id, script3.id),
            f"Global Script Deleted - #{script1.id} (evennia.scripts.scripts.DefaultScript)|"
            f"Global Script Deleted - #{script2.id} (evennia.scripts.scripts.DefaultScript)|"
            f"Global Script Deleted - #{script3.id} (evennia.scripts.scripts.DefaultScript)",
            inputs=["y"],
        )
        self.assertFalse(script1.pk)
        self.assertFalse(script2.pk)
        self.assertFalse(script3.pk)

    def test_teleport(self):
        self.call(building.CmdTeleport(), "", "Usage: ")
        self.call(building.CmdTeleport(), "Obj = Room", "Obj is already at Room.")
            building.CmdTeleport(),
            "Obj = NotFound",
            "Could not find 'NotFound'.|Destination not found.",
        )
            "Obj = Room2",
            "Obj(#{}) is leaving Room(#{}), heading for Room2(#{}).|Teleported Obj -> Room2.".format(
                oid, rid, rid2
            ),
        )
        self.call(building.CmdTeleport(), "NotFound = Room", "Could not find 'NotFound'.")
            building.CmdTeleport(), "Obj = Obj", "You can't teleport an object inside of itself!"
        )

        self.call(building.CmdTeleport(), "/tonone Obj2", "Teleported Obj2 -> None-location.")
        self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#{})".format(rid2))
            "/t",  # /t switch is abbreviated form of /tonone
            "Cannot teleport a puppeted object (Char, puppeted by TestAccount",
        )
            "/l Room2",  # /l switch is abbreviated form of /loc
            "Destination has no location.",
        )
            "/q me to Room2",  # /q switch is abbreviated form of /quiet
            "Char is already at Room2.",
        )

    def test_tag(self):
        self.call(building.CmdTag(), "", "Usage: ")

        self.call(building.CmdTag(), "Obj = testtag")
        self.call(building.CmdTag(), "Obj = testtag2")
        self.call(building.CmdTag(), "Obj = testtag2:category1")
        self.call(building.CmdTag(), "Obj = testtag3")

            building.CmdTag(),
            "Obj",
            "Tags on Obj: 'testtag', 'testtag2', 'testtag2' (category: category1), 'testtag3'",
        )

        self.call(building.CmdTag(), "/search NotFound", "No objects found with tag 'NotFound'.")
        self.call(building.CmdTag(), "/search testtag", "Found 1 object with tag 'testtag':")
        self.call(building.CmdTag(), "/search testtag2", "Found 1 object with tag 'testtag2':")
            "/search testtag2:category1",
            "Found 1 object with tag 'testtag2' (category: 'category1'):",
        )

        self.call(building.CmdTag(), "/del Obj = testtag3", "Removed tag 'testtag3' from Obj.")
            "/del Obj",
            "Cleared all tags from Obj: testtag, testtag2, testtag2 (category: category1)",
        )

    def test_spawn(self):
        def get_object(commandTest, obj_key):
            # A helper function to get a spawned object and
            # check that it exists in the process.
            query = search_object(obj_key)
            commandTest.assertIsNotNone(query)
            commandTest.assertTrue(bool(query))
            obj = query[0]
            commandTest.assertIsNotNone(obj)
            return obj

        # Tests "spawn" without any arguments.
        self.call(building.CmdSpawn(), " ", "Usage: spawn")

        # Tests "spawn <prototype_dictionary>" without specifying location.

            building.CmdSpawn(),
            "/save {'prototype_key': 'testprot', 'key':'Test Char', "
            "'typeclass':'evennia.objects.objects.DefaultCharacter'}",
            "Saved prototype: testprot",
        )

            "/save testprot2 = {'key':'Test Char', "
            "(Replacing `prototype_key` in prototype with given key.)|Saved prototype: testprot2",
        )

        self.call(building.CmdSpawn(), "/search ", "Key ")
        self.call(building.CmdSpawn(), "/search test;test2", "No prototypes found.")

            "/save {'key':'Test Char', 'typeclass':'evennia.objects.objects.DefaultCharacter'}",
            "A prototype_key must be given, either as `prototype_key = <prototype>` or as "
            "a key 'prototype_key' inside the prototype structure.",
        )

        self.call(building.CmdSpawn(), "/list", "Key ")
        self.call(building.CmdSpawn(), "testprot", "Spawned Test Char")

        # Tests that the spawned object's location is the same as the character's location, since
        # we did not specify it.
        testchar = get_object(self, "Test Char")
        self.assertEqual(testchar.location, self.char1.location)
        testchar.delete()

        # Test "spawn <prototype_dictionary>" with a location other than the character's.
        spawnLoc = self.room2
        if spawnLoc == self.char1.location:
            # Just to make sure we use a different location, in case someone changes
            # char1's default location in the future...
            spawnLoc = self.room1

            "{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', "
            "'key':'goblin', 'location':'%s'}" % spawnLoc.dbref,
            "Spawned goblin",
        )
        goblin = get_object(self, "goblin")
        # Tests that the spawned object's type is a DefaultCharacter.
        self.assertIsInstance(goblin, DefaultCharacter)
        self.assertEqual(goblin.location, spawnLoc)

        goblin.delete()

        # create prototype
        protlib.create_prototype(
            {
                "key": "Ball",
                "typeclass": "evennia.objects.objects.DefaultCharacter",
                "prototype_key": "testball",
            }
        )

        # Tests "spawn <prototype_name>"
        self.call(building.CmdSpawn(), "testball", "Spawned Ball")

        ball = get_object(self, "Ball")
        self.assertEqual(ball.location, self.char1.location)
        self.assertIsInstance(ball, DefaultObject)
        ball.delete()

        # Tests "spawn/n ..." without specifying a location.
        # Location should be "None".
            building.CmdSpawn(), "/n 'BALL'", "Spawned Ball"
        )  # /n switch is abbreviated form of /noloc
        self.assertIsNone(ball.location)

            "/noloc {'prototype_parent':'TESTBALL', 'prototype_key': 'testball', 'location':'%s'}"
            % spawnLoc.dbref,
            "Error: Prototype testball tries to parent itself.",
        )

        # Tests "spawn/noloc ...", but DO specify a location.
        # Location should be the specified location.
            "/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo',"
            " 'location':'%s'}" % spawnLoc.dbref,
            "Spawned Ball",
        )
        self.assertEqual(ball.location, spawnLoc)

        # test calling spawn with an invalid prototype.
        self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST' was found.")

        # Test listing commands

        # spawn/edit (missing prototype)
        # brings up olc menu
        msg = self.call(building.CmdSpawn(), "/edit")
        assert "Prototype wizard" in msg

        # spawn/edit with valid prototype
        # brings up olc menu loaded with prototype
        msg = self.call(building.CmdSpawn(), "/edit testball")
        assert hasattr(self.char1.ndb._menutree, "olc_prototype")
        assert (
            dict == type(self.char1.ndb._menutree.olc_prototype)
            and "prototype_key" in self.char1.ndb._menutree.olc_prototype
            and "key" in self.char1.ndb._menutree.olc_prototype
            and "testball" == self.char1.ndb._menutree.olc_prototype["prototype_key"]
            and "Ball" == self.char1.ndb._menutree.olc_prototype["key"]
        )
        assert "Ball" in msg and "testball" in msg

        # spawn/edit with valid prototype (synomym)
        msg = self.call(building.CmdSpawn(), "/edit BALL")

        # spawn/edit with invalid prototype
        msg = self.call(
            building.CmdSpawn(), "/edit NO_EXISTS", "No prototype named 'NO_EXISTS' was found."
        )

        # spawn/examine (missing prototype)
        # lists all prototypes that exist
        self.call(building.CmdSpawn(), "/examine", "You need to specify a prototype-key to show.")

        # spawn/examine with valid prototype
        # prints the prototype
        msg = self.call(building.CmdSpawn(), "/examine BALL")

        # spawn/examine with invalid prototype
        # shows error
            building.CmdSpawn(), "/examine NO_EXISTS", "No prototype named 'NO_EXISTS' was found."
        )

import evennia.commands.default.comms as cmd_comms  # noqa
from evennia.comms.comms import DefaultChannel  # noqa
from evennia.utils.create import create_channel  # noqa

@patch("evennia.commands.default.comms.CHANNEL_DEFAULT_TYPECLASS", DefaultChannel)
class TestCommsChannel(BaseEvenniaCommandTest):
    """
    Test the central `channel` command.

    """

        self.channel = create_channel(key="testchannel", desc="A test channel")
        self.channel.connect(self.char1)
        self.cmdchannel = cmd_comms.CmdChannel
        self.cmdchannel.account_caller = False

        if self.channel.pk:
            self.channel.delete()

    # test channel command
    def test_channel__noarg(self):
        self.call(self.cmdchannel(), "", "Channel subscriptions")

    def test_channel__msg(self):
        self.channel.msg = Mock()
        self.call(self.cmdchannel(), "testchannel = Test message", "")
        self.channel.msg.assert_called_with("Test message", senders=self.char1)

    def test_channel__list(self):
        self.call(self.cmdchannel(), "/list", "Channel subscriptions")

    def test_channel__all(self):
        self.call(self.cmdchannel(), "/all", "Available channels")

    def test_channel__history(self):
        with patch("evennia.commands.default.comms.tail_log_file") as mock_tail:
            self.call(self.cmdchannel(), "/history testchannel", "")
            mock_tail.assert_called()

    def test_channel__sub(self):
        self.channel.disconnect(self.char1)

        self.call(self.cmdchannel(), "/sub testchannel", "You are now subscribed")
        self.assertTrue(self.char1 in self.channel.subscriptions.all())
            self.char1.nicks.nickreplace("testchannel Hello"), "@channel testchannel = Hello"
        )

    def test_channel__unsub(self):
        self.call(self.cmdchannel(), "/unsub testchannel", "You un-subscribed")
        self.assertFalse(self.char1 in self.channel.subscriptions.all())

    def test_channel__alias__unalias(self):
        """Add and then remove a channel alias"""

        # add alias
            self.cmdchannel(),
            "/alias testchannel = foo",
            "Added/updated your alias 'foo' for channel testchannel.",
        )
        self.assertEqual(self.char1.nicks.nickreplace("foo Hello"), "@channel testchannel = Hello")

        # use alias
        self.call(self.cmdchannel(), "foo = test message", "")
        self.channel.msg.assert_called_with("test message", senders=self.char1)

        # remove alias
        self.call(self.cmdchannel(), "/unalias foo", "Removed your channel alias 'foo'")
        self.assertEqual(self.char1.nicks.get("foo $1", category="channel"), None)

    def test_channel__mute(self):
        self.call(self.cmdchannel(), "/mute testchannel", "Muted channel testchannel")
        self.assertTrue(self.char1 in self.channel.mutelist)

    def test_channel__unmute(self):
        self.channel.mute(self.char1)

        self.call(self.cmdchannel(), "/unmute testchannel = Char1", "Un-muted channel testchannel")
        self.assertFalse(self.char1 in self.channel.mutelist)

    def test_channel__create(self):
        self.call(self.cmdchannel(), "/create testchannel2", "Created (and joined) new channel")

    def test_channel__destroy(self):
            "/destroy testchannel = delete reason",
            "Are you sure you want to delete channel ",
            inputs=["Yes"],
        )
        self.channel.msg.assert_called_with("delete reason", bypass_mute=True, senders=self.char1)

    def test_channel__desc(self):
            "/desc testchannel = Another description",
            "Updated channel description.",
        )

    def test_channel__lock(self):
            self.cmdchannel(), "/lock testchannel = foo:false()", "Added/updated lock on channel"
        )
        self.assertEqual(self.channel.locks.get("foo"), "foo:false()")

    def test_channel__unlock(self):
        self.channel.locks.add("foo:true()")
        self.call(self.cmdchannel(), "/unlock testchannel = foo", "Removed lock from channel")
        self.assertEqual(self.channel.locks.get("foo"), "")

    def test_channel__boot(self):
        self.channel.connect(self.char2)
        self.assertTrue(self.char2 in self.channel.subscriptions.all())

            "/boot testchannel = Char2 : Booting from channel!",
            "Are you sure ",
        )
        self.channel.msg.assert_called_with(
            "Char2 was booted from channel by Char. Reason: Booting from channel!"
        )
            "You were booted from channel testchannel by Char. Reason: Booting from channel!"
        )

    def test_channel__ban__unban(self):
        """Test first ban and then unban"""

        # ban

            "/ban testchannel = Char2 : Banning from channel!",
        )
            "Char2 was booted from channel by Char. Reason: Banning from channel!"
        )
            "You were booted from channel testchannel by Char. Reason: Banning from channel!"
        )
        self.assertTrue(self.char2 in self.channel.banlist)

        # unban

            "/unban testchannel = Char2",
            "Un-banned Char2 from channel testchannel",
        )
        self.assertFalse(self.char2 in self.channel.banlist)

    def test_channel__who(self):
        self.call(self.cmdchannel(), "/who testchannel", "Subscribed to testchannel:\nChar")

from evennia.commands.default import comms  # noqa

class TestComms(BaseEvenniaCommandTest):
    def test_page(self):
            comms.CmdPage(),
            "TestAccount2 = Test",
            "TestAccount2 is offline. They will see your message if they list their pages later."
            "|You paged TestAccount2 with: 'Test'.",
        )

@override_settings(DISCORD_BOT_TOKEN="notarealtoken", DISCORD_ENABLED=True)
class TestDiscord(BaseEvenniaCommandTest):
        self.channel = create.create_channel(key="testchannel", desc="A test channel")
        self.cmddiscord = cmd_comms.CmdDiscord2Chan
        self.cmddiscord.account_caller = False
        # create bot manually so it doesn't get started
        self.discordbot = create.create_account(
            "DiscordBot", None, None, typeclass="evennia.accounts.bots.DiscordBot"
        )

        [
            ("", "No Discord connections found."),
            ("/list", "No Discord connections found."),
            ("/guild", "Messages to Evennia will include the Discord server."),
            ("/channel", "Relayed messages will include the originating channel."),
        ]
    )
    def test_discord__switches(self, cmd_args, expected):
        self.call(self.cmddiscord(), cmd_args, expected)

    def test_discord__linking(self):
            self.cmddiscord(), "nosuchchannel = 5555555", "There is no channel 'nosuchchannel'"
        )
            self.cmddiscord(),
            "testchannel = 5555555",
            "Discord connection created: testchannel <-> #5555555",
        )
        self.assertTrue(self.discordbot in self.channel.subscriptions.all())
        self.assertTrue(("testchannel", "5555555") in self.discordbot.db.channels)
        self.call(self.cmddiscord(), "testchannel = 5555555", "Those channels are already linked.")

    def test_discord__list(self):
        self.discordbot.db.channels = [("testchannel", "5555555")]
        cmdobj = self.cmddiscord()
        cmdobj.msg = lambda text, **kwargs: setattr(self, "out", str(text))
        self.call(cmdobj, "", None)
        self.assertIn("testchannel", self.out)
        self.assertIn("5555555", self.out)
        self.call(cmdobj, "testchannel", None)

class TestBatchProcess(BaseEvenniaCommandTest):
    """
    Test the batch processor.

    """

    # there is some sort of issue with the mock; it needs to loaded once to work
    from evennia.contrib.tutorials.red_button import red_button  # noqa

    @patch("evennia.contrib.tutorials.red_button.red_button.repeat")
    @patch("evennia.contrib.tutorials.red_button.red_button.delay")
    def test_batch_commands(self, mock_tutorials, mock_repeat):
        # cannot test batchcode here, it must run inside the server process
            batchprocess.CmdBatchCommands(),
            "batchprocessor.example_batch_cmds_test",
            "Running Batch-command processor - Automatic mode for"
            " batchprocessor.example_batch_cmds",
        )
        # we make sure to delete the button again here to stop the running reactor
        self.call(building.CmdDestroy(), "button", "button was destroyed.")
        mock_repeat.assert_called()

class CmdInterrupt(Command):
    key = "interrupt"

        raise InterruptCommand

        self.msg("in func")

class TestInterruptCommand(BaseEvenniaCommandTest):
    def test_interrupt_command(self):
        ret = self.call(CmdInterrupt(), "")
        self.assertEqual(ret, "")

class TestUnconnectedCommand(BaseEvenniaCommandTest):
    def test_info_command(self):
        # instead of using SERVER_START_TIME (0), we use 86400 because Windows won't let us use anything lower
        gametime.SERVER_START_TIME = 86400
        expected = (
            "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END"
            " INFO"
            % (
                settings.SERVERNAME,
                datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(),
                evennia.SESSION_HANDLER.account_count(),
                utils.get_evennia_version(),
            )
        )
        self.call(unloggedin.CmdUnconnectedInfo(), "", expected)
        del gametime.SERVER_START_TIME

    @override_settings(NEW_ACCOUNT_REGISTRATION_ENABLED=False)
    def test_disabled_registration(self):
            unloggedin.CmdUnconnectedCreate(),
            "testacct testpass",
            "Registration is currently disabled.",
        )

# Test syscommands

class TestSystemCommands(BaseEvenniaCommandTest):
    def test_simple_defaults(self):
        self.call(syscommands.SystemNoInput(), "")
        self.call(syscommands.SystemNoMatch(), "Huh?")

    def test_multimatch(self):
        # set up fake matches and store on command instance
        cmdset.add(general.CmdLook())
        matches = cmdparser.build_matches("look", cmdset)

        multimatch = syscommands.SystemMultimatch()
        multimatch.matches = matches

        self.call(multimatch, "look", "")

"""
Commands that are available from the connect screen.

"""
import re

from evennia.commands.cmdhandler import CMD_LOGINSTART
from evennia.utils import class_from_module, create, gametime, logger, utils

__all__ = (
    "CmdUnconnectedConnect",
    "CmdUnconnectedCreate",
    "CmdUnconnectedQuit",
    "CmdUnconnectedLook",
    "CmdUnconnectedHelp",
    "CmdUnconnectedEncoding",
    "CmdUnconnectedInfo",
    "CmdUnconnectedScreenreader",
)

CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE

def create_guest_account(session):
    """
    Creates a guest account/character for this session, if one is available.

    Args:
        session (Session): the session which will use the guest account/character.

    Returns:
        GUEST_ENABLED (boolean), account (Account):
            the boolean is whether guest accounts are enabled at all.
            the Account which was created from an available guest name.
    """
    enabled = settings.GUEST_ENABLED
    address = session.address

    # Get account class
    Guest = class_from_module(settings.BASE_GUEST_TYPECLASS)

    # Get an available guest account
    # authenticate() handles its own throttling
    account, errors = Guest.authenticate(ip=address)
    if account:
        return enabled, account
    else:
        session.msg("|R%s|n" % "\n".join(errors))
        return enabled, None

def create_normal_account(session, name, password):
    """
    Creates an account with the given name and password.

    Args:
        session (Session): the session which is requesting to create an account.
        name (str): the name that the account wants to use for login.
        password (str): the password desired by this account, for login.

    Returns:
        account (Account): the account which was created from the name and password.
    """
    Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)

    # Match account name and check password
    # authenticate() handles all its own throttling
    account, errors = Account.authenticate(
        username=name, password=password, ip=address, session=session
    )
    if not account:
        # No accountname or password match

    return account

class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
    """
    connect to the game

    Usage (at login screen):
      connect accountname password
      connect "account name" "pass word"

    Use the create command to first create an account before logging in.

    If you have spaces in your name, enclose it in double quotes.
    """

    key = "connect"
    aliases = ["conn", "con", "co"]
    locks = "cmd:all()"  # not really needed
    arg_regex = r"\s.*?|$"

        """
        Uses the Django admin api. Note that unlogged-in commands
        have a unique position in that their func() receives
        a session object instead of a source_object like all
        other types of logged-in commands (this is because
        there is no object yet before the account has logged in)
        """
        session = self.caller
        address = session.address

        # extract double quote parts
        parts = [part.strip() for part in re.split(r"\"", args) if part.strip()]
        if len(parts) == 1:
            # this was (hopefully) due to no double quotes being found, or a guest login
            parts = parts[0].split(None, 1)

            # Guest login
            if len(parts) == 1 and parts[0].lower() == "guest":
                # Get Guest typeclass
                Guest = class_from_module(settings.BASE_GUEST_TYPECLASS)

                account, errors = Guest.authenticate(ip=address)
                    session.sessionhandler.login(session, account)
                    return
                else:
                    session.msg("|R%s|n" % "\n".join(errors))
                    return

        if len(parts) != 2:
            session.msg("\n\r Usage (without <>): connect <name> <password>")
            return

        # Get account class
        Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)

        name, password = parts
        account, errors = Account.authenticate(
            username=name, password=password, ip=address, session=session
        )
        if account:
            session.sessionhandler.login(session, account)
        else:
            session.msg("|R%s|n" % "\n".join(errors))

class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
    """
    create a new account account

      create <accountname> <password>
      create "account name" "pass word"

    This creates a new account account.

    """

    key = "create"
    aliases = ["cre", "cr"]

        """Verify that account creation is enabled."""
        if not settings.NEW_ACCOUNT_REGISTRATION_ENABLED:
            # truthy return cancels the command
            self.msg("Registration is currently disabled.")

        return super().at_pre_cmd()

        """Do checks and create account"""

        args = self.args.strip()

        # extract double quoted parts
            # this was (hopefully) due to no quotes being found
            string = (
                "\n Usage (without <>): create <name> <password>"
                "\nIf <name> or <password> contains spaces, enclose it in double quotes."
            )
            session.msg(string)
            return

        username, password = parts

        # pre-normalize username so the user know what they get
        non_normalized_username = username
        username = Account.normalize_username(username)
        if non_normalized_username != username:
            session.msg(
                "Note: your username was normalized to strip spaces and remove characters "
                "that could be visually confusing."
            )

        # have the user verify their new account was what they intended
        answer = yield (
            f"You want to create an account '{username}' with password '{password}'."
            "\nIs this what you intended? [Y]/N?"
        )
        if answer.lower() in ("n", "no"):
            session.msg("Aborted. If your user name contains spaces, surround it by quotes.")
            return

        # everything's ok. Create the new player account.
        account, errors = Account.create(
            username=username, password=password, ip=address, session=session
        )
            # tell the caller everything went well.
            string = "A new account '%s' was created. Welcome!"
            if " " in username:
                string += (
                    "\n\nYou can now log in with the command 'connect \"%s\" <your password>'."
                )
            else:
                string += "\n\nYou can now log with the command 'connect %s <your password>'."
            session.msg(string % (username, username))
        else:

class CmdUnconnectedQuit(COMMAND_DEFAULT_CLASS):
    """
    quit when in unlogged-in state

    Usage:
      quit

    We maintain a different version of the quit command
    here for unconnected accounts for the sake of simplicity. The logged in
    version is a bit more complicated.
    """

    aliases = ["q", "qu"]

        """Simply close the connection."""
        session.sessionhandler.disconnect(session, "Good bye! Disconnecting.")

class CmdUnconnectedLook(COMMAND_DEFAULT_CLASS):
    """
    look when in unlogged-in state

    Usage:
      look

    This is an unconnected version of the look command for simplicity.

    This is called by the server and kicks everything in gear.
    All it does is display the connect screen.
    """

    key = CMD_LOGINSTART
    aliases = ["look", "l"]

        """Show the connect screen."""

        callables = utils.callables_from_module(CONNECTION_SCREEN_MODULE)
        if "connection_screen" in callables:
            connection_screen = callables["connection_screen"]()
        else:
            connection_screen = utils.random_string_from_module(CONNECTION_SCREEN_MODULE)
            if not connection_screen:
                connection_screen = "No connection screen found. Please contact an admin."
        self.msg(connection_screen)

class CmdUnconnectedHelp(COMMAND_DEFAULT_CLASS):
    """
    get help when in unconnected-in state

    Usage:
      help

    This is an unconnected version of the help command,
    for simplicity. It shows a pane of info.
    """

    aliases = ["h", "?"]

        """Shows help"""

You are not yet logged into the game. Commands available at this point:

  |wcreate|n - create a new account
  |wconnect|n - connect with an existing account
  |wlook|n - re-show the connection screen
  |whelp|n - show this help
  |wencoding|n - change the text encoding to match your client
  |wscreenreader|n - make the server more suitable for use with screen readers
  |wquit|n - abort the connection

First create an account e.g. with |wcreate Anna c67jHL8p|n
(If you have spaces in your name, use double quotes: |wcreate "Anna the Barbarian" c67jHL8p|n
Next you can connect to the game: |wconnect Anna c67jHL8p|n

You can use the |wlook|n command if you want to see the connect screen again.

"""

        if settings.STAFF_CONTACT_EMAIL:
            string += "For support, please contact: %s" % settings.STAFF_CONTACT_EMAIL

class CmdUnconnectedEncoding(COMMAND_DEFAULT_CLASS):
    """
    set which text encoding to use in unconnected-in state

    Usage:
      encoding/switches [<encoding>]

    Switches:
      clear - clear your custom encoding

    This sets the text encoding for communicating with Evennia. This is mostly
    an issue only if you want to use non-ASCII characters (i.e. letters/symbols
    not found in English). If you see that your characters look strange (or you
    get encoding errors), you should use this command to set the server
    encoding to be the same used in your client program.

    Common encodings are utf-8 (default), latin-1, ISO-8859-1 etc.

    If you don't submit an encoding, the current encoding will be displayed
    instead.
    """

    key = "encoding"
    aliases = "encode"

        """
        Sets the encoding.
        """

            return

        sync = False
        if "clear" in self.switches:
            # remove customization
            old_encoding = self.session.protocol_flags.get("ENCODING", None)
            if old_encoding:
                string = "Your custom text encoding ('%s') was cleared." % old_encoding
            else:
                string = "No custom encoding was set."
            self.session.protocol_flags["ENCODING"] = "utf-8"
            sync = True
        elif not self.args:
            # just list the encodings supported
            pencoding = self.session.protocol_flags.get("ENCODING", None)
            string = ""
            if pencoding:
                string += (
                    "Default encoding: |g%s|n (change with |wencoding <encoding>|n)" % pencoding
                )
            encodings = settings.ENCODINGS
            if encodings:
                string += (
                    "\nServer's alternative encodings (tested in this order):\n   |g%s|n"
                    % ", ".join(encodings)
                )
            if not string:
                string = "No encodings found."
        else:
            # change encoding
            encoding = self.args
            try:
                codecs_lookup(encoding)
                string = (
                    "|rThe encoding '|w%s|r' is invalid. Keeping the previous encoding '|w%s|r'.|n"
                    % (encoding, old_encoding)
                )
            else:
                self.session.protocol_flags["ENCODING"] = encoding
                string = "Your custom text encoding was changed from '|w%s|n' to '|w%s|n'." % (
                    old_encoding,
                    encoding,
                )
                sync = True
        if sync:
            self.session.sessionhandler.session_portal_sync(self.session)
        self.msg(string.strip())

class CmdUnconnectedScreenreader(COMMAND_DEFAULT_CLASS):
    """
    Activate screenreader mode.

    Usage:
        screenreader

    Used to flip screenreader mode on and off before logging in (when
    logged in, use option screenreader on).
    """

    key = "screenreader"

        """Flips screenreader setting."""
        new_setting = not self.session.protocol_flags.get("SCREENREADER", False)
        self.session.protocol_flags["SCREENREADER"] = new_setting
        string = "Screenreader mode turned |w%s|n." % ("on" if new_setting else "off")
        self.session.sessionhandler.session_portal_sync(self.session)

class CmdUnconnectedInfo(COMMAND_DEFAULT_CLASS):
    """
    Provides MUDINFO output, so that Evennia games can be added to Mudconnector
    and Mudstats.  Sadly, the MUDINFO specification seems to have dropped off the
    face of the net, but it is still used by some crawlers.  This implementation
    was created by looking at the MUDINFO implementation in MUX2, TinyMUSH, Rhost,
    and PennMUSH.
    """

    key = "info"

        self.msg(
            " INFO"
            % (
            )
        )

"""
Base typeclass for in-game Channels.

"""
import re

from django.contrib.contenttypes.models import ContentType

from evennia.comms.managers import ChannelManager
from evennia.utils import create, logger
from evennia.utils.utils import make_iter, inherits_from

class DefaultChannel(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()`. It is used
      in front of every channel message; use `{channelmessage}` token to insert the
      name of the current channel. Set to `None` if you want no prefix (or want to
      handle it in a hook during message generation instead.
    - `channel_msg_nick_pattern`(str, default `"{alias}\\s*?|{alias}\\s+?(?P<arg1>.+?)") -
      this is what used when a channel subscriber gets a channel nick assigned to this
      channel. The nickhandler uses the pattern to pick out this channel's name from user
      input. The `{alias}` token will get both the channel's key and any set/custom aliases
      per subscriber. You need to allow for an `<arg1>` regex group to catch any message
      that should be send to the  channel. You usually don't need to change this pattern
      unless you are changing channel command-style entirely.
    - `channel_msg_nick_replacement` (str, default `"channel {channelname} = $1"` - this
      is used by the nickhandler to generate a replacement string once the nickhandler (using
      the `channel_msg_nick_pattern`) identifies that the channel should be addressed
      to send a message to it. The `<arg1>` regex pattern match from `channel_msg_nick_pattern`
      will end up at the `$1` position in the replacement. Together, this allows you do e.g.
      'public Hello' and have that become a mapping to `channel public = Hello`. By default,
      the account-level `channel` command is used. If you were to rename that command you must
      tweak the output to something like `yourchannelcommandname {channelname} = $1`.

    """

    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"

        """
        Called by the typeclass system the very first time the channel
        is saved to the database. Generally, don't overload this but
        the hooks called by this method.

        """
        self.at_channel_creation()

            # this is only set if the channel was created
            # with the utils.create.create_channel function.
                    self.db_key = "#i" % self.dbid
            elif cdict["key"] and self.key != cdict["key"]:
                self.key = cdict["key"]
            if cdict.get("aliases"):
                self.aliases.add(cdict["aliases"])
            if cdict.get("keep_log"):
                self.attributes.add("keep_log", cdict["keep_log"])
            if cdict.get("desc"):
                self.attributes.add("desc", cdict["desc"])
            if cdict.get("attrs"):
                self.attributes.batch_add(*cdict["attrs"])

        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)

    def at_channel_creation(self):
        """
        Called once, when the channel is first created.

        """
        pass

    # helper methods, for easy overloading

    _log_file = None

    def get_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)

        """
        if not self._log_file:
            self._log_file = self.attributes.get(
                "log_file", self.log_file.format(channelname=self.key.lower())
            )
        return self._log_file

    def set_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)

    def has_connection(self, subscriber):
        """
        Checks so this account is actually listening
        to this channel.

        Args:
            subscriber (Account or Object): Entity to check.

        Returns:
            has_sub (bool): Whether the subscriber is subscribing to
                this channel or not.

        Notes:
            This will first try Account subscribers and only try Object
                if the Account fails.

        """
        has_sub = self.subscriptions.has(subscriber)
        if not has_sub and inherits_from(subscriber, evennia.DefaultObject):
            # it's common to send an Object when we
            # by default only allow Accounts to subscribe.
            has_sub = self.subscriptions.has(subscriber.account)
        return has_sub

    @property
    def mutelist(self):
        return self.db.mute_list or []

    @property
    def banlist(self):
        return self.db.ban_list or []

    @property
    def wholist(self):
        subs = self.subscriptions.all()
        muted = list(self.mutelist)
        listening = [ob for ob in subs if ob.is_connected and ob not in muted]
        if subs:
            # display listening subscribers in bold
            string = ", ".join(
                [
                    account.key if account not in listening else f"|w{account.key}|n"
                    for account in subs
                ]
            )
        else:
            string = "<None>"

    def mute(self, subscriber, **kwargs):
        """
        Adds an entity to the list of muted subscribers.
        A muted subscriber will no longer see channel messages,
        but may use channel commands.

        Args:
            subscriber (Object or Account): Subscriber to mute.

        Returns:
            bool: True if muting was successful, False if we were already
                muted.

        """
        mutelist = self.mutelist
        if subscriber not in mutelist:
            mutelist.append(subscriber)
            self.db.mute_list = mutelist

    def unmute(self, subscriber, **kwargs):
        """
        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.

        Returns:
            bool: True if unmuting was successful, False if we were already
                unmuted.

        """
        if subscriber in mutelist:
            mutelist.remove(subscriber)

    def ban(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.

        Returns:
            bool: True if banning was successful, False if target was already
                banned.
        """
        banlist = self.banlist
        if target not in banlist:
            banlist.append(target)
            self.db.ban_list = banlist

    def unban(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:

        Returns:
            bool: True if unbanning was successful, False if target was not
                previously banned.
        """
        banlist = list(self.banlist)
        if target in banlist:
            banlist = [banned for banned in banlist if banned != target]

    def connect(self, subscriber, **kwargs):
        """
        Connect the user to this channel. This checks access.

        Args:
            subscriber (Account or Object): the entity to subscribe
                to this channel.

        Returns:
            success (bool): Whether or not the addition was
                successful.

        """
        # check access
        if subscriber in self.banlist or not self.access(subscriber, "listen"):
        # pre-join hook
        connect = self.pre_join_channel(subscriber)
        if not connect:
        # subscribe
        self.subscriptions.add(subscriber)
        # unmute
        self.unmute(subscriber)
        # post-join hook
        self.post_join_channel(subscriber)

    def disconnect(self, subscriber, **kwargs):
        """
        Disconnect entity from this channel.

        Args:
            subscriber (Account of Object): the
                entity to disconnect.

        Returns:
            success (bool): Whether or not the removal was

        """
        # pre-disconnect hook
        disconnect = self.pre_leave_channel(subscriber)
        if not disconnect:
        self.subscriptions.remove(subscriber)
        # unmute
        # post-disconnect hook
        self.post_leave_channel(subscriber)

        self,
        accessing_obj,
        access_type="listen",
        no_superuser_bypass=False,
        **kwargs,
    ):
        """
        Determines if another object has permission to access.

        Args:
            accessing_obj (Object): Object trying to access this one.
            access_type (str, optional): Type of access sought.
            default (bool, optional): What to return if no lock of access_type was found
            no_superuser_bypass (bool, optional): Turns off superuser
                lock bypass. Be careful with this one.

        Returns:
            return (bool): Result of lock check.

        """
        return self.locks.check(
        )

    def create(cls, key, creator=None, *args, **kwargs):
        """
        Creates a basic Channel with default parameters, unless otherwise
        specified or extended.

        Provides a friendlier interface to the utils.create_channel() function.

        Args:
            key (str): This must be unique.
            creator (Account or Object): Entity to associate with this channel
                (used for tracking)

            aliases (list of str): List of alternative (likely shorter) keynames.
            description (str): A description of the channel, for use in listings.
            keep_log (bool): Log channel throughput.
            typeclass (str or class): The typeclass of the Channel (not
                often used).
            ip (str): IP address of creator (for object auditing).

        Returns:
            channel (Channel): A newly created Channel.
            errors (list): A list of errors in string form, if any.

        """
        errors = []
        obj = None
        ip = kwargs.pop("ip", "")

        try:
            kwargs["desc"] = kwargs.pop("description", "")
            kwargs["typeclass"] = kwargs.get("typeclass", cls)
            obj = create.create_channel(key, *args, **kwargs)

            # Record creator id and creation IP
            if ip:
                obj.db.creator_ip = ip
            if creator:
                obj.db.creator_id = creator.id

        except Exception as exc:
            errors.append("An error occurred while creating this '%s' object." % key)
            logger.log_err(exc)

        return obj, errors

    def delete(self):
        """
        Deletes channel.

        Returns:
            bool: If deletion was successful. Only time it can fail would be
                if channel was already deleted. Even if it were to fail, all subscribers
                will be disconnected.

        """
        for subscriber in self.subscriptions.all():
            self.disconnect(subscriber)
        super().delete()

    def channel_prefix(self):
        """
        Hook method. How the channel should prefix itself for users.

        Returns:
            str: The channel prefix.

        """
        return self.channel_prefix_string.format(channelname=self.key)

    def add_user_channel_alias(self, user, alias, **kwargs):
        """
        Add a personal user-alias for this channel to a given subscriber.

        Args:
            user (Object or Account): The one to alias this channel.
            alias (str): The desired alias.

        Note:
            This is tightly coupled to the default `channel` command. If you
            change that, you need to change this as well.

            users need to be able to reference this channel easily. The other
            structure of this nick is given by `self.channel_msg_nick_pattern`

        """
        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=re.escape(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,
        )

        if chan_key != alias:
            # this allows for using the alias for general channel lookups
            user.nicks.add(alias, chan_key, category="channel", **kwargs)

    def remove_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:
            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).

        """
        user.nicks.remove(alias, category="channel", **kwargs)
        msg_nick_pattern = cls.channel_msg_nick_pattern.format(alias=alias)
        user.nicks.remove(msg_nick_pattern, category="inputline", **kwargs)

    def at_pre_msg(self, message, **kwargs):
        """
        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:
            **kwargs (any): Keywords passed on from `.msg`. This includes
                `senders`.

        Returns:
            str, False or None: Any custom changes made to the message. If
                falsy, no message will be sent.

        """

    def msg(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:
            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) if senders else []
        if self.send_to_online_only:
            receivers = self.subscriptions.online()
        else:
            receivers = self.subscriptions.all()
        if not bypass_mute:
            receivers = [receiver for receiver in receivers if receiver not in self.mutelist]

        send_kwargs = {"senders": senders, "bypass_mute": bypass_mute, **kwargs}

        # pre-send hook
        message = self.at_pre_msg(message, **send_kwargs)
        if message in (None, False):
            return

        for receiver in receivers:
            # send to each individual subscriber

            try:
                recv_message = receiver.at_pre_channel_msg(message, self, **send_kwargs)
                if recv_message in (None, False):
                    return

                receiver.channel_msg(recv_message, self, **send_kwargs)

                receiver.at_post_channel_msg(recv_message, self, **send_kwargs)

                logger.log_trace(f"Error sending channel message to {receiver}.")

        # post-send hook
        self.at_post_msg(message, **send_kwargs)

    def at_post_msg(self, message, **kwargs):
        """
        This is called after sending to *all* valid recipients. It is normally
        used for logging/channel history.

        Args:
            message (str): The message sent.
            **kwargs (any): Keywords passed on from `msg`, including `senders`.

        """
        # save channel history to log file
        if log_file:
            senders = ",".join(sender.key for sender in kwargs.get("senders", []))
            senders = f"{senders}: " if senders else ""
            message = f"{senders}{message}"
            logger.log_file(message, log_file)

    def pre_join_channel(self, joiner, **kwargs):
        """
        Hook method. Runs right before a channel is joined. If this
        returns a false value, channel joining is aborted.

        Args:
            joiner (object): The joining object.

        Returns:
            should_join (bool): If `False`, channel joining is aborted.

        """

    def post_join_channel(self, joiner, **kwargs):
        """
        Hook method. Runs right after an object or account joins a channel.

        Args:

        Notes:
            By default this adds the needed channel nicks to the joiner.

        """
        key_and_aliases = [self.key.lower()] + [alias.lower() for alias in self.aliases.all()]
        for key_or_alias in key_and_aliases:
            self.add_user_channel_alias(joiner, key_or_alias, **kwargs)

    def pre_leave_channel(self, leaver, **kwargs):
        """
        Hook method. Runs right before a user leaves a channel. If this returns a false
        value, leaving the channel will be aborted.

        Args:
            leaver (object): The leaving object.

        Returns:
            should_leave (bool): If `False`, channel parting is aborted.

        """

    def post_leave_channel(self, leaver, **kwargs):
        """
        Hook method. Runs right after an object or account leaves a channel.

        Args:

        """
        key_or_aliases = [self.key.lower()] + [alias.lower() for alias in self.aliases.all()]
        nicktuples = leaver.nicks.get(category="channel", return_tuple=True, return_list=True)
        key_or_aliases += [tup[2] for tup in nicktuples if tup[3].lower() == chan_key]
        for key_or_alias in key_or_aliases:
            self.remove_user_channel_alias(leaver, key_or_alias, **kwargs)

        """
        Hook method. This is always called whenever this channel is
        initiated -- that is, whenever it its typeclass is cached from
        memory. This happens on-demand first time the channel is used
        or activated in some way after being created but also after
        each server restart or reload.

        """
        pass

    #
    # Web/Django methods
    #

        """

        Returns:

        """
        content_type = ContentType.objects.get_for_model(self.__class__)
        return reverse(
            "admin:%s_%s_change" % (content_type.app_label, content_type.model), args=(self.id,)
        )

    def web_get_create_url(cls):
        """
        Returns the URI path for a View that allows users to create new
        instances of this object.

        ex. Chargen = '/characters/create/'

        a named view of 'channel-create' would be referenced by this method.

        ex.
        url(r'channels/create/', ChannelCreateView.as_view(), name='channel-create')

        the actual view and limiting who can create new objects is the
        developer's responsibility.

        Returns:
            path (str): URI path to object creation page, if defined.

        """
        try:
            return reverse("%s-create" % slugify(cls._meta.verbose_name))
            return "#"

        """

        a named view of 'channel-detail' would be referenced by this method.

        ex.
        ::

            url(r'channels/(?P<slug>[\w\d\-]+)/$',
                ChannelDetailView.as_view(), name='channel-detail')

        Returns:

        """
        try:
                "%s-detail" % slugify(self._meta.verbose_name),
                kwargs={"slug": slugify(self.db_key)},
            )
            return "#"

    def web_get_update_url(self):
        """
        Returns the URI path for a View that allows users to update this
        object.

        ex. Oscar (Character) = '/characters/oscar/1/change/'

        a named view of 'channel-update' would be referenced by this method.

        ex.
        ::

            url(r'channels/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
                ChannelUpdateView.as_view(), name='channel-update')

        the actual view and limiting who can modify objects is the developer's

        Returns:
            path (str): URI path to object update page, if defined.

        """
        try:
                "%s-update" % slugify(self._meta.verbose_name),
            )
            return "#"

    def web_get_delete_url(self):
        """
        Returns the URI path for a View that allows users to delete this object.

        ex. Oscar (Character) = '/characters/oscar/1/delete/'

        a named view of 'channel-delete' would be referenced by this method.

        ex.
        url(r'channels/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',
            ChannelDeleteView.as_view(), name='channel-delete')

        the actual view and limiting who can delete this object is the developer's

        Returns:
            path (str): URI path to object deletion page, if defined.

        """
        try:
                "%s-delete" % slugify(self._meta.verbose_name),
            )
            return "#"

    # Used by Django Sites/Admin
    get_absolute_url = web_get_detail_url

    # TODO Evennia 1.0+ removed hooks. Remove in 1.1.
    def message_transform(self, *args, **kwargs):
        raise RuntimeError(
            "Channel.message_transform is no longer used in 1.0+. "
            "Use Account/Object.at_pre_channel_msg instead."
        )

    def distribute_message(self, msgobj, online=False, **kwargs):
        raise RuntimeError("Channel.distribute_message is no longer used in 1.0+.")

    def format_senders(self, senders=None, **kwargs):
            "Channel.format_senders is no longer used in 1.0+. "
        )

    def pose_transform(self, msgobj, sender_string, **kwargs):
            "Channel.pose_transform is no longer used in 1.0+. "
        )

    def format_external(self, msgobj, senders, emit=False, **kwargs):
            "Channel.format_external is no longer used in 1.0+. "
        )

    def format_message(self, msgobj, emit=False, **kwargs):
            "Channel.format_message is no longer used in 1.0+. "
        )

    def pre_send_message(self, msg, **kwargs):
        raise RuntimeError("Channel.pre_send_message was renamed to Channel.at_pre_msg.")

    def post_send_message(self, msg, **kwargs):
        raise RuntimeError("Channel.post_send_message was renamed to Channel.at_post_msg.")

"""
These managers define helper methods for accessing the database from
Comm system components.

"""

from django.db.models import Q

from evennia.utils.utils import class_from_module, dbref, make_iter

_AccountDB = None
_ObjectDB = None
_ChannelDB = None
_ScriptDB = None
_SESSIONS = None

# error class

class CommError(Exception):
    """
    Raised by comm system, to allow feedback to player when caught.
    """

    pass

#
#

def identify_object(inp):
    """
    Helper function. Identifies if an object is an account or an object;
    return its database model

    Args:
        inp (any): Entity to be idtified.

    Returns:
        identified (tuple): This is a tuple with (`inp`, identifier)
            where `identifier` is one of "account", "object", "channel",
            "string", "dbref" or None.

    """
    if hasattr(inp, "__dbclass__"):
        clsname = inp.__dbclass__.__name__
        if clsname == "AccountDB":
            return inp, "account"
        elif clsname == "ObjectDB":
            return inp, "object"
        elif clsname == "ChannelDB":
            return inp, "channel"
        elif clsname == "ScriptDB":
            return inp, "script"
    if isinstance(inp, str):
        return inp, "string"
    elif dbref(inp):
        return dbref(inp), "dbref"
    else:
        return inp, None

def to_object(inp, objtype="account"):
    """
    Locates the object related to the given accountname or channel key.
    If input was already the correct object, return it.

    Args:
        inp (any): The input object/string
        objtype (str): Either 'account' or 'channel'.

    Returns:
        obj (object): The correct object related to `inp`.

    """
    obj, typ = identify_object(inp)
    if typ == objtype:
        return obj
    if objtype == "account":
        if typ == "object":
            return obj.account
        if typ == "string":
            return _AccountDB.objects.get(user_username__iexact=obj)
        if typ == "dbref":
            return _AccountDB.objects.get(id=obj)
        logger.log_err("%s %s %s %s %s" % (objtype, inp, obj, typ, type(inp)))
        raise CommError()
    elif objtype == "object":
        if typ == "account":
            return obj.obj
            return _ObjectDB.objects.get(db_key__iexact=obj)
            return _ObjectDB.objects.get(id=obj)
    elif objtype == "channel":
            return _ChannelDB.objects.get(db_key__iexact=obj)
            return _ChannelDB.objects.get(id=obj)
    elif objtype == "script":
            return _ScriptDB.objects.get(db_key__iexact=obj)
            return _ScriptDB.objects.get(id=obj)

    # an unknown
    return None

#
# Msg manager
#

class MsgManager(TypedObjectManager):
    """
    This MsgManager implements methods for searching and manipulating
    Messages directly from the database.

    These methods will all return database objects (or QuerySets)
    directly.

    A Message represents one unit of communication, be it over a
    Channel or via some form of in-game mail system. Like an e-mail,
    it always has a sender and can have any number of receivers (some
    of which may be Channels).

    """

    def identify_object(self, inp):
        """
        Wrapper to identify_object if accessing via the manager directly.

        Args:
            inp (any): Entity to be idtified.

        Returns:
            identified (tuple): This is a tuple with (`inp`, identifier)
                where `identifier` is one of "account", "object", "channel",
                "string", "dbref" or None.

        """
        return identify_object(inp)

    def get_message_by_id(self, idnum):
        """
        Retrieve message by its id.

        Args:
            idnum (int or str): The dbref to retrieve.

        Returns:
            message (Msg): The message.

        """
        try:
            return self.get(id=self.dbref(idnum, reqhash=False))

    def get_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.

        Returns:
            QuerySet: Matching messages.

        Raises:
            CommError: For incorrect sender types.

        """
        obj, typ = identify_object(sender)
            return self.filter(db_sender_accounts=obj).exclude(db_hide_from_accounts=obj)
        elif typ == "object":
            return self.filter(db_sender_objects=obj).exclude(db_hide_from_objects=obj)
        elif typ == "script":
            return self.filter(db_sender_scripts=obj)
        else:
            raise CommError

    def get_messages_by_receiver(self, recipient):
        """
        Get all messages sent to one given recipient.

        Args:
            recipient (Object, Account or Channel): The recipient of the messages to search for.

        Returns:
            Queryset: Matching messages.

        Raises:
            CommError: If the `recipient` is not of a valid type.

        """
        obj, typ = identify_object(recipient)
            return self.filter(db_receivers_accounts=obj).exclude(db_hide_from_accounts=obj)
            return self.filter(db_receivers_objects=obj).exclude(db_hide_from_objects=obj)
            return self.filter(db_receivers_scripts=obj)
        else:

    def search_message(self, sender=None, receiver=None, freetext=None, dbref=None):
        """
        Search the message database for particular messages. At least
        one of the arguments must be given to do a search.

        Args:
            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:
                This can potentially be slow, so make sure to supply one of
                the other arguments to limit the search.
            dbref (int): The exact database id of the message. This will override
                    all other search criteria since it's unique and
                    always gives only one match.

        Returns:
            Queryset: Iterable with 0, 1 or more matches.

        """
        # unique msg id
        if dbref:
            return self.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 (we need __pk to avoid an error with empty Q() objects)
        sender, styp = identify_object(sender)
        if sender:
            spk = sender.pk
        if styp == "account":
            sender_restrict = Q(db_sender_accounts__pk=spk) & ~Q(db_hide_from_accounts__pk=spk)
        elif styp == "object":
            sender_restrict = Q(db_sender_objects__pk=spk) & ~Q(db_hide_from_objects__pk=spk)
        elif styp == "script":
            sender_restrict = Q(db_sender_scripts__pk=spk)
        else:
            sender_restrict = Q()
        # filter by receiver
        receiver, rtyp = identify_object(receiver)
        if receiver:
            rpk = receiver.pk
        if rtyp == "account":
            receiver_restrict = Q(db_receivers_accounts__pk=rpk) & ~Q(db_hide_from_accounts__pk=rpk)
        elif rtyp == "object":
            receiver_restrict = Q(db_receivers_objects__pk=rpk) & ~Q(db_hide_from_objects__pk=rpk)
        elif rtyp == "script":
            receiver_restrict = Q(db_receivers_scripts__pk=rpk)
        elif rtyp == "channel":
            raise DeprecationWarning(
                "Msg.objects.search don't accept channel recipients since "
                "Channels no longer accepts Msg objects."
            )
        else:
            receiver_restrict = Q()
        # filter by full text
        if freetext:
            fulltext_restrict = Q(db_header__icontains=freetext) | Q(db_message__icontains=freetext)
        else:
            fulltext_restrict = Q()
        # execute the query
        return self.filter(sender_restrict & receiver_restrict & fulltext_restrict)

    message_search = search_message

    def create_message(
        self,
        senderobj,
        message,
        receivers=None,
        header=None,
        **kwargs,
    ):
        """
        Create a new communication Msg. Msgs represent a unit of
        database-persistent communication between entites.

        Args:
            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.
            receivers (Object, Account, Script, str or list): An Account/Object to send
                to, or a list of them. If a string, it's an identifier for an external
                receiver.
            locks (str): Lock definition string.
            tags (list): A list of tags or tuples `(tag[,category[,data]])`.
            header (str): Mime-type or other optional information for the message

        Notes:
            The Comm system is created to be very open-ended, so it's fully
            possible to let a message both go several receivers at the same time,
            it's up to the command definitions to limit this as desired.

        """
        if "channels" in kwargs:
                "create_message() does not accept 'channel' kwarg anymore "
                "- channels no longer accept Msg objects."
            )

        if not message:
            # we don't allow empty messages.
        new_message = self.model(db_message=message)
        new_message.save()
        for sender in make_iter(senderobj):
            new_message.senders = sender
        new_message.header = header
        for receiver in make_iter(receivers):
            new_message.receivers = receiver
        if locks:
            new_message.locks.add(locks)
        if tags:
            new_message.tags.batch_add(*tags)

        return new_message

#
# Channel manager
#

class ChannelDBManager(TypedObjectManager):
    """
    This ChannelManager implements methods for searching and
    manipulating Channels directly from the database.

    directly.

    A Channel is an in-game venue for communication. It's essentially
    representation of a re-sender: Users sends Messages to the
    Channel, and the Channel re-sends those messages to all users
    subscribed to the Channel.

    """

    def get_all_channels(self):
        """
        Get all channels.

        Returns:
            channels (list): All channels in game.

        """
        return self.all()

    def get_channel(self, channelkey):
        """
        Return the channel object if given its key.
        Also searches its aliases.

        Args:
            channelkey (str): Channel key to search for.

        Returns:
            channel (Channel or None): A channel match.

        """
        dbref = self.dbref(channelkey)
        if dbref:
            try:
                return self.get(id=dbref)
            except self.model.DoesNotExist:
                pass
        results = self.filter(
            Q(db_key__iexact=channelkey)
            | Q(db_tags__db_tagtype__iexact="alias", db_tags__db_key__iexact=channelkey)
        ).distinct()
        return results[0] if results else None

    def get_subscriptions(self, subscriber):
        """
        Return all channels a given entity is subscribed to.

        Args:
            subscriber (Object or Account): The one subscribing.

        Returns:
            subscriptions (list): Channel subscribed to.

        """
        clsname = subscriber.__dbclass__.__name__
            return subscriber.account_subscription_set.all()
        if clsname == "ObjectDB":
            return subscriber.object_subscription_set.all()
        return []

    def search_channel(self, ostring, exact=True):
        """
        Search the channel database for a particular channel.

        Args:
            ostring (str): The key or database id of the channel.
            exact (bool, optional): Require an exact (but not
                case sensitive) match.

        Returns:

        """
        if dbref:

        if exact:
            channels = self.filter(
                Q(db_key__iexact=ostring)
                | Q(db_tags__db_tagtype__iexact="alias", db_tags__db_key__iexact=ostring)
            ).distinct()
        else:
                Q(db_key__icontains=ostring)
                | Q(db_tags__db_tagtype__iexact="alias", db_tags__db_key__icontains=ostring)
        return channels

    def create_channel(
        self,
        key,
        desc=None,
        keep_log=True,
        attrs=None,
    ):
        """
        Create A communication Channel. A Channel serves as a central hub
        for distributing Msgs to groups of people without specifying the
        receivers explicitly. Instead accounts may 'connect' to the channel
        and follow the flow of messages. By default the channel allows
        access to all old messages, but this can be turned off with the
        keep_log switch.

        Args:

            desc (str): A description of the channel, for use in listings.
            attrs (list): List of attributes on form `(name, value[,category[,lockstring]])`

        Returns:
            channel (Channel): A newly created channel.

        """
        typeclass = typeclass if typeclass else settings.BASE_CHANNEL_TYPECLASS

            # a path is given. Load the actual typeclass

        # create new instance
        new_channel = typeclass(db_key=key)

        # store call signature for the signal
        new_channel._createdict = dict(
            key=key,
            desc=desc,
            keep_log=keep_log,
            attrs=attrs,
        )

        # this will trigger the save signal which in turn calls the
        # at_first_save hook on the typeclass, where the _createdict can be
        # used.
        new_channel.save()

        signals.SIGNAL_CHANNEL_POST_CREATE.send(sender=new_channel)

        return new_channel

    channel_search = search_channel

class ChannelManager(ChannelDBManager, TypeclassManager):
    """
    Wrapper to group the typeclass manager to a consistent name.
    """

    pass

"""
Models for the in-game communication system.

The comm system could take the form of channels, but can also be
adopted for storing tells or in-game mail.

The comsystem's main component is the Message (Msg), which carries the
actual information between two parties.  Msgs are stored in the
database and usually not deleted.  A Msg always have one sender (a
user), but can have any number targets, both users and channels.

For non-persistent (and slightly faster) use one can also use the
TempMsg, which mimics the Msg API but without actually saving to the
database.

Channels are central objects that act as targets for Msgs. Accounts can
connect to channels by use of a ChannelConnect object (this object is
necessary to easily be able to delete connections on the fly).

"""

from evennia.comms import managers
from evennia.typeclasses.tags import Tag, TagHandler
from evennia.utils.idmapper.models import SharedMemoryModel
from evennia.utils.utils import crop, lazy_property, make_iter

__all__ = ("Msg", "TempMsg", "ChannelDB", "SubscriptionHandler")

#
# Msg
#

class Msg(SharedMemoryModel):
    """
    A single message. This model describes all ooc messages
    sent in-game, both to channels and between accounts.

    The Msg class defines the following database fields (all
    accessed via specific handler methods):

    - db_sender_accounts: Account senders
    - db_sender_objects: Object senders
    - db_sender_scripts: Script senders
    - db_sender_external: External sender (defined as string name)
    - db_receivers_accounts: Receiving accounts
    - db_receivers_objects: Receiving objects
    - db_receivers_scripts: Receiveing scripts
    - db_receiver_external: External sender (defined as string name)
    - db_header: Header text
    - db_message: The actual message text
    - db_date_created: time message was created / sent
    - db_hide_from_sender: bool if message should be hidden from sender
    - db_hide_from_receivers: list of receiver objects to hide message from
    - db_lock_storage: Internal storage of lock strings.

    """

    #
    # Msg database model setup
    #
    #
    # These databse fields are all set using their corresponding properties,
    # named same as the field, but withtout the db_* prefix.

    db_sender_accounts = models.ManyToManyField(
        "accounts.AccountDB",
        related_name="sender_account_set",
        blank=True,
        verbose_name="Senders (Accounts)",
        db_index=True,
    )

    db_sender_objects = models.ManyToManyField(
        "objects.ObjectDB",
        related_name="sender_object_set",
        verbose_name="Senders (Objects)",
    )
    db_sender_scripts = models.ManyToManyField(
        "scripts.ScriptDB",
        related_name="sender_script_set",
        verbose_name="Senders (Scripts)",
    )
    db_sender_external = models.CharField(
        "external sender",
            "Identifier for single external sender, for use with senders "
            "not represented by a regular database model."
        ),
    )

    db_receivers_accounts = models.ManyToManyField(
        related_name="receiver_account_set",
        verbose_name="Receivers (Accounts)",
        help_text="account receivers",
    )

    db_receivers_objects = models.ManyToManyField(
        related_name="receiver_object_set",
        verbose_name="Receivers (Objects)",
        help_text="object receivers",
    )
    db_receivers_scripts = models.ManyToManyField(
        related_name="receiver_script_set",
        verbose_name="Receivers (Scripts)",
        help_text="script_receivers",
    )

    db_receiver_external = models.CharField(
        "external receiver",
        max_length=1024,
            "Identifier for single external receiver, for use with recievers "
        ),
    )

    # header could be used for meta-info about the message if your system needs
    # it, or as a separate store for the mail subject line maybe.
    db_header = models.TextField("header", null=True, blank=True)
    # the message body itself
    db_message = models.TextField("message")
    # send date
    db_date_created = models.DateTimeField(
        "date sent", editable=False, auto_now_add=True, db_index=True
    )
    # lock storage
    db_lock_storage = models.TextField(
        "locks", blank=True, help_text="access locks on this message."
    )

    # these can be used to filter/hide a given message from supplied objects/accounts
    db_hide_from_accounts = models.ManyToManyField(
        "accounts.AccountDB", related_name="hide_from_accounts_set", blank=True
    )

    db_hide_from_objects = models.ManyToManyField(
        "objects.ObjectDB", related_name="hide_from_objects_set", blank=True
    )

    db_tags = models.ManyToManyField(
        Tag,
            "tags on this message. Tags are simple string markers to "
            "identify, group and alias messages."
        ),
    )

    objects = managers.MsgManager()
    _is_deleted = False

    class Meta(object):
        "Define Django meta options"
        verbose_name = "Msg"

    def locks(self):

    def tags(self):
        return TagHandler(self)

    # Wrapper properties to easily set database fields. These are
    # @property decorators that allows to access these fields using
    # normal python operations (without having to remember to save()
    # etc). So e.g. a property 'attr' has a get/set/del decorator
    # defined that allows the user to do self.attr = value,
    # value = self.attr and del self.attr respectively (where self
    # is the object in question).

    @property
    def senders(self):
        "Getter. Allows for value = self.senders"
        return (
            list(self.db_sender_accounts.all())
            + list(self.db_sender_objects.all())
            + list(self.db_sender_scripts.all())
            + ([self.db_sender_external] if self.db_sender_external else [])
        )

    @senders.setter
    def senders(self, senders):
        "Setter. Allows for self.sender = value"

        if isinstance(senders, str):
            self.db_sender_external = senders
            self.save(update_fields=["db_sender_external"])
            return

        for sender in make_iter(senders):
            if not sender:
                continue
            if not hasattr(sender, "__dbclass__"):
                raise ValueError("This is a not a typeclassed object!")
            clsname = sender.__dbclass__.__name__
            if clsname == "ObjectDB":
                self.db_sender_objects.add(sender)
            elif clsname == "AccountDB":
                self.db_sender_accounts.add(sender)
            elif clsname == "ScriptDB":
                self.db_sender_scripts.add(sender)

    @senders.deleter
        "Deleter. Clears all senders"
        self.db_sender_accounts.clear()
        self.db_sender_objects.clear()
        self.db_sender_scripts.clear()
        self.db_sender_external = ""
        self.save()

    def remove_sender(self, senders):
        """
        Remove a single sender or a list of senders.

        Args:
            senders (Account, Object, str or list): Senders to remove.
                If a string, removes the external sender.

        """
            self.db_sender_external = ""
            return

                continue
                self.db_sender_objects.remove(sender)
                self.db_sender_accounts.remove(sender)

    @property
    def receivers(self):
        """
        Getter. Allows for value = self.receivers.
        Returns four lists of receivers: accounts, objects, scripts and
            external_receivers.

        """
        return (
            list(self.db_receivers_accounts.all())
            + list(self.db_receivers_objects.all())
            + list(self.db_receivers_scripts.all())
            + ([self.db_receiver_external] if self.db_receiver_external else [])
        )

    @receivers.setter
    def receivers(self, receivers):
        """
        Setter. Allows for self.receivers = value.  This appends a new receiver
        to the message. If a string, replaces an external receiver.

        """
        if isinstance(receivers, str):
            self.db_receiver_external = receivers
            self.save(update_fields=["db_receiver_external"])
            return

            if not receiver:
                continue
            if not hasattr(receiver, "__dbclass__"):
            clsname = receiver.__dbclass__.__name__
                self.db_receivers_objects.add(receiver)
                self.db_receivers_accounts.add(receiver)
                self.db_receivers_scripts.add(receiver)

    @receivers.deleter
        "Deleter. Clears all receivers"
        self.db_receivers_accounts.clear()
        self.db_receivers_objects.clear()
        self.db_receivers_scripts.clear()
        self.db_receiver_external = ""

    def remove_receiver(self, receivers):
        """
        Remove a single receiver, a list of receivers, or a single extral receiver.

        Args:
            receivers (Account, Object, Script, list or str): Receiver
                to remove. A string removes the external receiver.

        """
            self.db_receiver_external = ""
            self.save(update_fields="db_receiver_external")
            return

                continue
            elif not hasattr(receiver, "__dbclass__"):
                self.db_receivers_objects.remove(receiver)
                self.db_receivers_accounts.remove(receiver)
                self.db_receivers_scripts.remove(receiver)

    @property
    def hide_from(self):
        """
        Getter. Allows for value = self.hide_from.
        Returns two lists of accounts and objects.

        """
        return (
            self.db_hide_from_accounts.all(),
            self.db_hide_from_objects.all(),
        )

    @hide_from.setter
    def hide_from(self, hiders):
        """
        Setter. Allows for self.hide_from = value. Will append to hiders.

        """
        for hider in make_iter(hiders):
            if not hider:
                continue
            if not hasattr(hider, "__dbclass__"):
            clsname = hider.__dbclass__.__name__
            if clsname == "AccountDB":
                self.db_hide_from_accounts.add(hider.__dbclass__)
            elif clsname == "ObjectDB":
                self.db_hide_from_objects.add(hider.__dbclass__)

    @hide_from.deleter
        """
        Deleter. Allows for del self.hide_from_senders

        """
        self.db_hide_from_accounts.clear()
        self.db_hide_from_objects.clear()

    #
    # Msg class methods
    #

        """
        This handles what is shown when e.g. printing the message.

        """
        senders = ",".join(getattr(obj, "key", str(obj)) for obj in self.senders)
        receivers = ",".join(getattr(obj, "key", str(obj)) for obj in self.receivers)
        return "%s->%s: %s" % (senders, receivers, crop(self.message, width=40))

    def access(self, accessing_obj, access_type="read", default=False):
        """
        Checks lock access.

        Args:
            accessing_obj (Object or Account): The object trying to gain access.
            access_type (str, optional): The type of lock access to check.
            default (bool): Fallback to use if `access_type` lock is not defined.

        Returns:
            result (bool): If access was granted or not.

        """
        return self.locks.check(accessing_obj, access_type=access_type, default=default)

#
# TempMsg
#

class TempMsg:
    """
    This is a non-persistent object for sending temporary messages that will not be stored.  It
    mimics the "real" Msg object, but doesn't require sender to be given.

    """

    def __init__(
        self,
        senders=None,
        message="",
        header="",
        type="",
        lockstring="",
        hide_from=None,
    ):
        """
        Creates the temp message.

        Args:
            senders (any or list, optional): Senders of the message.
            receivers (Account, Object, Script or list, optional): Receivers of this message.
            message (str, optional): Message to send.
            header (str, optional): Header of message.
            type (str, optional): Message class, if any.
            lockstring (str, optional): Lock for the message.
            hide_from (Account, Object, or list, optional): Entities to hide this message from.

        """
        self.senders = senders and make_iter(senders) or []
        self.receivers = receivers and make_iter(receivers) or []
        self.type = type
        self.header = header
        self.message = message
        self.lock_storage = lockstring
        self.hide_from = hide_from and make_iter(hide_from) or []
        self.date_created = timezone.now()

        """

        """
        senders = ",".join(obj.key for obj in self.senders)
        receivers = ",".join(obj.key for obj in self.receivers)

    def remove_sender(self, sender):
        """
        Remove a sender or a list of senders.

        Args:
            sender (Object, Account, str or list): Senders to remove.

        """
        for o in make_iter(sender):
            try:
                self.senders.remove(o)
                pass  # nothing to remove

    def remove_receiver(self, receiver):
        """
        Remove a receiver or a list of receivers

        Args:
            receiver (Object, Account, Script, str or list): Receivers to remove.

        """

        for o in make_iter(receiver):
            try:

        """

        Args:

        Returns:

        """

#
# Channel
#

class SubscriptionHandler:
    """
    This handler manages subscriptions to the
    channel and hides away which type of entity is
    subscribing (Account or Object)

    """

    def __init__(self, obj):
        """
        Initialize the handler

        Attr:
            obj (ChannelDB): The channel the handler sits on.

        """
        self._cache = None

    def _recache(self):
        self._cache = {
            account: True
            for account in self.obj.db_account_subscriptions.all().order_by("pk")
            if hasattr(account, "pk") and account.pk
        }
        self._cache.update(
            {
                obj: True
                for obj in self.obj.db_object_subscriptions.all().order_by("pk")
                if hasattr(obj, "pk") and obj.pk
            }
        )

    def has(self, entity):
        """
        Check if the given entity subscribe to this channel

        Args:
            entity (str, Account or Object): The entity to return. If
                a string, it assumed to be the key or the #dbref
                of the entity.

        Returns:
            subscriber (Account, Object or None): The given
                subscriber.

        """
        if self._cache is None:
            self._recache()
        return entity in self._cache

    def add(self, entity):
        """
        Subscribe an entity to this channel.

        Args:
            entity (Account, Object or list): The entity or
                list of entities to subscribe to this channel.

        Note:
            No access-checking is done here, this must have
                been done before calling this method. Also
                no hooks will be called.

        """
        for subscriber in make_iter(entity):
            if subscriber:
                clsname = subscriber.__dbclass__.__name__
                # chooses the right type
                if clsname == "ObjectDB":
                    self.obj.db_object_subscriptions.add(subscriber)
                elif clsname == "AccountDB":
                    self.obj.db_account_subscriptions.add(subscriber)
        self._recache()

    def remove(self, entity):
        """
        Remove a subscriber from the channel.

        Args:
                entities to un-subscribe from the channel.

        """
                if clsname == "AccountDB":
                    self.obj.db_account_subscriptions.remove(entity)
                elif clsname == "ObjectDB":
                    self.obj.db_object_subscriptions.remove(entity)

        """
        Get all subscriptions to this channel.

        Returns:
            subscribers (list): The subscribers. This
                may be a mix of Accounts and Objects!

        """
        return self._cache

    get = all  # alias

    def online(self):
        """
        Get all online accounts from our cache
        Returns:
            subscribers (list): Subscribers who are online or
                are puppeted by an online account.
        """
        subs = []
        recache_needed = False
        for obj in self.all():
            from django.core.exceptions import ObjectDoesNotExist

            try:
                if not obj.is_connected:
                    continue
            except ObjectDoesNotExist:
                # a subscribed object has already been deleted. Mark that we need a recache and
                # ignore it
                recache_needed = True
                continue
            subs.append(obj)
        if recache_needed:
        return subs

        """
        Remove all subscribers from channel.

        """
        self.obj.db_account_subscriptions.clear()
        self.obj.db_object_subscriptions.clear()

class ChannelDB(TypedObject):
    """
    This is the basis of a comm channel, only implementing
    the very basics of distributing messages.

    The Channel class defines the following database fields
    beyond the ones inherited from TypedObject:

      - db_account_subscriptions: The Account subscriptions.
      - db_object_subscriptions: The Object subscriptions.

    """

    db_account_subscriptions = models.ManyToManyField(
        related_name="account_subscription_set",
        verbose_name="account subscriptions",
    )

    db_object_subscriptions = models.ManyToManyField(
        related_name="object_subscription_set",
        verbose_name="object subscriptions",
    )

    objects = managers.ChannelDBManager()

    __settingclasspath__ = settings.BASE_CHANNEL_TYPECLASS
    __defaultclasspath__ = "evennia.comms.comms.DefaultChannel"
    __applabel__ = "comms"

        verbose_name = "Channel"
        verbose_name_plural = "Channels"

        "Echoes the text representation of the channel."
        return "Channel '%s' (%s)" % (self.key, self.db.desc)

    def subscriptions(self):
        return SubscriptionHandler(self)

import gzip
import pickle
from unittest import skipIf

from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import ContentFile
from django.test import TestCase, override_settings
from django.utils.timezone import is_aware, utc

_SKIP = False
try:
    from botocore.exceptions import ClientError

    from .awsstorage import aws_s3_cdn as s3boto3
    _SKIP = True

try:
    from django.utils.six.moves.urllib import parse as urlparse
    from urllib import parse as urlparse

try:
    from unittest import mock
except ImportError:  # Python 3.2 and below
    import mock

@skipIf(_SKIP, "botocore not installed")
class S3Boto3TestCase(TestCase):
        self.storage = s3boto3.S3Boto3Storage(access_key="foo", secret_key="bar")
        self.storage._connections.connection = mock.MagicMock()

class S3Boto3StorageTests(S3Boto3TestCase):
    def test_clean_name(self):
        """
        Test the base case of _clean_name
        """
        path = self.storage._clean_name("path/to/somewhere")
        self.assertEqual(path, "path/to/somewhere")

    def test_clean_name_normalize(self):
        """
        Test the normalization of _clean_name
        """
        path = self.storage._clean_name("path/to/../somewhere")
        self.assertEqual(path, "path/somewhere")

    def test_clean_name_trailing_slash(self):
        """
        Test the _clean_name when the path has a trailing slash
        """
        path = self.storage._clean_name("path/to/somewhere/")
        self.assertEqual(path, "path/to/somewhere/")

    def test_clean_name_windows(self):
        """
        """
        path = self.storage._clean_name("path\\to\\somewhere")

    def test_pickle_with_bucket(self):
        """
        Test that the storage can be pickled with a bucket attached
        """
        # Ensure the bucket has been used
        self.storage.bucket
        self.assertIsNotNone(self.storage._bucket)

        # Can't pickle MagicMock, but you can't pickle a real Bucket object either
        p = pickle.dumps(self.storage)
        new_storage = pickle.loads(p)

        self.assertIsInstance(new_storage._connections, threading.local)
        # Put the mock connection back in
        new_storage._connections.connection = mock.MagicMock()

        self.assertIsNone(new_storage._bucket)
        new_storage.bucket
        self.assertIsNotNone(new_storage._bucket)

    def test_pickle_without_bucket(self):
        """
        Test that the storage can be pickled, without a bucket instance
        """

        # Can't pickle a threadlocal

    def test_storage_url_slashes(self):
        """
        Test URL generation.
        """
        self.storage.custom_domain = "example.com"

        # We expect no leading slashes in the path,
        # and trailing slashes should be preserved.
        self.assertEqual(self.storage.url(""), "https://example.com/")
        self.assertEqual(self.storage.url("path"), "https://example.com/path")
        self.assertEqual(self.storage.url("path/"), "https://example.com/path/")
        self.assertEqual(self.storage.url("path/1"), "https://example.com/path/1")
        self.assertEqual(self.storage.url("path/1/"), "https://example.com/path/1/")

    def test_storage_save(self):
        """
        Test saving a file
        """
        name = "test_storage_save.txt"
        content = ContentFile("new content")
        self.storage.save(name, content)
        self.storage.bucket.Object.assert_called_once_with(name)

        obj = self.storage.bucket.Object.return_value
        obj.upload_fileobj.assert_called_with(
            content,
            ExtraArgs={
                "ContentType": "text/plain",
                "ACL": self.storage.default_acl,
            },
        )

    def test_storage_save_with_acl(self):
        """
        Test saving a file with user defined ACL.
        """
        self.storage.default_acl = "private"

            content,
                "ACL": "private",
            },
        )

    def test_content_type(self):
        """
        Test saving a file with a None content type.
        """
        name = "test_image.jpg"
        content = ContentFile("data")
        content.content_type = None

            content,
                "ContentType": "image/jpeg",
            },
        )

    def test_storage_save_gzipped(self):
        """
        Test saving a gzipped file
        """
        name = "test_storage_save.gz"
        content = ContentFile("I am gzip'd")
            content,
                "ContentType": "application/octet-stream",
                "ContentEncoding": "gzip",
            },
        )

    def test_storage_save_gzip(self):
        """
        Test saving a file with gzip enabled.
        """
        self.storage.gzip = True
        name = "test_storage_save.css"
        content = ContentFile("I should be gzip'd")
            mock.ANY,
                "ContentType": "text/css",
            },
        )
        args, kwargs = obj.upload_fileobj.call_args
        content = args[0]
        zfile = gzip.GzipFile(mode="rb", fileobj=content)
        self.assertEqual(zfile.read(), b"I should be gzip'd")

    def test_storage_save_gzip_twice(self):
        """
        Test saving the same file content twice with gzip enabled.
        """
        # Given

        # When
        self.storage.save("test_storage_save_2.css", content)

        # Then
            mock.ANY,
            },
        )

    def test_compress_content_len(self):
        """
        Test that file returned by _compress_content() is readable.
        """
        content = self.storage._compress_content(content)
        self.assertTrue(len(content.read()) > 0)

    def test_storage_open_write(self):
        """
        Test opening a file in write mode
        """
        name = "test_open_for_writïng.txt"
        content = "new content"

        # Set the encryption flag used for multipart uploads
        self.storage.encryption = True
        self.storage.reduced_redundancy = True
        self.storage.default_acl = "public-read"

        file = self.storage.open(name, "w")
        self.storage.bucket.Object.assert_called_with(name)
        # Set the name of the mock object
        obj.key = name

        file.write(content)
        obj.initiate_multipart_upload.assert_called_with(
            ACL="public-read",
            ContentType="text/plain",
            ServerSideEncryption="AES256",
            StorageClass="REDUCED_REDUNDANCY",
        )

        # Save the internal file before closing
        multipart = obj.initiate_multipart_upload.return_value
        multipart.parts.all.return_value = [mock.MagicMock(e_tag="123", part_number=1)]
        file.close()
        multipart.Part.assert_called_with(1)
        part = multipart.Part.return_value
        part.upload.assert_called_with(Body=content.encode("utf-8"))
        multipart.complete.assert_called_once_with(
            MultipartUpload={"Parts": [{"ETag": "123", "PartNumber": 1}]}
        )

    def test_storage_open_no_write(self):
        """
        Test opening file in write mode and closing without writing.

        A file should be created as by obj.put(...).
        """
        name = "test_open_no_write.txt"

        # Set the encryption flag used for puts

        obj.load.side_effect = ClientError(
            {"Error": {}, "ResponseMetadata": {"HTTPStatusCode": 404}}, "head_bucket"
        )

        obj.load.assert_called_once_with()
        obj.put.assert_called_once_with(
            Body=b"",
        )

    def test_storage_open_no_overwrite_existing(self):
        """
        Test opening an existing file in write mode and closing without writing.
        """
        name = "test_open_no_overwrite_existing.txt"

        obj.put.assert_not_called()

    def test_storage_write_beyond_buffer_size(self):
        """
        Test writing content that exceeds the buffer size
        """
        name = "test_open_for_writïng_beyond_buffer_size.txt"

        # Initiate the multipart upload
        file.write("")
        )

        # Write content at least twice as long as the buffer size
        written_content = ""
        counter = 1
        while len(written_content) < 2 * file.buffer_size:
            content = "hello, aws {counter}\n".format(counter=counter)
            # Write more than just a few bytes in each iteration to keep the
            # test reasonably fast
            content += "*" * int(file.buffer_size / 10)
            file.write(content)
            written_content += content
            counter += 1

        multipart.parts.all.return_value = [
            mock.MagicMock(e_tag="123", part_number=1),
            mock.MagicMock(e_tag="456", part_number=2),
        ]
        self.assertListEqual(multipart.Part.call_args_list, [mock.call(1), mock.call(2)])
        uploaded_content = "".join(
            args_list[1]["Body"].decode("utf-8") for args_list in part.upload.call_args_list
        )
        self.assertEqual(uploaded_content, written_content)
            MultipartUpload={
                "Parts": [
                    {"ETag": "123", "PartNumber": 1},
                    {"ETag": "456", "PartNumber": 2},
                ]
            }
        )

    def test_auto_creating_bucket(self):
        self.storage.auto_create_bucket = True
        Bucket = mock.MagicMock()
        self.storage._connections.connection.Bucket.return_value = Bucket
        self.storage._connections.connection.meta.client.meta.region_name = "sa-east-1"

        Bucket.meta.client.head_bucket.side_effect = ClientError(
        )
        self.storage._get_or_create_bucket("testbucketname")
        Bucket.create.assert_called_once_with(
            CreateBucketConfiguration={
                "LocationConstraint": "sa-east-1",
            },
        )

    def test_auto_creating_bucket_with_acl(self):
        self.storage.bucket_acl = "public-read"

        )
            },
        )

    def test_storage_exists(self):
        self.assertTrue(self.storage.exists("file.txt"))
        self.storage.connection.meta.client.head_object.assert_called_with(
            Bucket=self.storage.bucket_name,
            Key="file.txt",
        )

    def test_storage_exists_false(self):
        self.storage.connection.meta.client.head_object.side_effect = ClientError(
            {"Error": {"Code": "404", "Message": "Not Found"}},
            "HeadObject",
        )
        self.assertFalse(self.storage.exists("file.txt"))
        )

    def test_storage_exists_doesnt_create_bucket(self):
        with mock.patch.object(self.storage, "_get_or_create_bucket") as method:
            self.storage.exists("file.txt")
            self.assertFalse(method.called)

    def test_storage_delete(self):
        self.storage.delete("path/to/file.txt")
        self.storage.bucket.Object.assert_called_with("path/to/file.txt")
        self.storage.bucket.Object.return_value.delete.assert_called_with()

    def test_storage_listdir_base(self):
        # Files:
        #   some/path/1.txt
        #   2.txt
        #   other/path/3.txt
        #   4.txt
        pages = [
            {
                "CommonPrefixes": [
                    {"Prefix": "some"},
                    {"Prefix": "other"},
                ],
                "Contents": [
                    {"Key": "2.txt"},
                    {"Key": "4.txt"},
                ],
            },
        ]

        paginator = mock.MagicMock()
        paginator.paginate.return_value = pages
        self.storage._connections.connection.meta.client.get_paginator.return_value = paginator

        dirs, files = self.storage.listdir("")
        paginator.paginate.assert_called_with(Bucket=None, Delimiter="/", Prefix="")

        self.assertEqual(dirs, ["some", "other"])
        self.assertEqual(files, ["2.txt", "4.txt"])

    def test_storage_listdir_subdir(self):
        # Files:
        #   some/2.txt
        pages = [
            {
                    {"Prefix": "some/path"},
                ],
                    {"Key": "some/2.txt"},
                ],
            },
        ]

        dirs, files = self.storage.listdir("some/")
        paginator.paginate.assert_called_with(Bucket=None, Delimiter="/", Prefix="some/")

        self.assertEqual(dirs, ["path"])
        self.assertEqual(files, ["2.txt"])

    def test_storage_size(self):
        obj.content_length = 4098

        name = "file.txt"
        self.assertEqual(self.storage.size(name), obj.content_length)

    def test_storage_mtime(self):
        # Test both USE_TZ cases
        for use_tz in (True, False):
            with self.settings(USE_TZ=use_tz):
                self._test_storage_mtime(use_tz)

    def _test_storage_mtime(self, use_tz):
        obj.last_modified = datetime.datetime.now(utc)

        self.assertFalse(
            is_aware(self.storage.modified_time(name)),
            "Naive datetime object expected from modified_time()",
        )

        self.assertIs(
            settings.USE_TZ,
            is_aware(self.storage.get_modified_time(name)),
            "{} datetime object expected from get_modified_time() when USE_TZ={}".format(
                ("Naive", "Aware")[settings.USE_TZ], settings.USE_TZ
            ),
        )

    def test_storage_url(self):
        name = "test_storage_size.txt"
        url = "http://aws.amazon.com/%s" % name
        self.storage.bucket.meta.client.generate_presigned_url.return_value = url
        self.storage.bucket.name = "bucket"
        self.assertEqual(self.storage.url(name), url)
        self.storage.bucket.meta.client.generate_presigned_url.assert_called_with(
            "get_object",
            Params={"Bucket": self.storage.bucket.name, "Key": name},
            ExpiresIn=self.storage.querystring_expire,
        )

        custom_expire = 123

        self.assertEqual(self.storage.url(name, expire=custom_expire), url)
            ExpiresIn=custom_expire,
        )

    def test_generated_url_is_encoded(self):
        self.storage.custom_domain = "mock.cloudfront.net"
        filename = "whacky & filename.mp4"
        url = self.storage.url(filename)
        parsed_url = urlparse.urlparse(url)
        self.assertEqual(parsed_url.path, "/whacky%20%26%20filename.mp4")
        self.assertFalse(self.storage.bucket.meta.client.generate_presigned_url.called)

    def test_special_characters(self):

        name = "ãlöhâ.jpg"

        url = self.storage.url(name)
        self.assertEqual(parsed_url.path, "/%C3%A3l%C3%B6h%C3%A2.jpg")

    def test_strip_signing_parameters(self):
        expected = "http://bucket.s3-aws-region.amazonaws.com/foo/bar"
            self.storage._strip_signing_parameters(
                "%s?X-Amz-Date=12345678&X-Amz-Signature=Signature" % expected
            ),
            expected,
        )
                "%s?expires=12345678&signature=Signature" % expected
            ),
            expected,
        )

    @skipIf(threading is None, "Test requires threading")
    def test_connection_threading(self):
        connections = []

        def thread_storage_connection():
            connections.append(self.storage.connection)

        for x in range(2):
            t = threading.Thread(target=thread_storage_connection)
            t.start()
            t.join()

        # Connection for each thread needs to be unique
        self.assertIsNot(connections[0], connections[1])

    def test_location_leading_slash(self):
        msg = (
            "S3Boto3Storage.location cannot begin with a leading slash. "
            "Found '/'. Use '' instead."
        )
        with self.assertRaises(ImproperlyConfigured, msg=msg):
            s3boto3.S3Boto3Storage(location="/")

    def test_override_class_variable(self):
        class MyStorage1(s3boto3.S3Boto3Storage):
            location = "foo1"

        storage = MyStorage1()
        self.assertEqual(storage.location, "foo1")

        class MyStorage2(s3boto3.S3Boto3Storage):
            location = "foo2"

        storage = MyStorage2()
        self.assertEqual(storage.location, "foo2")

    def test_override_init_argument(self):
        storage = s3boto3.S3Boto3Storage(location="foo1")
        storage = s3boto3.S3Boto3Storage(location="foo2")

"""
Module containing the building menu system.

Evennia contributor: vincent-lg 2018

Building menus are in-game menus, not unlike `EvMenu` though using a
different approach.  Building menus have been specifically designed to edit
information as a builder.  Creating a building menu in a command allows
builders quick-editing of a given object, like a room.  If you follow the
steps below to add the contrib, you will have access to an `@edit` command
that will edit any default object offering to change its key and description.

1. Import the `GenericBuildingCmd` class from this contrib in your
   `mygame/commands/default_cmdset.py` file:

    ```python
    from evennia.contrib.base_systems.building_menu import GenericBuildingCmd

    ```

2. Below, add the command in the `CharacterCmdSet`:

    ```python
    # ... These lines should exist in the file
    class CharacterCmdSet(default_cmds.CharacterCmdSet):
        key = "DefaultCharacter"

        def at_cmdset_creation(self):
            super().at_cmdset_creation()
            # ... add the line below
            self.add(GenericBuildingCmd())
    ```

The `@edit` command will allow you to edit any object.  You will need to
specify the object name or ID as an argument.  For instance: `@edit here`
will edit the current room.  However, building menus can perform much more
than this very simple example, read on for more details.

Building menus can be set to edit about anything.  Here is an example of
output you could obtain when editing the room:

```
 Editing the room: Limbo(#2)

 [T]itle: the limbo room
 [D]escription
    This is the limbo room.  You can easily change this default description,
    either by using the |y@desc/edit|n command, or simply by entering this
    menu (enter |yd|n).
 [E]xits:
     north to A parking(#4)
 [Q]uit this menu
```

From there, you can open the title choice by pressing t.  You can then
change the room title by simply entering text, and go back to the
main menu entering @ (all this is customizable).  Press q to quit this menu.

The first thing to do is to create a new module and place a class
inheriting from `BuildingMenu` in it.

```python
from evennia.contrib.base_systems.building_menu.building_menu import BuildingMenu

class RoomBuildingMenu(BuildingMenu):
    # ...

```

Next, override the `init` method.  You can add choices (like the title,
description, and exits choices as seen above) by using the `add_choice`
method.

```
    def init(self, room):
        self.add_choice("title", "t", attr="key")
```

That will create the first choice, the title choice.  If one opens your menu
and enter t, she will be in the title choice.  She can change the title
(it will write in the room's `key` attribute) and then go back to the
main menu using `@`.

`add_choice` has a lot of arguments and offers a great deal of
flexibility.  The most useful ones is probably the usage of callbacks,
as you can set almost any argument in `add_choice` to be a callback, a
function that you have defined above in your module.  This function will be
called when the menu element is triggered.

Notice that in order to edit a description, the best method to call isn't
`add_choice`, but `add_choice_edit`.  This is a convenient shortcut
which is available to quickly open an `EvEditor` when entering this choice
and going back to the menu when the editor closes.

```
        self.add_choice_edit("description", key="d", attr="db.desc")
```

When you wish to create a building menu, you just need to import your
class, create it specifying your intended caller and object to edit,
then call `open`:

```python
from <wherever> import RoomBuildingMenu

class CmdEdit(Command):

    key = "redit"

        menu = RoomBuildingMenu(self.caller, self.caller.location)
        menu.open()
```

This is a very short introduction.  For more details, see the online tutorial
(https://github.com/evennia/evennia/wiki/Building-menus) or read the
heavily-documented code below.

"""

from inspect import getfullargspec
from textwrap import dedent

from evennia import CmdSet, Command
from evennia.commands import cmdhandler
from evennia.utils.logger import log_err, log_trace
from evennia.utils.utils import class_from_module

# Constants
_MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
_CMD_NOMATCH = cmdhandler.CMD_NOMATCH
_CMD_NOINPUT = cmdhandler.CMD_NOINPUT

# Private functions
def _menu_loadfunc(caller):
    obj, attr = caller.attributes.get("_building_menu_to_edit", [None, None])
    if obj and attr:
        for part in attr.split(".")[:-1]:
            obj = getattr(obj, part)

    return getattr(obj, attr.split(".")[-1]) if obj is not None else ""

def _menu_savefunc(caller, buf):

        setattr(obj, attr.split(".")[-1], buf)

    caller.attributes.remove("_building_menu_to_edit")

def _menu_quitfunc(caller):
    caller.cmdset.add(
        BuildingMenuCmdSet,
        persistent=caller.ndb._building_menu and caller.ndb._building_menu.persistent or False,
    )
    if caller.ndb._building_menu:
        caller.ndb._building_menu.move(back=True)

def _call_or_get(value, menu=None, choice=None, string=None, obj=None, caller=None):
    """
    Call the value, if appropriate, or just return it.

    Args:
        value (any): the value to obtain.  It might be a callable (see note).

        menu (BuildingMenu, optional): the building menu to pass to value
                if it is a callable.
        choice (Choice, optional): the choice to pass to value if a callable.
        string (str, optional): the raw string to pass to value if a callback.
        obj (Object): the object to pass to value if a callable.
        caller (Account or Object, optional): the caller to pass to value
                if a callable.

    Returns:
        The value itself.  If the argument is a function, call it with
        specific arguments (see note).

    Note:
        If `value` is a function, call it with varying arguments.  The
        list of arguments will depend on the argument names in your callable.
        - An argument named `menu` will contain the building menu or None.
        - The `choice` argument will contain the choice or None.
        - The `string` argument will contain the raw string or None.
        - The `obj` argument will contain the object or None.
        - The `caller` argument will contain the caller or None.
        - Any other argument will contain the object (`obj`).
        Thus, you could define callbacks like this:
            def on_enter(menu, caller, obj):
            def on_nomatch(string, choice, menu):
            def on_leave(caller, room): # note that room will contain `obj`

    """
    if callable(value):
        # Check the function arguments
        kwargs = {}
        spec = getfullargspec(value)
        args = spec.args
        if spec.varkw:
            kwargs.update(dict(menu=menu, choice=choice, string=string, obj=obj, caller=caller))
        else:
            if "menu" in args:
                kwargs["menu"] = menu
            if "choice" in args:
                kwargs["choice"] = choice
            if "string" in args:
                kwargs["string"] = string
            if "obj" in args:
                kwargs["obj"] = obj
            if "caller" in args:
                kwargs["caller"] = caller

        # Fill missing arguments
        for arg in args:
            if arg not in kwargs:
                kwargs[arg] = obj

        # Call the function and return its return value
        return value(**kwargs)

# Helper functions, to be used in menu choices

def menu_setattr(menu, choice, obj, string):
    """
    Set the value at the specified attribute.

    Args:
        menu (BuildingMenu): the menu object.
        choice (Chocie): the specific choice.
        obj (Object): the object to modify.
        string (str): the string with the new value.

    Note:
        This function is supposed to be used as a default to
        `BuildingMenu.add_choice`, when an attribute name is specified
        (in the `attr` argument) but no function `on_nomatch` is defined.

    """
    attr = getattr(choice, "attr", None) if choice else None
    if choice is None or string is None or attr is None or menu is None:
        log_err(
            dedent(
                """
                The `menu_setattr` function was called to set the attribute {} of object {} to {},
                but the choice {} of menu {} or another information is missing.
            """.format(
                    attr, obj, repr(string), choice, menu
                )
            ).strip("\n")
        ).strip()
        return

    for part in attr.split(".")[:-1]:
        obj = getattr(obj, part)

    setattr(obj, attr.split(".")[-1], string)

def menu_quit(caller, menu):
    """
    Quit the menu, closing the CmdSet.

    Args:
        caller (Account or Object): the caller.
        menu (BuildingMenu): the building menu to close.

    Note:
        This callback is used by default when using the
        `BuildingMenu.add_choice_quit` method.  This method is called
        automatically if the menu has no parent.

    """
    if caller is None or menu is None:
        log_err(
            "The function `menu_quit` was called with missing arguments: caller={}, menu={}".format(
                caller, menu
            )
        )

    if caller.cmdset.has(BuildingMenuCmdSet):
        menu.close()
        caller.msg("Closing the building menu.")
    else:
        caller.msg("It looks like the building menu has already been closed.")

def menu_edit(caller, choice, obj):
    """
    Open the EvEditor to edit a specified attribute.

    Args:
        choice (Choice): the choice object.
        obj (Object): the object to edit.

    """
    attr = choice.attr
    caller.db._building_menu_to_edit = (obj, attr)
    caller.cmdset.remove(BuildingMenuCmdSet)
    EvEditor(
        caller,
        loadfunc=_menu_loadfunc,
        savefunc=_menu_savefunc,
        quitfunc=_menu_quitfunc,
        key="editor",
        persistent=True,
    )

# Building menu commands and CmdSet

class CmdNoInput(Command):

    """No input has been found."""

    key = _CMD_NOINPUT

        self.menu = kwargs.pop("building_menu", None)
        super().__init__(**kwargs)

        """Display the menu or choice text."""
        if self.menu:
            self.menu.display()
        else:
            log_err("When CMDNOINPUT was called, the building menu couldn't be found")
            self.caller.msg("|rThe building menu couldn't be found, remove the CmdSet.|n")
            self.caller.cmdset.delete(BuildingMenuCmdSet)

class CmdNoMatch(Command):

    key = _CMD_NOMATCH

        """Call the proper menu or redirect to nomatch."""
        raw_string = self.args.rstrip()
        if self.menu is None:
            log_err("When CMDNOMATCH was called, the building menu couldn't be found")
            return

        choice = self.menu.current_choice
        if raw_string in self.menu.keys_go_back:
            if self.menu.keys:
                self.menu.move(back=True)
            elif self.menu.parents:
                self.menu.open_parent_menu()
            else:
                self.menu.display()
        elif choice:
            if choice.nomatch(raw_string):
                self.caller.msg(choice.format_text())
        else:
            for choice in self.menu.relevant_choices:
                if choice.key.lower() == raw_string.lower() or any(
                    raw_string.lower() == alias for alias in choice.aliases
                ):
                    self.menu.move(choice.key)
                    return

            self.msg("|rUnknown command: {}|n.".format(raw_string))

class BuildingMenuCmdSet(CmdSet):

    """Building menu CmdSet."""

    key = "building_menu"
    priority = 5
    mergetype = "Replace"

        """Populates the cmdset with commands."""
        caller = self.cmdsetobj

        # The caller could recall the menu
        menu = caller.ndb._building_menu
        if menu is None:
            menu = caller.db._building_menu
            if menu:
                menu = BuildingMenu.restore(caller)

        cmds = [CmdNoInput, CmdNoMatch]
            self.add(cmd(building_menu=menu))

# Menu classes

class Choice:

    """A choice object, created by `add_choice`."""

        self,
        title,
        key=None,
        attr=None,
        text=None,
        glance=None,
        on_enter=None,
        on_nomatch=None,
        on_leave=None,
        menu=None,
        caller=None,
        obj=None,
    ):
        """Constructor.

        Args:
            title (str): the choice's title.
            key (str, optional): the key of the letters to type to access
                    the choice.  If not set, try to guess it based on the title.
            aliases (list of str, optional): the allowed aliases for this choice.
            attr (str, optional): the name of the attribute of 'obj' to set.
            text (str or callable, optional): a text to be displayed for this
                    choice.  It can be a callable.
            glance (str or callable, optional): an at-a-glance summary of the
                    sub-menu shown in the main menu.  It can be set to
                    display the current value of the attribute in the
                    main menu itself.
            menu (BuildingMenu, optional): the parent building menu.
            on_enter (callable, optional): a callable to call when the
                    caller enters into the choice.
            on_nomatch (callable, optional): a callable to call when no
                    match is entered in the choice.
            on_leave (callable, optional): a callable to call when the caller
                    leaves the choice.
            caller (Account or Object, optional): the caller.
            obj (Object, optional): the object to edit.

        """
        self.title = title
        self.key = key
        self.aliases = aliases
        self.attr = attr
        self.text = text
        self.glance = glance
        self.on_enter = on_enter
        self.on_nomatch = on_nomatch
        self.on_leave = on_leave
        self.menu = menu

        return "<Choice (title={}, key={})>".format(self.title, self.key)

    @property
    def keys(self):
        """Return a tuple of keys separated by `sep_keys`."""
        return tuple(self.key.split(self.menu.sep_keys))

    def format_text(self):
        """Format the choice text and return it, or an empty string."""
        text = ""
        if self.text:
            text = _call_or_get(
                self.text, menu=self.menu, choice=self, string="", caller=self.caller, obj=self.obj
            )
            text = dedent(text.strip("\n"))
            text = text.format(obj=self.obj, caller=self.caller)

    def enter(self, string):
        """Called when the user opens the choice.

        Args:
            string (str): the entered string.

        """
        if self.on_enter:
            _call_or_get(
                self.on_enter,
                menu=self.menu,
                choice=self,
                string=string,
                caller=self.caller,
                obj=self.obj,
            )

    def nomatch(self, string):
        """Called when the user entered something in the choice.

        Args:

        Returns:
            to_display (bool): The return value of `nomatch` if set or
            `True`.  The rule is that if `no_match` returns `True`,
            then the choice or menu is displayed.

        """
        if self.on_nomatch:
            return _call_or_get(
                self.on_nomatch,
            )

    def leave(self, string):
        """Called when the user closes the choice.

        Args:

        """
        if self.on_leave:
                self.on_leave,
            )

class BuildingMenu:

    """
    Class allowing to create and set building menus to edit specific objects.

    A building menu is somewhat similar to `EvMenu`, but designed to edit
    objects by builders, although it can be used for players in some contexts.
    You could, for instance, create a building menu to edit a room with a
    sub-menu for the room's key, another for the room's description,
    another for the room's exits, and so on.

    To add choices (simple sub-menus), you should call `add_choice` (see the
    full documentation of this method).  With most arguments, you can
    specify either a plain string or a callback.  This callback will be
    called when the operation is to be performed.

    Some methods are provided for frequent needs (see the `add_choice_*`
    methods).  Some helper functions are defined at the top of this
    module in order to be used as arguments to `add_choice`
    in frequent cases.

    """

    keys_go_back = ["@"]  # The keys allowing to go back in the menu tree
    sep_keys = "."  # The key separator for menus with more than 2 levels
    joker_key = "*"  # The special key meaning "anything" in a choice key
    min_shortcut = 1  # The minimum length of shortcuts when `key` is not set

        self,
        obj=None,
        title="Building menu: {obj}",
        keys=None,
        parents=None,
        persistent=False,
    ):
        """Constructor, you shouldn't override.  See `init` instead.

        Args:
            caller (Account or Object): the caller.
            obj (Object): the object to be edited, like a room.
            title (str, optional): the menu title.
            keys (list of str, optional): the starting menu keys (None
                    to start from the first level).
            parents (tuple, optional): information for parent menus,
                    automatically supplied.
            persistent (bool, optional): should this building menu
                    survive a reload/restart?

        Note:
            If some of these options have to be changed, it is
            preferable to do so in the `init` method and not to
            override `__init__`.  For instance:
                class RoomBuildingMenu(BuildingMenu):
                    def init(self, room):
                        self.title = "Menu for room: {obj.key}(#{obj.id})"
                        # ...

        """
        self.keys = keys or []
        self.parents = parents or ()
        self.persistent = persistent
        self.choices = []
        self.cmds = {}
        self.can_quit = False

        if obj:
            self.init(obj)
            if not parents and not self.can_quit:
                # Automatically add the menu to quit
                self.add_choice_quit(key=None)
            self._add_keys_choice()

    @property
    def current_choice(self):
        """Return the current choice or None.

        Returns:
            choice (Choice): the current choice or None.

        Note:
            We use the menu keys to identify the current position of
            the caller in the menu.  The menu `keys` hold a list of
            keys that should match a choice to be usable.

        """
        menu_keys = self.keys
        if not menu_keys:

        for choice in self.choices:
            choice_keys = choice.keys
            if len(menu_keys) == len(choice_keys):
                # Check all the intermediate keys
                common = True
                for menu_key, choice_key in zip(menu_keys, choice_keys):
                    if choice_key == self.joker_key:
                        continue

                    if not isinstance(menu_key, str) or menu_key != choice_key:
                        common = False
                        break

                if common:
                    return choice

    @property
    def relevant_choices(self):
        """Only return the relevant choices according to the current meny key.

        Returns:
            relevant (list of Choice object): the relevant choices.

        Note:

        """
        relevant = []
            if not menu_keys and len(choice_keys) == 1:
                # First level choice with the menu key empty, that's relevant
                relevant.append(choice)
            elif len(menu_keys) == len(choice_keys) - 1:
                        continue

                        break

                if common:
                    relevant.append(choice)

        return relevant

    def _save(self):
        """Save the menu in a attributes on the caller.

        If `persistent` is set to `True`, also save in a persistent attribute.

        """
        self.caller.ndb._building_menu = self

        if self.persistent:
            self.caller.db._building_menu = {
                "class": type(self).__module__ + "." + type(self).__name__,
                "obj": self.obj,
                "title": self.title,
                "keys": self.keys,
                "parents": self.parents,
                "persistent": self.persistent,
            }

    def _add_keys_choice(self):
        """Add the choices' keys if some choices don't have valid keys."""
        # If choices have been added without keys, try to guess them
            if not choice.key:
                title = strip_ansi(choice.title.strip()).lower()
                length = self.min_shortcut
                while length <= len(title):
                    i = 0
                    while i < len(title) - length + 1:
                        guess = title[i : i + length]
                        if guess not in self.cmds:
                            choice.key = guess
                            break

                        i += 1

                    if choice.key:
                        break

                    length += 1

                if choice.key:
                    self.cmds[choice.key] = choice
                else:
                    raise ValueError("Cannot guess the key for {}".format(choice))

    def init(self, obj):
        """Create the sub-menu to edit the specified object.

        Args:
            obj (Object): the object to edit.

        Note:
            This method is probably to be overridden in your subclasses.
            Use `add_choice` and its variants to create menu choices.

        """
        pass

    def add_choice(
        self,
        title,
        key=None,
    ):
        """
        Add a choice, a valid sub-menu, in the current builder menu.

        Args:
                    the sub-neu.  If not set, try to guess it based on the
                    choice title.
            aliases (list of str, optional): the aliases for this choice.
                    This is really useful if you want to edit an
                    attribute of the object (that's a frequent need).  If
                    you don't want to do so, just use the `on_*` arguments.
            text (str or callable, optional): a text to be displayed when
                    the menu is opened  It can be a callable.
                    caller enters into this choice.
            on_nomatch (callable, optional): a callable to call when
                    the caller enters something in this choice.  If you
                    don't set this argument but you have specified
                    `attr`, then `obj`.`attr` will be set with the value
                    entered by the user.
            on_leave (callable, optional): a callable to call when the
                    caller leaves the choice.

        Returns:
            choice (Choice): the newly-created choice.

        Raises:
            ValueError if the choice cannot be added.

        Note:
            Most arguments can be callables, like functions.  This has the
            advantage of allowing great flexibility.  If you specify
            a callable in most of the arguments, the callable should return
            the value expected by the argument (a str more often than
            not).  For instance, you could set a function to be called
            to get the menu text, which allows for some filtering:
                def text_exits(menu):
                    return "Some text to display"
                    def init(self):
                        self.add_choice("exits", key="x", text=text_exits)

            The allowed arguments in a callable are specific to the
            argument names (they are not sensitive to orders, not all
            arguments have to be present).  For more information, see
            `_call_or_get`.

        """
        key = key or ""
        key = key.lower()
        aliases = aliases or []
        aliases = [a.lower() for a in aliases]
        if attr and on_nomatch is None:
            on_nomatch = menu_setattr

        if key and key in self.cmds:
                "A conflict exists between {} and {}, both use key or alias {}".format(
                    self.cmds[key], title, repr(key)
                )
            )

        if attr:
            if glance is None:
                glance = "{obj." + attr + "}"
            if text is None:
                text = """
                        -------------------------------------------------------------------------------
                        {attr} for {{obj}}(#{{obj.id}})

                        You can change this value simply by entering it.
                        Use |y{back}|n to go back to the main menu.

                        Current value: |c{{{obj_attr}}}|n
                """.format(
                    attr=attr, obj_attr="obj." + attr, back="|n or |y".join(self.keys_go_back)
                )

        choice = Choice(
            title,
            key=key,
            attr=attr,
            text=text,
            glance=glance,
            on_enter=on_enter,
            on_nomatch=on_nomatch,
            on_leave=on_leave,
            menu=self,
            caller=self.caller,
            obj=self.obj,
        )
        self.choices.append(choice)
        if key:
            self.cmds[key] = choice

        for alias in aliases:
            self.cmds[alias] = choice

        return choice

    def add_choice_edit(
        self,
        title="description",
        key="d",
        attr="db.desc",
        glance="\n   {obj.db.desc}",
    ):
        """
        Add a simple choice to edit a given attribute in the EvEditor.

        Args:
            title (str, optional): the choice's title.
            key (str, optional): the choice's key.
            aliases (list of str, optional): the choice's aliases.
            glance (str or callable, optional): the at-a-glance description.
            on_enter (callable, optional): a different callable to edit
                    the attribute.

        Returns:

        Note:
            This is just a shortcut method, calling `add_choice`.
            If `on_enter` is not set, use `menu_edit` which opens
            an EvEditor to edit the specified attribute.
            When the caller closes the editor (with :q), the menu
            will be re-opened.

        """
        on_enter = on_enter or menu_edit
        return self.add_choice(
            title, key=key, aliases=aliases, attr=attr, glance=glance, on_enter=on_enter, text=""
        )

    def add_choice_quit(self, title="quit the menu", key="q", aliases=None, on_enter=None):
        """
        Add a simple choice just to quit the building menu.

        Args:
            on_enter (callable, optional): a different callable
                    to quit the building menu.

        Note:
            If `on_enter` is not set, use `menu_quit` which simply
            closes the menu and displays a message.  It also
            removes the CmdSet from the caller.  If you supply
            another callable instead, make sure to do the same.

        """
        on_enter = on_enter or menu_quit
        self.can_quit = True
        return self.add_choice(title, key=key, aliases=aliases, on_enter=on_enter)

    def open(self):
        """Open the building menu for the caller.

        Note:
            This method should be called once when the building menu
            has been instanciated.  From there, the building menu will
            be re-created automatically when the server
            reloads/restarts, assuming `persistent` is set to `True`.

        """
        self._save()

        # Remove the same-key cmdset if exists
        if caller.cmdset.has(BuildingMenuCmdSet):
            caller.cmdset.remove(BuildingMenuCmdSet)

        self.caller.cmdset.add(BuildingMenuCmdSet, persistent=self.persistent)
        self.display()

    def open_parent_menu(self):
        """Open the parent menu, using `self.parents`.

        Note:
            You probably don't need to call this method directly,
            since the caller can go back to the parent menu using the
            `keys_go_back` automatically.

        """
        parents = list(self.parents)
        if parents:
            parent_class, parent_obj, parent_keys = parents[-1]
            del parents[-1]

            if self.caller.cmdset.has(BuildingMenuCmdSet):
                self.caller.cmdset.remove(BuildingMenuCmdSet)

            try:
                menu_class = class_from_module(parent_class)
                log_trace(
                    "BuildingMenu: attempting to load class {} failed".format(repr(parent_class))
                )
                return

            # Create the parent menu
            try:
                building_menu = menu_class(
                    self.caller, parent_obj, keys=parent_keys, parents=tuple(parents)
                )
                    "An error occurred while creating building menu {}".format(repr(parent_class))
                )
                return
            else:
                return building_menu.open()

    def open_submenu(self, submenu_class, submenu_obj, parent_keys=None):
        """
        Open a sub-menu, closing the current menu and opening the new one.

        Args:
            submenu_class (str): the submenu class as a Python path.
            submenu_obj (Object): the object to give to the submenu.
            parent_keys (list of str, optional): the parent keys when
                    the submenu is closed.

        Note:
            When the user enters `@` in the submenu, she will go back to
            the current menu, with the `parent_keys` set as its keys.
            Therefore, you should set it on the keys of the choice that
            should be opened when the user leaves the submenu.

        Returns:
            new_menu (BuildingMenu): the new building menu or None.

        """
        parent_keys = parent_keys or []
        parents.append((type(self).__module__ + "." + type(self).__name__, self.obj, parent_keys))
        if self.caller.cmdset.has(BuildingMenuCmdSet):
            self.caller.cmdset.remove(BuildingMenuCmdSet)

        # Shift to the new menu
        try:
            menu_class = class_from_module(submenu_class)
            log_trace(
                "BuildingMenu: attempting to load class {} failed".format(repr(submenu_class))
            )
            return

        # Create the submenu
        try:
            building_menu = menu_class(self.caller, submenu_obj, parents=parents)
                "An error occurred while creating building menu {}".format(repr(submenu_class))
            )
            return
        else:
            return building_menu.open()

    def move(self, key=None, back=False, quiet=False, string=""):
        """
        Move inside the menu.

        Args:
            key (any): the portion of the key to add to the current
                    menu keys.  If you wish to go back in the menu
                    tree, don't provide a `key`, just set `back` to `True`.
            back (bool, optional): go back in the menu (`False` by default).
            quiet (bool, optional): should the menu or choice be
                    displayed afterward?
            string (str, optional): the string sent by the caller to move.

        Note:
            This method will need to be called directly should you
            use more than two levels in your menu.  For instance,
            in your room menu, if you want to have an "exits"
            option, and then be able to enter "north" in this
            choice to edit an exit.  The specific exit choice
            could be a different menu (with a different class), but
            it could also be an additional level in your original menu.
            If that's the case, you will need to use this method.

        """
        choice = self.current_choice
        if choice:
            choice.leave("")

        if not back:  # Move forward
            if not key:
                raise ValueError("you are asking to move forward, you should specify a key.")

            self.keys.append(key)
        else:  # Move backward
            if not self.keys:
                    "you already are at the top of the tree, you cannot move backward."
                )

            del self.keys[-1]

        if choice:
            choice.enter(string)

            self.display()

    def close(self):
        """Close the building menu, removing the CmdSet."""
        if self.caller.attributes.has("_building_menu"):
            self.caller.attributes.remove("_building_menu")
        if self.caller.nattributes.has("_building_menu"):
            self.caller.nattributes.remove("_building_menu")

    # Display methods.  Override for customization
    def display_title(self):
        """Return the menu title to be displayed."""
        return _call_or_get(self.title, menu=self, obj=self.obj, caller=self.caller).format(
            obj=self.obj
        )

    def display_choice(self, choice):
        """Display the specified choice.

        Args:
            choice (Choice): the menu choice.

        """
        title = _call_or_get(
            choice.title, menu=self, choice=choice, obj=self.obj, caller=self.caller
        )
        clear_title = title.lower()
        pos = clear_title.find(choice.key.lower())
        ret = " "
        if pos >= 0:
            ret += title[:pos] + "[|y" + choice.key.title() + "|n]" + title[pos + len(choice.key) :]
        else:
            ret += "[|y" + choice.key.title() + "|n] " + title

        if choice.glance:
            glance = _call_or_get(
                choice.glance, menu=self, choice=choice, caller=self.caller, string="", obj=self.obj
            )
            glance = glance.format(obj=self.obj, caller=self.caller)

            ret += ": " + glance

        return ret

    def display(self):
        """Display the entire menu or a single choice, depending on the keys."""
        if self.keys and choice:
            text = choice.format_text()
        else:
            text = self.display_title() + "\n"
            for choice in self.relevant_choices:
                text += "\n" + self.display_choice(choice)

        self.caller.msg(text)

    def restore(caller):
        """Restore the building menu for the caller.

        Args:

        Note:
            This method should be automatically called if a menu is
            saved in the caller, but the object itself cannot be found.

        """
        menu = caller.db._building_menu
        if menu:
            class_name = menu.get("class")
            if not class_name:
                log_err(
                    "BuildingMenu: on caller {}, a persistent attribute holds building menu "
                    "data, but no class could be found to restore the menu".format(caller)
                )
                return

            try:
                menu_class = class_from_module(class_name)
                    "BuildingMenu: attempting to load class {} failed".format(repr(class_name))
                )
                return

            # Create the menu
            obj = menu.get("obj")
            keys = menu.get("keys")
            title = menu.get("title", "")
            parents = menu.get("parents")
            persistent = menu.get("persistent", False)
            try:
                    caller, obj, title=title, keys=keys, parents=parents, persistent=persistent
                )
                    "An error occurred while creating building menu {}".format(repr(class_name))
                )
                return

            return building_menu

# Generic building menu and command
class GenericBuildingMenu(BuildingMenu):

    """A generic building menu, allowing to edit any object.

    This is more a demonstration menu.  By default, it allows to edit the
    object key and description.  Nevertheless, it will be useful to demonstrate
    how building menus are meant to be used.

    """

        """Build the meny, adding the 'key' and 'description' choices.

        Args:
            obj (Object): any object to be edited, like a character or room.

        Note:
            The 'quit' choice will be automatically added, though you can
            call `add_choice_quit` to add this choice with different options.

        """
        self.add_choice(
            "key",
            key="k",
            attr="key",
            glance="{obj.key}",
            text="""
                -------------------------------------------------------------------------------
                Editing the key of {{obj.key}}(#{{obj.id}})

                You can change the simply by entering it.
                Use |y{back}|n to go back to the main menu.

                Current key: |c{{obj.key}}|n
                back="|n or |y".join(self.keys_go_back)
            ),
        )

class GenericBuildingCmd(Command):

    """
    Generic building command.

    Syntax:
      @edit [object]

    Open a building menu to edit the specified object.  This menu allows to
    change the object's key and description.

    Examples:
      @edit here
      @edit self
      @edit #142

    """

    key = "@edit"

        if not self.args.strip():
            self.msg("You should provide an argument to this function: the object to edit.")
            return

        obj = self.caller.search(self.args.strip(), global_search=True)
        if not obj:
            return

        menu = GenericBuildingMenu(self.caller, obj)

"""
Building menu tests.

"""

from evennia.commands.default.tests import BaseEvenniaCommandTest

from .building_menu import BuildingMenu, CmdNoMatch

class Submenu(BuildingMenu):
    def init(self, exit):
        self.add_choice("title", key="t", attr="key")

class TestBuildingMenu(BaseEvenniaCommandTest):
        self.menu = BuildingMenu(caller=self.char1, obj=self.room1, title="test")
        self.menu.add_choice("title", key="t", attr="key")

        """Try to quit the building menu."""
        self.assertFalse(self.char1.cmdset.has("building_menu"))
        self.menu.open()
        self.assertTrue(self.char1.cmdset.has("building_menu"))
        self.call(CmdNoMatch(building_menu=self.menu), "q")
        # char1 tries to quit the editor

    def test_setattr(self):
        """Test the simple setattr provided by building menus."""
        self.call(CmdNoMatch(building_menu=self.menu), "t")
        self.assertIsNotNone(self.menu.current_choice)
        self.call(CmdNoMatch(building_menu=self.menu), "some new title")
        self.call(CmdNoMatch(building_menu=self.menu), "@")
        self.assertIsNone(self.menu.current_choice)
        self.assertEqual(self.room1.key, "some new title")

    def test_add_choice_without_key(self):
        """Try to add choices without keys."""
        choices = []
        for i in range(20):
            choices.append(self.menu.add_choice("choice", attr="test"))
        self.menu._add_keys_choice()
        keys = [
            "c",
            "h",
            "o",
            "i",
            "e",
            "ch",
            "ho",
            "oi",
            "ic",
            "ce",
            "cho",
            "hoi",
            "oic",
            "ice",
            "choi",
            "hoic",
            "oice",
            "choic",
            "hoice",
            "choice",
        ]
            self.assertEqual(choices[i].key, keys[i])

        # Adding another key of the same title would break, no more available shortcut
        self.menu.add_choice("choice", attr="test")
        with self.assertRaises(ValueError):
            self.menu._add_keys_choice()

    def test_callbacks(self):
        """Test callbacks in menus."""
        self.room1.key = "room1"

        def on_enter(caller, menu):
            caller.msg("on_enter:{}".format(menu.title))

        def on_nomatch(caller, string, choice):
            caller.msg("on_nomatch:{},{}".format(string, choice.key))

        def on_leave(caller, obj):
            caller.msg("on_leave:{}".format(obj.key))

        self.menu.add_choice(
            "test", key="e", on_enter=on_enter, on_nomatch=on_nomatch, on_leave=on_leave
        )
        self.call(CmdNoMatch(building_menu=self.menu), "e", "on_enter:test")
        self.call(CmdNoMatch(building_menu=self.menu), "ok", "on_nomatch:ok,e")
        self.call(CmdNoMatch(building_menu=self.menu), "@", "on_leave:room1")

    def test_multi_level(self):
        """Test multi-level choices."""

        # Creaste three succeeding menu (t2 is contained in t1, t3 is contained in t2)
        def on_nomatch_t1(caller, menu):
            menu.move("whatever")  # this will be valid since after t1 is a joker

        def on_nomatch_t2(caller, menu):
            menu.move("t3")  # this time the key matters

        t1 = self.menu.add_choice("what", key="t1", on_nomatch=on_nomatch_t1)
        t2 = self.menu.add_choice("and", key="t1.*", on_nomatch=on_nomatch_t2)
        t3 = self.menu.add_choice("why", key="t1.*.t3")

        # Move into t1
        self.assertIn(t1, self.menu.relevant_choices)
        self.assertNotIn(t2, self.menu.relevant_choices)
        self.assertNotIn(t3, self.menu.relevant_choices)
        self.call(CmdNoMatch(building_menu=self.menu), "t1")
        self.assertEqual(self.menu.current_choice, t1)
        self.assertNotIn(t1, self.menu.relevant_choices)
        self.assertIn(t2, self.menu.relevant_choices)

        # Move into t2
        self.call(CmdNoMatch(building_menu=self.menu), "t2")
        self.assertEqual(self.menu.current_choice, t2)
        self.assertIn(t3, self.menu.relevant_choices)

        # Move into t3
        self.call(CmdNoMatch(building_menu=self.menu), "t3")
        self.assertEqual(self.menu.current_choice, t3)

        # Move back to t2

        # Move back into t1

        # Moves back to the main menu

    def test_submenu(self):
        """Test to add sub-menus."""

        def open_exit(menu):
            menu.open_submenu("evennia.contrib.base_systems.building_menu.tests.Submenu", self.exit)

        self.menu.add_choice("exit", key="x", on_enter=open_exit)
        self.call(CmdNoMatch(building_menu=self.menu), "x")
        self.menu = self.char1.ndb._building_menu
        self.call(CmdNoMatch(building_menu=self.menu), "in")
        self.assertEqual(self.char1.ndb._building_menu.obj, self.room1)
        self.assertEqual(self.exit.key, "in")

"""
Test Color markup.

"""

import re

from evennia.utils.test_resources import BaseEvenniaTest

from . import color_markups

class TestColorMarkup(BaseEvenniaTest):
    """
    Note: Normally this would be tested by importing the ansi parser and run
    the mappings through it. This is not possible since the ansi module creates
    its mapping at the module/class level; since the ansi module is used by so
    many other modules it appears that trying to overload
    settings to test it causes issues with unrelated tests.
    """

    def test_curly_markup(self):
        ansi_map = color_markups.CURLY_COLOR_ANSI_EXTRA_MAP
        self.assertIsNotNone(re.match(re.escape(ansi_map[7][0]), "{r"))
        self.assertIsNotNone(re.match(re.escape(ansi_map[-1][0]), "{[X"))
        xterm_fg = color_markups.CURLY_COLOR_XTERM256_EXTRA_FG
        self.assertIsNotNone(re.match(xterm_fg[0], "{001"))
        self.assertIsNotNone(re.match(xterm_fg[0], "{123"))
        self.assertIsNotNone(re.match(xterm_fg[0], "{455"))
        xterm_bg = color_markups.CURLY_COLOR_XTERM256_EXTRA_BG
        self.assertIsNotNone(re.match(xterm_bg[0], "{[001"))
        self.assertIsNotNone(re.match(xterm_bg[0], "{[123"))
        self.assertIsNotNone(re.match(xterm_bg[0], "{[455"))
        xterm_gfg = color_markups.CURLY_COLOR_XTERM256_EXTRA_GFG
        self.assertIsNotNone(re.match(xterm_gfg[0], "{=h"))
        self.assertIsNotNone(re.match(xterm_gfg[0], "{=e"))
        self.assertIsNotNone(re.match(xterm_gfg[0], "{=w"))
        xterm_gbg = color_markups.CURLY_COLOR_XTERM256_EXTRA_GBG
        self.assertIsNotNone(re.match(xterm_gbg[0], "{[=a"))
        self.assertIsNotNone(re.match(xterm_gbg[0], "{[=k"))
        self.assertIsNotNone(re.match(xterm_gbg[0], "{[=z"))
        bright_map = color_markups.CURLY_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP
        self.assertEqual(bright_map[0][1], "{[500")
        self.assertEqual(bright_map[-1][1], "{[222")

    def test_mux_markup(self):
        ansi_map = color_markups.MUX_COLOR_ANSI_EXTRA_MAP
        self.assertIsNotNone(re.match(re.escape(ansi_map[10][0]), "%cr"))
        self.assertIsNotNone(re.match(re.escape(ansi_map[-1][0]), "%cX"))
        xterm_fg = color_markups.MUX_COLOR_XTERM256_EXTRA_FG
        self.assertIsNotNone(re.match(xterm_fg[0], "%c001"))
        self.assertIsNotNone(re.match(xterm_fg[0], "%c123"))
        self.assertIsNotNone(re.match(xterm_fg[0], "%c455"))
        xterm_bg = color_markups.MUX_COLOR_XTERM256_EXTRA_BG
        self.assertIsNotNone(re.match(xterm_bg[0], "%c[001"))
        self.assertIsNotNone(re.match(xterm_bg[0], "%c[123"))
        self.assertIsNotNone(re.match(xterm_bg[0], "%c[455"))
        xterm_gfg = color_markups.MUX_COLOR_XTERM256_EXTRA_GFG
        self.assertIsNotNone(re.match(xterm_gfg[0], "%c=h"))
        self.assertIsNotNone(re.match(xterm_gfg[0], "%c=e"))
        self.assertIsNotNone(re.match(xterm_gfg[0], "%c=w"))
        xterm_gbg = color_markups.MUX_COLOR_XTERM256_EXTRA_GBG
        self.assertIsNotNone(re.match(xterm_gbg[0], "%c[=a"))
        self.assertIsNotNone(re.match(xterm_gbg[0], "%c[=k"))
        self.assertIsNotNone(re.match(xterm_gbg[0], "%c[=z"))
        bright_map = color_markups.MUX_COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP
        self.assertEqual(bright_map[0][1], "%c[500")
        self.assertEqual(bright_map[-1][1], "%c[222")

"""
Components - ChrisLR 2022

This is a basic Component System.
It allows you to use components on typeclasses using a simple syntax.
This helps writing isolated code and reusing it over multiple objects.

See the docs for more information.
"""

from evennia.contrib.base_systems.components.component import Component
from evennia.contrib.base_systems.components.dbfield import DBField, NDBField, TagField
from evennia.contrib.base_systems.components.holder import (
    ComponentHolderMixin,
    ComponentProperty,
)

def get_component_class(component_name):
    subclasses = Component.__subclasses__()
    component_class = next((sc for sc in subclasses if sc.name == component_name), None)
    if component_class is None:
        message = (
            f"Component named {component_name} has not been found. "
            f"Make sure it has been imported before being used."
        )
        raise Exception(message)

    return component_class

"""

This file contains the base class to inherit for creating new components.
"""
import itertools

class Component:
    """
    This is the base class for components.
    Any component must inherit from this class to be considered for usage.

    Each Component must supply the name, it is used as a slot name but also part of the attribute key.
    """

    name = ""

    def __init__(self, host=None):
        assert self.name, "All Components must have a Name"
        self.host = host

    def default_create(cls, host):
        """
        This is called when the host is created
         and should return the base initialized state of a component.

        Args:
            host (object): The host typeclass instance

        Returns:
            Component: The created instance of the component

        """
        new = cls(host)
        return new

    def create(cls, host, **kwargs):
        """
        This is the method to call when supplying kwargs to initialize a component.

        Args:
            **kwargs: Key-Value of default values to replace.
                      To persist the value, the key must correspond to a DBField.

        Returns:

        """

        new = cls.default_create(host)
        for key, value in kwargs.items():
            setattr(new, key, value)

        return new

    def cleanup(self):
        """
        This deletes all component attributes from the host's db
        """
        for attribute in self._all_db_field_names:
            delattr(self, attribute)

    def load(cls, host):
        """
        Loads a component instance
         This is called whenever a component is loaded (ex: Server Restart)

        Args:

        Returns:
            Component: The loaded instance of the component

        """

        return cls(host)

    def at_added(self, host):
        """
        This is the method called when a component is registered on a host.

        Args:

        """

        if self.host:
            if self.host == host:
                return
            else:
                raise ComponentRegisterError("Components must not register twice!")

    def at_removed(self, host):
        """
        This is the method called when a component is removed from a host.

        Args:

        """
        if host != self.host:
            raise ComponentRegisterError("Component attempted to remove from the wrong host.")
        self.host = None

    @property
    def attributes(self):
        """
        Shortcut property returning the host's AttributeHandler.

        Returns:
            AttributeHandler: The Host's AttributeHandler

        """
        return self.host.attributes

    @property
    def nattributes(self):
        """
        Shortcut property returning the host's In-Memory AttributeHandler (Non Persisted).

        Returns:
            AttributeHandler: The Host's In-Memory AttributeHandler

        """
        return self.host.nattributes

    @property
    def _all_db_field_names(self):
        return itertools.chain(self.db_field_names, self.ndb_field_names)

    @property
    def db_field_names(self):
        db_fields = getattr(self, "_db_fields", {})
        return db_fields.keys()

    @property
    def ndb_field_names(self):
        ndb_fields = getattr(self, "_ndb_fields", {})
        return ndb_fields.keys()

    @property
    def tag_field_names(self):
        tag_fields = getattr(self, "_tag_fields", {})
        return tag_fields.keys()

class ComponentRegisterError(Exception):
    pass

"""

This file contains the Descriptors used to set Fields in Components
"""
from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty

class DBField(AttributeProperty):
    """
    Component Attribute Descriptor.
    Allows you to set attributes related to a component on the class.
    It uses AttributeProperty under the hood but prefixes the key with the component name.
    """

        """
        Called when descriptor is first assigned to the class.

        Args:
            owner (object): The component classF on which this is set
            name (str): The name that was used to set the DBField.
        """
        key = f"{owner.name}::{name}"
        self._key = key
        db_fields = getattr(owner, "_db_fields", None)
        if db_fields is None:
            db_fields = {}
            setattr(owner, "_db_fields", db_fields)
        db_fields[name] = self

class NDBField(NAttributeProperty):
    """
    Component In-Memory Attribute Descriptor.
    Allows you to set in-memory attributes related to a component on the class.
    It uses NAttributeProperty under the hood but prefixes the key with the component name.
    """

        """

        Args:
            owner (object): The component class on which this is set
        """
        ndb_fields = getattr(owner, "_ndb_fields", None)
        if ndb_fields is None:
            ndb_fields = {}
            setattr(owner, "_ndb_fields", ndb_fields)
        ndb_fields[name] = self

class TagField:
    """
    Component Tags Descriptor.
    Allows you to set Tags related to a component on the class.
    The tags are set with a prefixed category, so it can support
    multiple tags or enforce a single one.

    Default value of a tag is added when the component is registered.
    Tags are removed if the component itself is removed.
    """

    def __init__(self, default=None, enforce_single=False):
        self._category_key = None
        self._default = default
        self._enforce_single = enforce_single

        """
        Called when TagField is first assigned to the class.
        It is called with the component class and the name of the field.
        """
        self._category_key = f"{owner.name}::{name}"
        tag_fields = getattr(owner, "_tag_fields", None)
        if tag_fields is None:
            tag_fields = {}
            setattr(owner, "_tag_fields", tag_fields)
        tag_fields[name] = self

        """
        Called when retrieving the value of the TagField.
        It is called with the component instance and the class.
        """
        tag_value = instance.host.tags.get(
            default=self._default,
            category=self._category_key,
        )
        return tag_value

        """
        Called when setting a value on the TagField.
        It is called with the component instance and the value.
        """

        tag_handler = instance.host.tags
        if self._enforce_single:
            tag_handler.clear(category=self._category_key)

        tag_handler.add(
            key=value,
        )

    def __delete__(self, instance):
        """
        Used when 'del' is called on the TagField.
        It is called with the component instance.
        """
        instance.host.tags.clear(category=self._category_key)

"""

This file contains the classes that allow a typeclass to use components.
"""

from evennia.contrib.base_systems import components
from evennia.contrib.base_systems.components import signals

class ComponentProperty:
    """
    This allows you to register a component on a typeclass.
    Components registered with this property are automatically added
    to any instance of this typeclass.

    Defaults can be overridden for this typeclass by passing kwargs
    """

    def __init__(self, component_name, **kwargs):
        """
        Initializes the descriptor

        Args:
            component_name (str): The name of the component
            **kwargs (any): Key=Values overriding default values of the component
        """
        self.component_name = component_name
        self.values = kwargs

        component = instance.components.get(self.component_name)
        return component

        raise Exception("Cannot set a class property")

        # Retrieve the class_components set on the direct class only
        class_components = owner.__dict__.get("_class_components")
        if not class_components:
            # Create a new list, including inherited class components
            class_components = list(getattr(owner, "_class_components", []))
            setattr(owner, "_class_components", class_components)

        class_components.append((self.component_name, self.values))

class ComponentHandler:
    """
    This is the handler that will be added to any typeclass that inherits from ComponentHolder.
    It lets you add or remove components and will load components as needed.
    It stores the list of registered components on the host .db with component_names as key.
    """

    def __init__(self, host):
        self._loaded_components = {}

    def add(self, component):
        """
        Method to add a Component to a host.
        It caches the loaded component and appends its name to the host's component name list.
        It will also call the component's 'at_added' method, passing its host.

        Args:
            component (object): The 'loaded' component instance to add.

        """
        self._set_component(component)
        self.db_names.append(component.name)
        self._add_component_tags(component)
        component.at_added(self.host)
        self.host.signals.add_object_listeners_and_responders(component)

    def add_default(self, name):
        """
        Method to add a Component initialized to default values on a host.
        It will retrieve the proper component and instanciate it with 'default_create'.
        It will cache this new component and add it to its list.

        Args:
            name (str): The name of the component class to add.

        """
        component = components.get_component_class(name)
        if not component:
            raise ComponentDoesNotExist(f"Component {name} does not exist.")

        new_component = component.default_create(self.host)
        self._set_component(new_component)
        self.db_names.append(name)
        self._add_component_tags(new_component)
        new_component.at_added(self.host)
        self.host.signals.add_object_listeners_and_responders(new_component)

    def _add_component_tags(self, component):
        """
        Private method that adds the Tags set on a Component via TagFields
        It will also add the name of the component so objects can be filtered
        by the components the implement.

        Args:
            component (object): The component instance that is added.
        """
        self.host.tags.add(component.name, category="components")
        for tag_field_name in component.tag_field_names:
            default_tag = type(component).__dict__[tag_field_name]._default
            if default_tag:
                setattr(component, tag_field_name, default_tag)

    def remove(self, component):
        """
        Method to remove a component instance from a host.
        It removes the component from the cache and listing.
        It will call the component's 'at_removed' method.

        Args:
            component (object): The component instance to remove.

        """
        component_name = component.name
        if component_name in self._loaded_components:
            self._remove_component_tags(component)
            component.at_removed(self.host)
            self.db_names.remove(component_name)
            self.host.signals.remove_object_listeners_and_responders(component)
            del self._loaded_components[component_name]
        else:
            message = (
                f"Cannot remove {component_name} from {self.host.name} as it is not registered."
            )
            raise ComponentIsNotRegistered(message)

    def remove_by_name(self, name):
        """

        Args:
            name (str): The name of the component to remove.

        """
        instance = self.get(name)
        if not instance:
            message = f"Cannot remove {name} from {self.host.name} as it is not registered."

        self._remove_component_tags(instance)
        instance.at_removed(self.host)
        self.host.signals.remove_object_listeners_and_responders(instance)
        self.db_names.remove(name)

        del self._loaded_components[name]

    def _remove_component_tags(self, component):
        """
        Private method that will remove the Tags set on a Component via TagFields
        It will also remove the component name tag.

        Args:
            component (object): The component instance that is removed.
        """
        self.host.tags.remove(component.name, category="components")
            delattr(component, tag_field_name)

    def get(self, name):
        """
        Method to retrieve a cached Component instance by its name.

        Args:
            name (str): The name of the component to retrieve.

        """
        return self._loaded_components.get(name)

    def has(self, name):
        """
        Method to check if a component is registered and ready.

        Args:
            name (str): The name of the component.

        """
        return name in self._loaded_components

    def initialize(self):
        """
        Method that loads and caches each component currently registered on the host.
        It retrieves the names from the registered listing and calls 'load' on each
        prototype class that can be found from this listing.

        """
        component_names = self.db_names
        if not component_names:
            return

        for component_name in component_names:
            component = components.get_component_class(component_name)
            if component:
                component_instance = component.load(self.host)
                self._set_component(component_instance)
                self.host.signals.add_object_listeners_and_responders(component_instance)
            else:
                message = (
                    f"Could not initialize runtime component {component_name} of {self.host.name}"
                )
                raise ComponentDoesNotExist(message)

    def _set_component(self, component):
        self._loaded_components[component.name] = component

    @property
    def db_names(self):
        """
        Property shortcut to retrieve the registered component names

        Returns:
            component_names (iterable): The name of each component that is registered

        """
        return self.host.attributes.get("component_names")

    def __getattr__(self, name):
        return self.get(name)

class ComponentHolderMixin:
    """
    Mixin to add component support to a typeclass

    Components are set on objects using the component.name as an object attribute.
    All registered components are initialized on the typeclass.
    They will be of None value if not present in the class components or runtime components.
    """

        """
        Method that initializes the ComponentHandler.
        """
        super(ComponentHolderMixin, self).at_init()
        setattr(self, "_component_handler", ComponentHandler(self))
        setattr(self, "_signal_handler", signals.SignalsHandler(self))
        self.components.initialize()
        self.signals.trigger("at_after_init")

    def at_post_puppet(self, *args, **kwargs):
        super().at_post_puppet(*args, **kwargs)
        self.signals.trigger("at_post_puppet", *args, **kwargs)

    def at_post_unpuppet(self, *args, **kwargs):
        super().at_post_unpuppet(*args, **kwargs)
        self.signals.trigger("at_post_unpuppet", *args, **kwargs)

        """
        Method that initializes the ComponentHandler, creates and registers all
        components that were set on the typeclass using ComponentProperty.
        """
        super().basetype_setup()
        component_names = []
        class_components = getattr(self, "_class_components", ())
        for component_name, values in class_components:
            component_class = components.get_component_class(component_name)
            component = component_class.create(self, **values)
            component_names.append(component_name)
            self.components._loaded_components[component_name] = component
            self.signals.add_object_listeners_and_responders(component)

        self.db.component_names = component_names
        self.signals.trigger("at_basetype_setup")

    def basetype_posthook_setup(self):
        """
        Method that add component related tags that were set using ComponentProperty.
        """
        super().basetype_posthook_setup()
        for component in self.components._loaded_components.values():
            self.components._add_component_tags(component)

    @property
    def components(self) -> ComponentHandler:
        """
        Property getter to retrieve the component_handler.
        Returns:
            ComponentHandler: This Host's ComponentHandler
        """
        return getattr(self, "_component_handler", None)

    @property
    def cmp(self) -> ComponentHandler:
        """
        Shortcut Property getter to retrieve the component_handler.
        Returns:
        """
        return self.components

    @property
    def signals(self) -> signals.SignalsHandler:
        return getattr(self, "_signal_handler", None)

class ComponentDoesNotExist(Exception):
    pass

class ComponentIsNotRegistered(Exception):
    pass

"""

This file contains classes functions related to signals.
"""

def as_listener(func=None, signal_name=None):
    """
    Decorator style function that marks a method to be connected as listener.
    It will use the provided signal name and default to the decorated function name.

    Args:
        func (callable): The method to mark as listener
        signal_name (str): The name of the signal to listen to, defaults to function name.
    """
    if not func and signal_name:

        def wrapper(func):
            func._listener_signal_name = signal_name
            return func

        return wrapper

    signal_name = func.__name__
    func._listener_signal_name = signal_name

def as_responder(func=None, signal_name=None):
    """
    Decorator style function that marks a method to be connected as responder.

    Args:
        func (callable): The method to mark as responder
        signal_name (str): The name of the signal to respond to, defaults to function name.
    """

            func._responder_signal_name = signal_name

    func._responder_signal_name = signal_name

class SignalsHandler(object):
    """
    This object handles all about signals.
    It holds the connected listeners and responders.
    It allows triggering signals or querying responders.
    """

        self.listeners = {}
        self.responders = {}
        self.add_object_listeners_and_responders(host)

    def add_listener(self, signal_name, callback):
        """
        Connect a listener to a specific signal.

        Args:
            signal_name (str): The name of the signal to listen to
            callback (callable): The callable that is called when the signal is triggered
        """

        signal_listeners = self.listeners.setdefault(signal_name, [])
        if callback not in signal_listeners:
            signal_listeners.append(callback)

    def add_responder(self, signal_name, callback):
        """
        Connect a responder to a specific signal.

        Args:
            signal_name (str): The name of the signal to respond to
            callback (callable): The callable that is called when the signal is queried
        """

        signal_responders = self.responders.setdefault(signal_name, [])
        if callback not in signal_responders:
            signal_responders.append(callback)

    def remove_listener(self, signal_name, callback):
        """
        Removes a listener for a specific signal.

        Args:
            signal_name (str): The name of the signal to disconnect from
            callback (callable): The callable that was used to connect
        """

        signal_listeners = self.listeners.get(signal_name)
        if not signal_listeners:
            return

        if callback in signal_listeners:
            signal_listeners.remove(callback)

    def remove_responder(self, signal_name, callback):
        """
        Removes a responder for a specific signal.

        Args:
        """
        signal_responders = self.responders.get(signal_name)
        if not signal_responders:
            return

        if callback in signal_responders:
            signal_responders.remove(callback)

    def trigger(self, signal_name, *args, **kwargs):
        """
        Triggers a specific signal with specified args and kwargs
        This method does not return anything

        Args:
            signal_name (str): The name of the signal to trigger
        """

        callbacks = self.listeners.get(signal_name)
        if not callbacks:
            return

        for callback in callbacks:
            callback(*args, **kwargs)

    def query(self, signal_name, *args, default=None, aggregate_func=None, **kwargs):
        """
        Queries a specific signal with specified args and kwargs
        This method will return the responses from its connected responders.
        If an aggregate_func is specified, it is called with the responses
        and its result is returned instead.

        Args:
            default (any): The value to use when no responses are given
                           It will be passed to aggregate_func if it is also given.
            aggregate_func (callable): The function to process the results before returning.

        Returns:
            list: An iterable of the responses
                  OR the aggregated result when aggregate_func is specified.

        """
        callbacks = self.responders.get(signal_name)

            default = [] if default is None else default
            if aggregate_func:
                return aggregate_func(default)
            return default

        responses = []
            response = callback(*args, **kwargs)
            if response is not None:
                responses.append(response)

        if aggregate_func and responses:
            return aggregate_func(responses)

        return responses

    def add_object_listeners_and_responders(self, obj):
        """
        This connects the methods marked as listener or responder from an object.

        Args:
            obj (object): The instance of an object to connect to this handler.
        """
        type_host = type(obj)
        for att_name, att_obj in type_host.__dict__.items():
            listener_signal_name = getattr(att_obj, "_listener_signal_name", None)
            if listener_signal_name:
                callback = getattr(obj, att_name)
                self.add_listener(signal_name=listener_signal_name, callback=callback)

            responder_signal_name = getattr(att_obj, "_responder_signal_name", None)
            if responder_signal_name:
                self.add_responder(signal_name=responder_signal_name, callback=callback)

    def remove_object_listeners_and_responders(self, obj):
        """
        This disconnects the methods marked as listener or responder from an object.

        Args:
            obj (object): The instance of an object to disconnect from this handler.
        """
                self.remove_listener(signal_name=listener_signal_name, callback=callback)

                self.remove_responder(signal_name=responder_signal_name, callback=callback)

from evennia.contrib.base_systems.components import (
    Component,
    DBField,
    TagField,
    signals,
)
)
from evennia.contrib.base_systems.components.signals import as_listener
from evennia.objects.objects import DefaultCharacter
from evennia.utils import create
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTest

class ComponentTestA(Component):
    name = "test_a"
    my_int = DBField(default=1)
    my_list = DBField(default=[])

class ComponentTestB(Component):
    name = "test_b"
    default_tag = TagField(default="initial_value")
    single_tag = TagField(enforce_single=True)
    multiple_tags = TagField()
    default_single_tag = TagField(default="initial_value", enforce_single=True)

class RuntimeComponentTestC(Component):
    name = "test_c"
    my_int = DBField(default=6)
    my_dict = DBField(default={})
    added_tag = TagField(default="added_value")

class CharacterWithComponents(ComponentHolderMixin, DefaultCharacter):
    test_a = ComponentProperty("test_a")
    test_b = ComponentProperty("test_b", my_int=3, my_list=[1, 2, 3])

class InheritedTCWithComponents(CharacterWithComponents):
    test_c = ComponentProperty("test_c")

class TestComponents(EvenniaTest):
    character_typeclass = CharacterWithComponents

    def test_character_has_class_components(self):
        assert self.char1.test_a
        assert self.char1.test_b

    def test_inherited_typeclass_does_not_include_child_class_components(self):
        char_with_c = create.create_object(
            InheritedTCWithComponents, key="char_with_c", location=self.room1, home=self.room1
        )
        assert not self.char1.cmp.get("test_c")
        assert char_with_c.test_c

    def test_character_instances_components_properly(self):
        assert isinstance(self.char1.test_a, ComponentTestA)
        assert isinstance(self.char1.test_b, ComponentTestB)

    def test_character_assigns_default_value(self):
        assert self.char1.test_a.my_int == 1
        assert self.char1.test_a.my_list == []

    def test_character_assigns_default_provided_values(self):
        assert self.char1.test_b.my_int == 3
        assert self.char1.test_b.my_list == [1, 2, 3]

    def test_character_can_register_runtime_component(self):
        rct = RuntimeComponentTestC.create(self.char1)
        self.char1.components.add(rct)
        test_c = self.char1.components.get("test_c")

        assert test_c
        assert test_c.my_int == 6
        assert test_c.my_dict == {}

    def test_handler_can_add_default_component(self):
        self.char1.components.add_default("test_c")

    def test_handler_has_returns_true_for_any_components(self):
        handler = self.char1.components
        handler.add(rct)

        assert handler.has("test_a")
        assert handler.has("test_b")
        assert handler.has("test_c")

    def test_can_remove_component(self):
        handler.remove(rct)

        assert not handler.has("test_c")

    def test_can_remove_component_by_name(self):
        handler.remove_by_name("test_c")

    def test_cannot_replace_component(self):
        with self.assertRaises(Exception):
            self.char1.test_a = None

    def test_can_get_component(self):

        assert handler.get("test_c") is rct

    def test_can_access_component_regular_get(self):
        assert self.char1.cmp.test_a is self.char1.components.get("test_a")

    def test_returns_none_with_regular_get_when_no_attribute(self):
        assert self.char1.cmp.does_not_exist is None

    def test_host_has_class_component_tags(self):
        assert self.char1.tags.has(key="test_a", category="components")
        assert self.char1.tags.has(key="test_b", category="components")
        assert self.char1.tags.has(key="initial_value", category="test_b::default_tag")
        assert self.char1.test_b.default_tag == "initial_value"
        assert not self.char1.tags.has(key="test_c", category="components")
        assert not self.char1.tags.has(category="test_b::single_tag")
        assert not self.char1.tags.has(category="test_b::multiple_tags")

    def test_host_has_added_component_tags(self):

        assert self.char1.tags.has(key="test_c", category="components")
        assert self.char1.tags.has(key="added_value", category="test_c::added_tag")
        assert test_c.added_tag == "added_value"

    def test_host_has_added_default_component_tags(self):

    def test_host_remove_component_tags(self):

        assert not self.char1.tags.has(key="added_value", category="test_c::added_tag")

    def test_host_remove_by_name_component_tags(self):

    def test_component_tags_only_hold_one_value_when_enforce_single(self):
        test_b = self.char1.components.get("test_b")
        test_b.single_tag = "first_value"
        test_b.single_tag = "second value"

        assert self.char1.tags.has(key="second value", category="test_b::single_tag")
        assert test_b.single_tag == "second value"
        assert not self.char1.tags.has(key="first_value", category="test_b::single_tag")

    def test_component_tags_default_value_is_overridden_when_enforce_single(self):
        test_b.default_single_tag = "second value"

        assert self.char1.tags.has(key="second value", category="test_b::default_single_tag")
        assert test_b.default_single_tag == "second value"
        assert not self.char1.tags.has(key="first_value", category="test_b::default_single_tag")

    def test_component_tags_support_multiple_values_by_default(self):
        test_b.multiple_tags = "first value"
        test_b.multiple_tags = "second value"
        test_b.multiple_tags = "third value"

        assert all(
            val in test_b.multiple_tags for val in ("first value", "second value", "third value")
        )
        assert self.char1.tags.has(key="first value", category="test_b::multiple_tags")
        assert self.char1.tags.has(key="second value", category="test_b::multiple_tags")
        assert self.char1.tags.has(key="third value", category="test_b::multiple_tags")

class CharWithSignal(ComponentHolderMixin, DefaultCharacter):
    @signals.as_listener
    def my_signal(self):
        setattr(self, "my_signal_is_called", True)

    def my_other_signal(self):
        setattr(self, "my_other_signal_is_called", True)

    @signals.as_responder
    def my_response(self):
        return 1

    def my_other_response(self):
        return 2

class ComponentWithSignal(Component):
    name = "test_signal_a"

        return 1

        return 2

    def my_component_response(self):
        return 3

class TestComponentSignals(BaseEvenniaTest):
        self.char1 = create.create_object(
            CharWithSignal,
            key="Char",
        )

    def test_host_can_register_as_listener(self):
        self.char1.signals.trigger("my_signal")

        assert self.char1.my_signal_is_called
        assert not getattr(self.char1, "my_other_signal_is_called", None)

    def test_host_can_register_as_responder(self):
        responses = self.char1.signals.query("my_response")

        assert 1 in responses
        assert 2 not in responses

    def test_component_can_register_as_listener(self):
        char = self.char1
        char.components.add(ComponentWithSignal.create(char))
        char.signals.trigger("my_signal")

        component = char.cmp.test_signal_a
        assert component.my_signal_is_called
        assert not getattr(component, "my_other_signal_is_called", None)

    def test_component_can_register_as_responder(self):
        responses = char.signals.query("my_response")

    def test_signals_can_add_listener(self):
        result = []

        def my_fake_listener():
            result.append(True)

        self.char1.signals.add_listener("my_fake_signal", my_fake_listener)
        self.char1.signals.trigger("my_fake_signal")

        assert result

    def test_signals_can_add_responder(self):
        def my_fake_responder():
            return 1

        self.char1.signals.add_responder("my_fake_response", my_fake_responder)
        responses = self.char1.signals.query("my_fake_response")

    def test_signals_can_remove_listener(self):
        result = []

        self.char1.signals.remove_listener("my_fake_signal", my_fake_listener)

        assert not result

    def test_signals_can_remove_responder(self):
            return 1

        self.char1.signals.remove_responder("my_fake_response", my_fake_responder)

        assert not responses

    def test_signals_can_trigger_with_args(self):
        result = []

        def my_fake_listener(arg1, kwarg1):
            result.append((arg1, kwarg1))

        self.char1.signals.trigger("my_fake_signal", 1, kwarg1=2)

        assert (1, 2) in result

    def test_signals_can_query_with_args(self):
        def my_fake_responder(arg1, kwarg1):
            return (arg1, kwarg1)

        responses = self.char1.signals.query("my_fake_response", 1, kwarg1=2)

        assert (1, 2) in responses

    def test_signals_trigger_does_not_fail_without_listener(self):
        self.char1.signals.trigger("some_unknown_signal")

    def test_signals_query_does_not_fail_wihout_responders(self):
        self.char1.signals.query("no_responders_allowed")

    def test_signals_query_with_aggregate(self):

    def test_signals_can_add_object_listeners_and_responders(self):
        result = []

        class FakeObj:
            @as_listener
            def my_signal(self):
                result.append(True)

        self.char1.signals.add_object_listeners_and_responders(FakeObj())

    def test_signals_can_remove_object_listeners_and_responders(self):
        result = []

        obj = FakeObj()
        self.char1.signals.add_object_listeners_and_responders(obj)
        self.char1.signals.remove_object_listeners_and_responders(obj)

    def test_component_handler_signals_connected_when_adding_default_component(self):
        char.components.add_default("test_signal_a")
        responses = char.signals.query("my_component_response")

        assert 3 in responses

    def test_component_handler_signals_disconnected_when_removing_component(self):
        comp = ComponentWithSignal.create(char)
        char.components.add(comp)
        char.components.remove(comp)

    def test_component_handler_signals_disconnected_when_removing_component_by_name(self):
        char.components.remove_by_name("test_signal_a")

"""
Custom gametime

Contrib - Griatch 2017, vlgeoff 2017

This implements the evennia.utils.gametime module but supporting
a custom calendar for your game world. It allows for scheduling
events to happen at given in-game times, taking this custom
calendar into account.

Usage:

Use as the normal gametime module, that is by importing and using the
helper functions in this module in your own code. The calendar can be
customized by adding the `TIME_UNITS` dictionary to your settings
file. This maps unit names to their length, expressed in the smallest
unit. Here's the default as an example:

    TIME_UNITS = {
        "sec": 1,
        "min": 60,
        "hr": 60 * 60,
        "hour": 60 * 60,
        "day": 60 * 60 * 24,
        "week": 60 * 60 * 24 * 7,
        "month": 60 * 60 * 24 * 7 * 4,
        "yr": 60 * 60 * 24 * 7 * 4 * 12,
        "year": 60 * 60 * 24 * 7 * 4 * 12, }

When using a custom calendar, these time unit names are used as kwargs to
the converter functions in this module.

"""

# change these to fit your game world

from evennia import DefaultScript
from evennia.utils import gametime
from evennia.utils.create import create_script

# The game time speedup  / slowdown relative real time
TIMEFACTOR = settings.TIME_FACTOR

# These are the unit names understood by the scheduler.
# Each unit must be consistent and expressed in seconds.
UNITS = getattr(
    settings,
    "TIME_UNITS",
    {
        # default custom calendar
        "sec": 1,
        "min": 60,
        "year": 60 * 60 * 24 * 7 * 4 * 12,
    },
)

def time_to_tuple(seconds, *divisors):
    """
    Helper function. Creates a tuple of even dividends given a range
    of divisors.

    Args:
        seconds (int): Number of seconds to format
        *divisors (int): a sequence of numbers of integer dividends. The
            number of seconds will be integer-divided by the first number in
            this sequence, the remainder will be divided with the second and
            so on.
    Returns:
        time (tuple): This tuple has length len(*args)+1, with the
            last element being the last remaining seconds not evenly
            divided by the supplied dividends.

    """
    results = []
    seconds = int(seconds)
    for divisor in divisors:
        results.append(seconds // divisor)
        seconds %= divisor
    results.append(seconds)
    return tuple(results)

def gametime_to_realtime(format=False, **kwargs):
    """
    This method helps to figure out the real-world time it will take until an
    in-game time has passed. E.g. if an event should take place a month later
    in-game, you will be able to find the number of real-world seconds this
    corresponds to (hint: Interval events deal with real life seconds).

        format (bool): Formatting the output.
        days, month etc (int): These are the names of time units that must
            match the `settings.TIME_UNITS` dict keys.

    Returns:
        time (float or tuple): The realtime difference or the same
            time split up into time units.

    Example:
         gametime_to_realtime(days=2) -> number of seconds in real life from
                        now after which 2 in-game days will have passed.

    """
    # Dynamically creates the list of units based on kwarg names and UNITs list
    rtime = 0
    for name, value in kwargs.items():
        # Allow plural names (like mins instead of min)
        if name not in UNITS and name.endswith("s"):
            name = name[:-1]

        if name not in UNITS:
            raise ValueError("the unit {} isn't defined as a valid " "game time unit".format(name))
        rtime += value * UNITS[name]
    rtime /= TIMEFACTOR
    if format:
        return time_to_tuple(rtime, 31536000, 2628000, 604800, 86400, 3600, 60)
    return rtime

def realtime_to_gametime(secs=0, mins=0, hrs=0, days=1, weeks=1, months=1, yrs=0, format=False):
    """
    This method calculates how much in-game time a real-world time
    interval would correspond to. This is usually a lot less
    interesting than the other way around.

        times (int): The various components of the time.

    Returns:
        time (float or tuple): The gametime difference or the same

    Note:
        days/weeks/months start from 1 (there is no day/week/month 0). This makes it
            consistent with the real world datetime.

    Raises:
        ValueError: If trying to add a days/weeks/months of <=0.

    Example:
      realtime_to_gametime(days=2) -> number of game-world seconds

    """
    if days <= 0 or weeks <= 0 or months <= 0:
        raise ValueError(
            "realtime_to_gametime: days/weeks/months cannot be set <= 0, " "they start from 1."
        )

    # days/weeks/months start from 1, we need to adjust them to work mathematically.
    days, weeks, months = days - 1, weeks - 1, months - 1

    gtime = TIMEFACTOR * (
        secs
        + mins * 60
        + hrs * 3600
        + days * 86400
        + weeks * 604800
        + months * 2628000
        + yrs * 31536000
    )
    if format:
        units = sorted(set(UNITS.values()), reverse=True)
        # Remove seconds from the tuple
        del units[-1]

        return time_to_tuple(gtime, *units)
    return gtime

def custom_gametime(absolute=False):
    """
    Return the custom game time as a tuple of units, as defined in settings.

    Args:
        absolute (bool, optional): return the relative or absolute time.

    Returns:
        The tuple describing the game time.  The length of the tuple
        is related to the number of unique units defined in the
        settings.  By default, the tuple would be (year, month,
        week, day, hour, minute, second).

    """
    current = gametime.gametime(absolute=absolute)
    units = sorted(set(UNITS.values()), reverse=True)
    del units[-1]
    return time_to_tuple(current, *units)

def real_seconds_until(**kwargs):
    """
    Return the real seconds until game time.

    If the game time is 5:00, TIME_FACTOR is set to 2 and you ask
    the number of seconds until it's 5:10, then this function should
    return 300 (5 minutes).

    Args:
        times (str: int): the time units.

    Example:
        real_seconds_until(hour=5, min=10, sec=0)

    Returns:
        The number of real seconds before the given game time is up.

    Notes:
        day/week/month start from 1, not from 0 (there is no month 0 for example)

    """
    current = gametime.gametime(absolute=True)
    # Remove seconds from the tuple
    divisors = list(time_to_tuple(current, *units))

    # For each keyword, add in the unit's
    units.append(1)
    higher_unit = None
    for unit, value in kwargs.items():
        if unit in ("day", "week", "month"):
            # these start from 1 so we must adjust
            value -= 1

        # Get the unit's index
        if unit not in UNITS:
            raise ValueError(f"Unknown unit '{unit}'. Allowed: {', '.join(UNITS)}")

        seconds = UNITS[unit]
        index = units.index(seconds)
        divisors[index] = value
        if higher_unit is None or higher_unit > index:
            higher_unit = index

    # Check the projected time
    # Note that it can be already passed (the given time may be in the past)
    projected = 0
    for i, value in enumerate(divisors):
        seconds = units[i]
        projected += value * seconds

    if projected <= current:
        # The time is in the past, increase the higher unit
        if higher_unit:
            divisors[higher_unit - 1] += 1
        else:
            divisors[0] += 1

    # Get the projected time again

    return (projected - current) / TIMEFACTOR

def schedule(callback, repeat=False, **kwargs):
    """
    Call the callback when the game time is up.

    Args:
        callback (function): The callback function that will be called. This
            must be a top-level function since the script will be persistent.
        repeat (bool, optional): Should the callback be called regularly?
        day, month, etc (str: int): The time units to call the callback; should
            match the keys of TIME_UNITS.

    Returns:
        script (Script): The created script.

    Examples:
        schedule(func, min=5, sec=0)          # Will call next hour at :05.
        schedule(func, hour=2, min=30, sec=0) # Will call the next day at 02:30.
    Notes:
        This function will setup a script that will be called when the
        time corresponds to the game time.  If the game is stopped for
        more than a few seconds, the callback may be called with a
        slight delay. If `repeat` is set to True, the callback will be
        called again next time the game time matches the given time.
        The time is given in units as keyword arguments.

    """
    seconds = real_seconds_until(**kwargs)
    script = create_script(
        "evennia.contrib.base_systems.custom_gametime.GametimeScript",
        key="GametimeScript",
        desc="A timegame-sensitive script",
        interval=seconds,
        start_delay=True,
        repeats=-1 if repeat else 1,
    )
    script.db.callback = callback
    script.db.gametime = kwargs
    return script

# Scripts dealing in gametime (use `schedule`  to create it)

class GametimeScript(DefaultScript):

    """Gametime-sensitive script."""

        """The script is created."""
        self.key = "unknown scr"
        self.interval = 100
        self.start_delay = True

        """Call the callback and reset interval."""

        from evennia.utils.utils import calledby

        callback = self.db.callback
        if callback:
            callback()

        seconds = real_seconds_until(**self.db.gametime)
        self.start(interval=seconds, force_restart=True)

"""
Testing custom game time

"""

# Testing custom_gametime
from mock import Mock, patch

from .. import custom_gametime

def _testcallback():
    pass

@patch("evennia.utils.gametime.gametime", new=Mock(return_value=2975000898.46))
class TestCustomGameTime(BaseEvenniaTest):
        if hasattr(self, "timescript"):
            self.timescript.stop()

    def test_time_to_tuple(self):
        self.assertEqual(custom_gametime.time_to_tuple(10000, 34, 2, 4, 6, 1), (294, 2, 0, 0, 0, 0))
        self.assertEqual(custom_gametime.time_to_tuple(10000, 3, 3, 4), (3333, 0, 0, 1))
        self.assertEqual(custom_gametime.time_to_tuple(100000, 239, 24, 3), (418, 4, 0, 2))

    def test_gametime_to_realtime(self):
        self.assertEqual(custom_gametime.gametime_to_realtime(days=2, mins=4), 86520.0)
            custom_gametime.gametime_to_realtime(format=True, days=2), (0, 0, 0, 1, 0, 0, 0)
        )

    def test_realtime_to_gametime(self):
        self.assertEqual(custom_gametime.realtime_to_gametime(days=3, mins=34), 349680.0)
            custom_gametime.realtime_to_gametime(days=3, mins=34, format=True),
            (0, 0, 0, 4, 1, 8, 0),
        )
            custom_gametime.realtime_to_gametime(format=True, days=3, mins=4), (0, 0, 0, 4, 0, 8, 0)
        )

    def test_custom_gametime(self):
        self.assertEqual(custom_gametime.custom_gametime(), (102, 5, 2, 6, 21, 8, 18))
        self.assertEqual(custom_gametime.custom_gametime(absolute=True), (102, 5, 2, 6, 21, 8, 18))

    def test_real_seconds_until(self):
            custom_gametime.real_seconds_until(year=2300, month=12, day=7), 31911667199.77
        )

    def test_schedule(self):
        self.timescript = custom_gametime.schedule(_testcallback, repeat=True, min=5, sec=0)
        self.assertEqual(self.timescript.interval, 1700.7699999809265)

"""
Email-based login system

Evennia contrib - Griatch 2012

This is a variant of the login system that requires an email-address
instead of a username to login.

This used to be the default Evennia login before replacing it with a
more standard username + password system (having to supply an email
for some reason caused a lot of confusion when people wanted to expand
on it. The email is not strictly needed internally, nor is any
confirmation email sent out anyway).

Installation is simple:

To your settings file, add/edit settings as follows:

```python
CMDSET_UNLOGGEDIN = "contrib.base_systems.email_login.email_login.UnloggedinCmdSet"
CONNECTION_SCREEN_MODULE = "contrib.base_systems.email_login.connection_screens"

```

That's it. Reload the server and try to log in to see it.

The initial login "graphic" will still not mention email addresses
after this change. The login splashscreen is taken from strings in
the module given by settings.CONNECTION_SCREEN_MODULE.

"""

from evennia.utils import ansi, class_from_module, utils

__all__ = (
)

CONNECTION_SCREEN = ""
try:
    CONNECTION_SCREEN = ansi.parse_ansi(utils.random_string_from_module(CONNECTION_SCREEN_MODULE))
except Exception:
    # malformed connection screen or no screen given
    pass
if not CONNECTION_SCREEN:
    CONNECTION_SCREEN = (
        "\nEvennia: Error in CONNECTION_SCREEN MODULE"
        " (randomly picked connection screen variable is not a string). \nEnter 'help' for aid."
    )

class CmdUnconnectedConnect(MuxCommand):
    """
    Connect to the game.

        connect <email> <password>

    """

        """
        have a unique position in that their `func()` receives
        a session object instead of a `source_object` like all
        """

        arglist = self.arglist

        if not arglist or len(arglist) < 2:
            session.msg("\n\r Usage (without <>): connect <email> <password>")
            return
        email = arglist[0]
        password = arglist[1]

        # Match an email address to an account.
        account = AccountDB.objects.get_account_from_email(email)
        # No accountname match
            string = "The email '%s' does not match any accounts." % email
            string += "\n\r\n\rIf you are new you should first create a new account "
            string += "using the 'create' command."
            return
        # We have at least one result, so we can check the password.
        if not account[0].check_password(password):
            session.msg("Incorrect password.")
            return

            any(tup[0] == account.name for tup in bans)
            or any(tup[2].match(session.address[0]) for tup in bans if tup[2])
        ):
            string = "|rYou have been banned and cannot continue from here."
            string += "\nIf you feel this ban is in error, please email an admin.|x"
            session.execute_cmd("quit")
            return

        # actually do the login. This will call all hooks.
        session.sessionhandler.login(session, account)

class CmdUnconnectedCreate(MuxCommand):
    """
    Create a new account.

        create \"accountname\" <email> <password>

    """

        """
        The parser must handle the multiple-word account
        name enclosed in quotes:
            connect "Long name with many words" my@myserv.com mypassw
        """

        self.accountinfo = []
        if len(self.arglist) < 3:
            return
        if len(self.arglist) > 3:
            # this means we have a multi_word accountname. pop from the back.
            password = self.arglist.pop()
            email = self.arglist.pop()
            # what remains is the username.
            username = " ".join(self.arglist)
        else:
            username, email, password = self.arglist

        username = username.replace('"', "")  # remove "
        username = username.replace("'", "")
        self.accountinfo = (username, email, password)

        address = self.session.address

        try:
            username, email, password = self.accountinfo
        except ValueError:
            string = '\n\r Usage (without <>): create "<accountname>" <email> <password>'
            return
        if not email or not password:
            session.msg("\n\r You have to supply an e-mail address followed by a password.")
            return
        if not utils.validate_email_address(email):
            # check so the email at least looks ok.
            session.msg("'%s' is not a valid e-mail address." % email)
            return

            )

            f"You want to create an account '{username}' with email '{email}' and password "
            f"'{password}'.\nIs this what you intended? [Y]/N?"
        )
            return

            username=username, email=email, password=password, ip=address, session=session
        )
                string += (
                )
            else:
        else:

class CmdUnconnectedQuit(MuxCommand):
    """
    We maintain a different version of the `quit` command
    """

class CmdUnconnectedLook(MuxCommand):
    """
    This is an unconnected version of the `look` command for simplicity.

    """

        self.caller.msg(CONNECTION_SCREEN)

class CmdUnconnectedHelp(MuxCommand):
    """
    """

  |wcreate, connect, look, help, quit|n

To login to the system, you need to do one of the following:

|w1)|n If you have no previous account, you need to use the 'create'
   command like this:

     |wcreate "Anna the Barbarian" anna@myemail.com c67jHL8p|n

   It's always a good idea (not only here, but everywhere on the net)
   to not use a regular word for your password. Make it longer than
   3 characters (ideally 6 or more) and mix numbers and capitalization
   into it.

|w2)|n If you have an account already, either because you just created
   one in |w1)|n above or you are returning, use the 'connect' command:

     |wconnect anna@myemail.com c67jHL8p|n

   This should log you in. Run |whelp|n again once you're logged in
   to get more aid. Hope you enjoy your stay!

"""

# command set for the mux-like login

    """
    """

    key = "Unloggedin"

        """Populate the cmdset"""
        self.add(CmdUnconnectedConnect())
        self.add(CmdUnconnectedCreate())
        self.add(CmdUnconnectedQuit())
        self.add(CmdUnconnectedLook())
        self.add(CmdUnconnectedHelp())

"""
Test email login.

"""

from . import email_login

class TestEmailLogin(BaseEvenniaCommandTest):
    def test_connect(self):
            email_login.CmdUnconnectedConnect(),
            "mytest@test.com test",
            "The email 'mytest@test.com' does not match any accounts.",
        )
            email_login.CmdUnconnectedCreate(),
            '"mytest" mytest@test.com test11111',
            "A new account 'mytest' was created. Welcome!",
        )
            "mytest@test.com test11111",
            "",
            caller=self.account.sessions.get()[0],
        )

        self.call(email_login.CmdUnconnectedQuit(), "", "", caller=self.account.sessions.get()[0])

    def test_unconnectedlook(self):
        self.call(email_login.CmdUnconnectedLook(), "", "==========")

    def test_unconnectedhelp(self):
        self.call(email_login.CmdUnconnectedHelp(), "", "You are not yet logged into the game.")

"""Tests for text2bbcode """

import mock
from django.test import TestCase

from evennia.contrib.base_systems.godotwebsocket import text2bbcode
from evennia.utils import ansi

class TestText2Bbcode(TestCase):
    def test_format_styles(self):
        parser = text2bbcode.BBCODE_PARSER
        self.assertEqual("foo", parser.format_styles("foo"))
            "[color=#800000]red[/color]foo",
            parser.format_styles(
                ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"
            ),
        )
            "[bgcolor=#800000]red[/bgcolor]foo",
            parser.format_styles(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
        )
            "[bgcolor=#800000][color=#008000]red[/color][/bgcolor]foo",
                ansi.ANSI_BACK_RED
                + ansi.ANSI_UNHILITE
                + ansi.ANSI_GREEN
                + "red"
                + ansi.ANSI_NORMAL
                + "foo"
            ),
        )
            "a [u]red[/u]foo",
            parser.format_styles("a " + ansi.ANSI_UNDERLINE + "red" + ansi.ANSI_NORMAL + "foo"),
        )
            "a [blink]red[/blink]foo",
            parser.format_styles("a " + ansi.ANSI_BLINK + "red" + ansi.ANSI_NORMAL + "foo"),
        )
            "a [bgcolor=#c0c0c0][color=#000000]red[/color][/bgcolor]foo",
            parser.format_styles("a " + ansi.ANSI_INVERSE + "red" + ansi.ANSI_NORMAL + "foo"),
        )

    def test_convert_urls(self):
        self.assertEqual("foo", parser.convert_urls("foo"))
            "a [url=http://redfoo]http://redfoo[/url] runs",
            parser.convert_urls("a http://redfoo runs"),
        )

    def test_sub_mxp_links(self):
        mocked_match = mock.Mock()
        mocked_match.groups.return_value = ["cmd", "text"]

        self.assertEqual("[mxp=send cmd=cmd]text[/mxp]", parser.sub_mxp_links(mocked_match))

    def test_sub_text(self):

        mocked_match.groupdict.return_value = {"lineend": "foo"}
        self.assertEqual("\n", parser.sub_text(mocked_match))

    def test_parse_bbcode(self):
        self.assertEqual("foo", text2bbcode.parse_to_bbcode("foo"))
        self.maxDiff = None
            text2bbcode.parse_to_bbcode("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"),
            "[blink][bgcolor=#008080]Hello[/bgcolor][/blink]"
            "[u][color=#ff0000]W[/color][/u]"
            "[u][color=#00ff00]o[/color][/u]"
            "[u][color=#ffff00]r[/color][/u]"
            "[u][color=#0000ff]l[/color][/u]"
            "[u][color=#ff00ff]d[/color][/u]"
            "[u][color=#00ffff]![/color][/u]"
            "[u][bgcolor=#008000][color=#00ffff]![/color][/bgcolor][/u]",
        )

"""
Godot Websocket - ChrisLR 2022

This file contains the necessary code and data to convert text with color tags to bbcode (For godot)
"""
from evennia.utils.ansi import *
from evennia.utils.text2html import TextToHTMLparser

# All xterm256 RGB equivalents

XTERM256_FG = "\033[38;5;{}m"
XTERM256_BG = "\033[48;5;{}m"

COLOR_INDICE_TO_HEX = {
    "color-000": "#000000",
    "color-001": "#800000",
    "color-002": "#008000",
    "color-003": "#808000",
    "color-004": "#000080",
    "color-005": "#800080",
    "color-006": "#008080",
    "color-007": "#c0c0c0",
    "color-008": "#808080",
    "color-009": "#ff0000",
    "color-010": "#00ff00",
    "color-011": "#ffff00",
    "color-012": "#0000ff",
    "color-013": "#ff00ff",
    "color-014": "#00ffff",
    "color-015": "#ffffff",
    "color-016": "#000000",
    "color-017": "#00005f",
    "color-018": "#000087",
    "color-019": "#0000af",
    "color-020": "#0000df",
    "color-021": "#0000ff",
    "color-022": "#005f00",
    "color-023": "#005f5f",
    "color-024": "#005f87",
    "color-025": "#005faf",
    "color-026": "#005fdf",
    "color-027": "#005fff",
    "color-028": "#008700",
    "color-029": "#00875f",
    "color-030": "#008787",
    "color-031": "#0087af",
    "color-032": "#0087df",
    "color-033": "#0087ff",
    "color-034": "#00af00",
    "color-035": "#00af5f",
    "color-036": "#00af87",
    "color-037": "#00afaf",
    "color-038": "#00afdf",
    "color-039": "#00afff",
    "color-040": "#00df00",
    "color-041": "#00df5f",
    "color-042": "#00df87",
    "color-043": "#00dfaf",
    "color-044": "#00dfdf",
    "color-045": "#00dfff",
    "color-046": "#00ff00",
    "color-047": "#00ff5f",
    "color-048": "#00ff87",
    "color-049": "#00ffaf",
    "color-050": "#00ffdf",
    "color-051": "#00ffff",
    "color-052": "#5f0000",
    "color-053": "#5f005f",
    "color-054": "#5f0087",
    "color-055": "#5f00af",
    "color-056": "#5f00df",
    "color-057": "#5f00ff",
    "color-058": "#5f5f00",
    "color-059": "#5f5f5f",
    "color-060": "#5f5f87",
    "color-061": "#5f5faf",
    "color-062": "#5f5fdf",
    "color-063": "#5f5fff",
    "color-064": "#5f8700",
    "color-065": "#5f875f",
    "color-066": "#5f8787",
    "color-067": "#5f87af",
    "color-068": "#5f87df",
    "color-069": "#5f87ff",
    "color-070": "#5faf00",
    "color-071": "#5faf5f",
    "color-072": "#5faf87",
    "color-073": "#5fafaf",
    "color-074": "#5fafdf",
    "color-075": "#5fafff",
    "color-076": "#5fdf00",
    "color-077": "#5fdf5f",
    "color-078": "#5fdf87",
    "color-079": "#5fdfaf",
    "color-080": "#5fdfdf",
    "color-081": "#5fdfff",
    "color-082": "#5fff00",
    "color-083": "#5fff5f",
    "color-084": "#5fff87",
    "color-085": "#5fffaf",
    "color-086": "#5fffdf",
    "color-087": "#5fffff",
    "color-088": "#870000",
    "color-089": "#87005f",
    "color-090": "#870087",
    "color-091": "#8700af",
    "color-092": "#8700df",
    "color-093": "#8700ff",
    "color-094": "#875f00",
    "color-095": "#875f5f",
    "color-096": "#875f87",
    "color-097": "#875faf",
    "color-098": "#875fdf",
    "color-099": "#875fff",
    "color-100": "#878700",
    "color-101": "#87875f",
    "color-102": "#878787",
    "color-103": "#8787af",
    "color-104": "#8787df",
    "color-105": "#8787ff",
    "color-106": "#87af00",
    "color-107": "#87af5f",
    "color-108": "#87af87",
    "color-109": "#87afaf",
    "color-110": "#87afdf",
    "color-111": "#87afff",
    "color-112": "#87df00",
    "color-113": "#87df5f",
    "color-114": "#87df87",
    "color-115": "#87dfaf",
    "color-116": "#87dfdf",
    "color-117": "#87dfff",
    "color-118": "#87ff00",
    "color-119": "#87ff5f",
    "color-120": "#87ff87",
    "color-121": "#87ffaf",
    "color-122": "#87ffdf",
    "color-123": "#87ffff",
    "color-124": "#af0000",
    "color-125": "#af005f",
    "color-126": "#af0087",
    "color-127": "#af00af",
    "color-128": "#af00df",
    "color-129": "#af00ff",
    "color-130": "#af5f00",
    "color-131": "#af5f5f",
    "color-132": "#af5f87",
    "color-133": "#af5faf",
    "color-134": "#af5fdf",
    "color-135": "#af5fff",
    "color-136": "#af8700",
    "color-137": "#af875f",
    "color-138": "#af8787",
    "color-139": "#af87af",
    "color-140": "#af87df",
    "color-141": "#af87ff",
    "color-142": "#afaf00",
    "color-143": "#afaf5f",
    "color-144": "#afaf87",
    "color-145": "#afafaf",
    "color-146": "#afafdf",
    "color-147": "#afafff",
    "color-148": "#afdf00",
    "color-149": "#afdf5f",
    "color-150": "#afdf87",
    "color-151": "#afdfaf",
    "color-152": "#afdfdf",
    "color-153": "#afdfff",
    "color-154": "#afff00",
    "color-155": "#afff5f",
    "color-156": "#afff87",
    "color-157": "#afffaf",
    "color-158": "#afffdf",
    "color-159": "#afffff",
    "color-160": "#df0000",
    "color-161": "#df005f",
    "color-162": "#df0087",
    "color-163": "#df00af",
    "color-164": "#df00df",
    "color-165": "#df00ff",
    "color-166": "#df5f00",
    "color-167": "#df5f5f",
    "color-168": "#df5f87",
    "color-169": "#df5faf",
    "color-170": "#df5fdf",
    "color-171": "#df5fff",
    "color-172": "#df8700",
    "color-173": "#df875f",
    "color-174": "#df8787",
    "color-175": "#df87af",
    "color-176": "#df87df",
    "color-177": "#df87ff",
    "color-178": "#dfaf00",
    "color-179": "#dfaf5f",
    "color-180": "#dfaf87",
    "color-181": "#dfafaf",
    "color-182": "#dfafdf",
    "color-183": "#dfafff",
    "color-184": "#dfdf00",
    "color-185": "#dfdf5f",
    "color-186": "#dfdf87",
    "color-187": "#dfdfaf",
    "color-188": "#dfdfdf",
    "color-189": "#dfdfff",
    "color-190": "#dfff00",
    "color-191": "#dfff5f",
    "color-192": "#dfff87",
    "color-193": "#dfffaf",
    "color-194": "#dfffdf",
    "color-195": "#dfffff",
    "color-196": "#ff0000",
    "color-197": "#ff005f",
    "color-198": "#ff0087",
    "color-199": "#ff00af",
    "color-200": "#ff00df",
    "color-201": "#ff00ff",
    "color-202": "#ff5f00",
    "color-203": "#ff5f5f",
    "color-204": "#ff5f87",
    "color-205": "#ff5faf",
    "color-206": "#ff5fdf",
    "color-207": "#ff5fff",
    "color-208": "#ff8700",
    "color-209": "#ff875f",
    "color-210": "#ff8787",
    "color-211": "#ff87af",
    "color-212": "#ff87df",
    "color-213": "#ff87ff",
    "color-214": "#ffaf00",
    "color-215": "#ffaf5f",
    "color-216": "#ffaf87",
    "color-217": "#ffafaf",
    "color-218": "#ffafdf",
    "color-219": "#ffafff",
    "color-220": "#ffdf00",
    "color-221": "#ffdf5f",
    "color-222": "#ffdf87",
    "color-223": "#ffdfaf",
    "color-224": "#ffdfdf",
    "color-225": "#ffdfff",
    "color-226": "#ffff00",
    "color-227": "#ffff5f",
    "color-228": "#ffff87",
    "color-229": "#ffffaf",
    "color-230": "#ffffdf",
    "color-231": "#ffffff",
    "color-232": "#080808",
    "color-233": "#121212",
    "color-234": "#1c1c1c",
    "color-235": "#262626",
    "color-236": "#303030",
    "color-237": "#3a3a3a",
    "color-238": "#444444",
    "color-239": "#4e4e4e",
    "color-240": "#585858",
    "color-241": "#606060",
    "color-242": "#666666",
    "color-243": "#767676",
    "color-244": "#808080",
    "color-245": "#8a8a8a",
    "color-246": "#949494",
    "color-247": "#9e9e9e",
    "color-248": "#a8a8a8",
    "color-249": "#b2b2b2",
    "color-250": "#bcbcbc",
    "color-251": "#c6c6c6",
    "color-252": "#d0d0d0",
    "color-253": "#dadada",
    "color-254": "#e4e4e4",
    "color-255": "#eeeeee",
    "bgcolor-000": "#000000",
    "bgcolor-001": "#800000",
    "bgcolor-002": "#008000",
    "bgcolor-003": "#808000",
    "bgcolor-004": "#000080",
    "bgcolor-005": "#800080",
    "bgcolor-006": "#008080",
    "bgcolor-007": "#c0c0c0",
    "bgcolor-008": "#808080",
    "bgcolor-009": "#ff0000",
    "bgcolor-010": "#00ff00",
    "bgcolor-011": "#ffff00",
    "bgcolor-012": "#0000ff",
    "bgcolor-013": "#ff00ff",
    "bgcolor-014": "#00ffff",
    "bgcolor-015": "#ffffff",
    "bgcolor-016": "#000000",
    "bgcolor-017": "#00005f",
    "bgcolor-018": "#000087",
    "bgcolor-019": "#0000af",
    "bgcolor-020": "#0000df",
    "bgcolor-021": "#0000ff",
    "bgcolor-022": "#005f00",
    "bgcolor-023": "#005f5f",
    "bgcolor-024": "#005f87",
    "bgcolor-025": "#005faf",
    "bgcolor-026": "#005fdf",
    "bgcolor-027": "#005fff",
    "bgcolor-028": "#008700",
    "bgcolor-029": "#00875f",
    "bgcolor-030": "#008787",
    "bgcolor-031": "#0087af",
    "bgcolor-032": "#0087df",
    "bgcolor-033": "#0087ff",
    "bgcolor-034": "#00af00",
    "bgcolor-035": "#00af5f",
    "bgcolor-036": "#00af87",
    "bgcolor-037": "#00afaf",
    "bgcolor-038": "#00afdf",
    "bgcolor-039": "#00afff",
    "bgcolor-040": "#00df00",
    "bgcolor-041": "#00df5f",
    "bgcolor-042": "#00df87",
    "bgcolor-043": "#00dfaf",
    "bgcolor-044": "#00dfdf",
    "bgcolor-045": "#00dfff",
    "bgcolor-046": "#00ff00",
    "bgcolor-047": "#00ff5f",
    "bgcolor-048": "#00ff87",
    "bgcolor-049": "#00ffaf",
    "bgcolor-050": "#00ffdf",
    "bgcolor-051": "#00ffff",
    "bgcolor-052": "#5f0000",
    "bgcolor-053": "#5f005f",
    "bgcolor-054": "#5f0087",
    "bgcolor-055": "#5f00af",
    "bgcolor-056": "#5f00df",
    "bgcolor-057": "#5f00ff",
    "bgcolor-058": "#5f5f00",
    "bgcolor-059": "#5f5f5f",
    "bgcolor-060": "#5f5f87",
    "bgcolor-061": "#5f5faf",
    "bgcolor-062": "#5f5fdf",
    "bgcolor-063": "#5f5fff",
    "bgcolor-064": "#5f8700",
    "bgcolor-065": "#5f875f",
    "bgcolor-066": "#5f8787",
    "bgcolor-067": "#5f87af",
    "bgcolor-068": "#5f87df",
    "bgcolor-069": "#5f87ff",
    "bgcolor-070": "#5faf00",
    "bgcolor-071": "#5faf5f",
    "bgcolor-072": "#5faf87",
    "bgcolor-073": "#5fafaf",
    "bgcolor-074": "#5fafdf",
    "bgcolor-075": "#5fafff",
    "bgcolor-076": "#5fdf00",
    "bgcolor-077": "#5fdf5f",
    "bgcolor-078": "#5fdf87",
    "bgcolor-079": "#5fdfaf",
    "bgcolor-080": "#5fdfdf",
    "bgcolor-081": "#5fdfff",
    "bgcolor-082": "#5fff00",
    "bgcolor-083": "#5fff5f",
    "bgcolor-084": "#5fff87",
    "bgcolor-085": "#5fffaf",
    "bgcolor-086": "#5fffdf",
    "bgcolor-087": "#5fffff",
    "bgcolor-088": "#870000",
    "bgcolor-089": "#87005f",
    "bgcolor-090": "#870087",
    "bgcolor-091": "#8700af",
    "bgcolor-092": "#8700df",
    "bgcolor-093": "#8700ff",
    "bgcolor-094": "#875f00",
    "bgcolor-095": "#875f5f",
    "bgcolor-096": "#875f87",
    "bgcolor-097": "#875faf",
    "bgcolor-098": "#875fdf",
    "bgcolor-099": "#875fff",
    "bgcolor-100": "#878700",
    "bgcolor-101": "#87875f",
    "bgcolor-102": "#878787",
    "bgcolor-103": "#8787af",
    "bgcolor-104": "#8787df",
    "bgcolor-105": "#8787ff",
    "bgcolor-106": "#87af00",
    "bgcolor-107": "#87af5f",
    "bgcolor-108": "#87af87",
    "bgcolor-109": "#87afaf",
    "bgcolor-110": "#87afdf",
    "bgcolor-111": "#87afff",
    "bgcolor-112": "#87df00",
    "bgcolor-113": "#87df5f",
    "bgcolor-114": "#87df87",
    "bgcolor-115": "#87dfaf",
    "bgcolor-116": "#87dfdf",
    "bgcolor-117": "#87dfff",
    "bgcolor-118": "#87ff00",
    "bgcolor-119": "#87ff5f",
    "bgcolor-120": "#87ff87",
    "bgcolor-121": "#87ffaf",
    "bgcolor-122": "#87ffdf",
    "bgcolor-123": "#87ffff",
    "bgcolor-124": "#af0000",
    "bgcolor-125": "#af005f",
    "bgcolor-126": "#af0087",
    "bgcolor-127": "#af00af",
    "bgcolor-128": "#af00df",
    "bgcolor-129": "#af00ff",
    "bgcolor-130": "#af5f00",
    "bgcolor-131": "#af5f5f",
    "bgcolor-132": "#af5f87",
    "bgcolor-133": "#af5faf",
    "bgcolor-134": "#af5fdf",
    "bgcolor-135": "#af5fff",
    "bgcolor-136": "#af8700",
    "bgcolor-137": "#af875f",
    "bgcolor-138": "#af8787",
    "bgcolor-139": "#af87af",
    "bgcolor-140": "#af87df",
    "bgcolor-141": "#af87ff",
    "bgcolor-142": "#afaf00",
    "bgcolor-143": "#afaf5f",
    "bgcolor-144": "#afaf87",
    "bgcolor-145": "#afafaf",
    "bgcolor-146": "#afafdf",
    "bgcolor-147": "#afafff",
    "bgcolor-148": "#afdf00",
    "bgcolor-149": "#afdf5f",
    "bgcolor-150": "#afdf87",
    "bgcolor-151": "#afdfaf",
    "bgcolor-152": "#afdfdf",
    "bgcolor-153": "#afdfff",
    "bgcolor-154": "#afff00",
    "bgcolor-155": "#afff5f",
    "bgcolor-156": "#afff87",
    "bgcolor-157": "#afffaf",
    "bgcolor-158": "#afffdf",
    "bgcolor-159": "#afffff",
    "bgcolor-160": "#df0000",
    "bgcolor-161": "#df005f",
    "bgcolor-162": "#df0087",
    "bgcolor-163": "#df00af",
    "bgcolor-164": "#df00df",
    "bgcolor-165": "#df00ff",
    "bgcolor-166": "#df5f00",
    "bgcolor-167": "#df5f5f",
    "bgcolor-168": "#df5f87",
    "bgcolor-169": "#df5faf",
    "bgcolor-170": "#df5fdf",
    "bgcolor-171": "#df5fff",
    "bgcolor-172": "#df8700",
    "bgcolor-173": "#df875f",
    "bgcolor-174": "#df8787",
    "bgcolor-175": "#df87af",
    "bgcolor-176": "#df87df",
    "bgcolor-177": "#df87ff",
    "bgcolor-178": "#dfaf00",
    "bgcolor-179": "#dfaf5f",
    "bgcolor-180": "#dfaf87",
    "bgcolor-181": "#dfafaf",
    "bgcolor-182": "#dfafdf",
    "bgcolor-183": "#dfafff",
    "bgcolor-184": "#dfdf00",
    "bgcolor-185": "#dfdf5f",
    "bgcolor-186": "#dfdf87",
    "bgcolor-187": "#dfdfaf",
    "bgcolor-188": "#dfdfdf",
    "bgcolor-189": "#dfdfff",
    "bgcolor-190": "#dfff00",
    "bgcolor-191": "#dfff5f",
    "bgcolor-192": "#dfff87",
    "bgcolor-193": "#dfffaf",
    "bgcolor-194": "#dfffdf",
    "bgcolor-195": "#dfffff",
    "bgcolor-196": "#ff0000",
    "bgcolor-197": "#ff005f",
    "bgcolor-198": "#ff0087",
    "bgcolor-199": "#ff00af",
    "bgcolor-200": "#ff00df",
    "bgcolor-201": "#ff00ff",
    "bgcolor-202": "#ff5f00",
    "bgcolor-203": "#ff5f5f",
    "bgcolor-204": "#ff5f87",
    "bgcolor-205": "#ff5faf",
    "bgcolor-206": "#ff5fdf",
    "bgcolor-207": "#ff5fff",
    "bgcolor-208": "#ff8700",
    "bgcolor-209": "#ff875f",
    "bgcolor-210": "#ff8787",
    "bgcolor-211": "#ff87af",
    "bgcolor-212": "#ff87df",
    "bgcolor-213": "#ff87ff",
    "bgcolor-214": "#ffaf00",
    "bgcolor-215": "#ffaf5f",
    "bgcolor-216": "#ffaf87",
    "bgcolor-217": "#ffafaf",
    "bgcolor-218": "#ffafdf",
    "bgcolor-219": "#ffafff",
    "bgcolor-220": "#ffdf00",
    "bgcolor-221": "#ffdf5f",
    "bgcolor-222": "#ffdf87",
    "bgcolor-223": "#ffdfaf",
    "bgcolor-224": "#ffdfdf",
    "bgcolor-225": "#ffdfff",
    "bgcolor-226": "#ffff00",
    "bgcolor-227": "#ffff5f",
    "bgcolor-228": "#ffff87",
    "bgcolor-229": "#ffffaf",
    "bgcolor-230": "#ffffdf",
    "bgcolor-231": "#ffffff",
    "bgcolor-232": "#080808",
    "bgcolor-233": "#121212",
    "bgcolor-234": "#1c1c1c",
    "bgcolor-235": "#262626",
    "bgcolor-236": "#303030",
    "bgcolor-237": "#3a3a3a",
    "bgcolor-238": "#444444",
    "bgcolor-239": "#4e4e4e",
    "bgcolor-240": "#585858",
    "bgcolor-241": "#606060",
    "bgcolor-242": "#666666",
    "bgcolor-243": "#767676",
    "bgcolor-244": "#808080",
    "bgcolor-245": "#8a8a8a",
    "bgcolor-246": "#949494",
    "bgcolor-247": "#9e9e9e",
    "bgcolor-248": "#a8a8a8",
    "bgcolor-249": "#b2b2b2",
    "bgcolor-250": "#bcbcbc",
    "bgcolor-251": "#c6c6c6",
    "bgcolor-252": "#d0d0d0",
    "bgcolor-253": "#dadada",
    "bgcolor-254": "#e4e4e4",
    "bgcolor-255": "#eeeeee",
}

"""
The classes below exist to properly encapsulate text and other tag classes
because the order of how tags are opened and closed are important to display in godot.
"""

class RootTag:
    """
    The Root tag class made to contain other tags.
    """

    __slots__ = ("child",)

    def __init__(self):
        self.child = None

        return str(self.child) if self.child else ""

class ChildTag:
    """
    A node made to be contained.
    """

    def __init__(self, parent):
        self.parent = parent
        if parent:
            parent.child = self

    def set_parent(self, parent):
        if parent:

class TextTag(ChildTag):
    """
    A BBCodeTag node to output regular text.
    Output: SomeText
    """

    __slots__ = ("parent", "child", "text")

    def __init__(self, parent, text):
        super().__init__(parent)

        return f"{self.text}{self.child or ''}"

class BBCodeTag(ChildTag):
    """
    Base BBCodeTag node to encapsulate and be encapsulated.
    """

    __slots__ = (
        "parent",
        "child",
    )

    code = ""

        return f"[{self.code}]{self.child or ''}[/{self.code}]"

class UnderlineTag(BBCodeTag):
    """
    A BBCodeTag node for underlined text.
    Output: [u]Underlined Text[/u]
    """

    code = "u"

class BlinkTag(BBCodeTag):
    """
    A BBCodeTag node for blinking text.
    Output: [blink]Blinking Text[/blink]
    """

    code = "blink"

class ColorTag(BBCodeTag):
    """
    A BBCodeTag node for foreground color.
    Output: [fgcolor=#000000]Colorized Text[/fgcolor]
    """

        "parent",
        "child",
        "color_hex",
    )

    code = "color"

    def __init__(self, parent, color_hex):
        self.color_hex = color_hex

        return f"[{self.code}={self.color_hex}]{self.child or ''}[/{self.code}]"

class BGColorTag(ColorTag):
    """
    A BBCodeTag node for background color.
    Output: [bgcolor=#000000]Colorized Text[/bgcolor]
    """

    code = "bgcolor"

class UrlTag(BBCodeTag):
    """
    A BBCodeTag node used for urls.
    Output: [url=www.example.com]Child Text[/url]

    """

        "parent",
        "child",
        "url_data",
    )

    code = "url"

    def __init__(self, parent, url_data=""):
        self.url_data = url_data

        return f"[{self.code}={self.url_data}]{self.child or ''}[/{self.code}]"

class TextToBBCODEparser(TextToHTMLparser):
    """
    This class describes a parser for converting from ANSI to BBCode.
    It inherits from the TextToHTMLParser and overrides the specifics for bbcode.
    """

    def convert_urls(self, text):
        """
        Converts urls within text to bbcode style

        Args:
            text (str): Text to parse

        Returns:
             text (str): Processed text
        """
        # Converts to bbcode styled urls
        return self.re_url.sub(r"[url=\1]\1[/url]\2", text)

    def sub_mxp_links(self, match):
        """
        Helper method to be passed to re.sub,
        replaces MXP links with bbcode.

        Args:
            match (re.Matchobject): Match for substitution.

        Returns:
            text (str): Processed text.

        """
        cmd, text = [grp.replace('"', "\\&quot;") for grp in match.groups()]
        val = f"[mxp=send cmd={cmd}]{text}[/mxp]"

        return val

    def sub_mxp_urls(self, match):
        """
        Args:
        Returns:
        """

        url, text = [grp.replace('"', "\\&quot;") for grp in match.groups()]
        val = f"[url={url}]{text}[/url]"

        return val

    def sub_text(self, match):
        """
        for handling all substitutions.

        Args:

        Returns:

        """
        cdict = match.groupdict()
        if cdict["lineend"]:
            return "\n"

    def format_styles(self, text):
        """
        Takes a string with parsed ANSI codes and replaces them with bbcode style tags

        Args:
            text (str): The string to process.

        Returns:
        """

        # split out the ANSI codes and clean out any empty items
        str_list = [substr for substr in self.re_style.split(text) if substr]

        inverse = False
        # default color is light grey - unhilite + white
        hilight = ANSI_UNHILITE
        fg = ANSI_WHITE
        # default bg is black
        bg = ANSI_BACK_BLACK
        previous_fg = None
        previous_bg = None
        blink = False
        underline = False
        new_style = False

        parts = []
        root_tag = RootTag()
        current_tag = root_tag

        for i, substr in enumerate(str_list):
            # reset all current styling
            if substr == ANSI_NORMAL:
                # close any existing span if necessary
                parts.append(str(root_tag))
                root_tag = RootTag()
                current_tag = root_tag
                # reset to defaults
                inverse = False
                hilight = ANSI_UNHILITE
                fg = ANSI_WHITE
                bg = ANSI_BACK_BLACK
                previous_fg = None
                previous_bg = None
                blink = False
                underline = False
                new_style = False

            # change color
            elif substr in self.ansi_color_codes + self.xterm_fg_codes:
                # set new color
                fg = substr
                new_style = True

            # change bg color
            elif substr in self.ansi_bg_codes + self.xterm_bg_codes:
                # set new bg
                bg = substr

            # non-color codes
            elif substr in self.style_codes:

                # hilight codes
                if substr in (ANSI_HILITE, ANSI_UNHILITE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
                    # set new hilight status
                    hilight = ANSI_UNHILITE if substr == ANSI_UNHILITE else ANSI_HILITE

                # inversion codes
                if substr in (ANSI_INVERSE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
                    inverse = True

                # blink codes
                if substr in (ANSI_BLINK, ANSI_BLINK_HILITE, ANSI_INV_BLINK_HILITE) and not blink:
                    blink = True
                    current_tag = BlinkTag(current_tag)

                # underline
                if substr == ANSI_UNDERLINE and not underline:
                    underline = True
                    current_tag = UnderlineTag(current_tag)

            else:
                close_tags = False
                color_tag = None
                bgcolor_tag = None
                # normal text, add text back to list
                if new_style:
                    # prior entry was cleared, which means style change
                    # get indices for the fg and bg codes
                    bg_index = self.bglist.index(bg)
                    try:
                        color_index = self.colorlist.index(hilight + fg)
                    except ValueError:
                        # xterm256 colors don't have the hilight codes
                        color_index = self.colorlist.index(fg)

                    if inverse:
                        # inverse means swap fg and bg indices
                        bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0"))
                        color_class = "color-{}".format(str(bg_index).rjust(3, "0"))
                    else:
                        # use fg and bg indices for classes
                        bg_class = "bgcolor-{}".format(str(bg_index).rjust(3, "0"))
                        color_class = "color-{}".format(str(color_index).rjust(3, "0"))

                    # black bg is the default, don't explicitly style
                    if bg_class != "bgcolor-000":
                        color_hex = COLOR_INDICE_TO_HEX.get(bg_class)
                        bgcolor_tag = BGColorTag(None, color_hex=color_hex)
                        if previous_bg and previous_bg != color_hex:
                            close_tags = True
                        else:
                            previous_bg = color_hex

                    # light grey text is the default, don't explicitly style
                    if color_class != "color-007":
                        color_hex = COLOR_INDICE_TO_HEX.get(color_class)
                        color_tag = ColorTag(None, color_hex=color_hex)
                        if previous_fg and previous_fg != color_hex:
                        else:
                            previous_fg = color_hex

                new_tag = TextTag(None, substr)
                if close_tags:
                    # Because the order is important, we need to close the tags and reopen those who shouldn't reset.
                    new_style = False
                    parts.append(str(root_tag))
                    root_tag = RootTag()
                    current_tag = root_tag
                    if blink:
                        current_tag = BlinkTag(current_tag)

                    if underline:
                        current_tag = UnderlineTag(current_tag)

                    if bgcolor_tag:
                        bgcolor_tag.set_parent(current_tag)
                        current_tag = bgcolor_tag

                    if color_tag:
                        color_tag.set_parent(current_tag)
                        current_tag = color_tag

                    new_tag.set_parent(current_tag)
                    current_tag = new_tag
                else:

        any_text = self._get_text_tag(root_tag)
        if any_text:
            # Only append tags if text was added.
            last_part = str(root_tag)
            parts.append(last_part)

        # recombine back into string
        return "".join(parts)

    def _get_text_tag(self, root):
        child = root.child
        while child:
            if isinstance(child, TextTag):
                return child
            else:
                child = child.child

    def parse(self, text, strip_ansi=False):
        """
        Main access function, converts a text containing ANSI codes
        into html statements.

        Args:
            text (str): Text to process.
            strip_ansi (bool, optional):

        Returns:
            text (str): Parsed text.

        """
        # parse everything to ansi first
        text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True)
        # convert all ansi to html
        result = re.sub(self.re_string, self.sub_text, text)
        result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
        result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result)
        result = self.remove_bells(result)
        result = self.format_styles(result)
        result = self.remove_backspaces(result)
        result = self.convert_urls(result)

BBCODE_PARSER = TextToBBCODEparser()

#
# Access function
#

def parse_to_bbcode(string, strip_ansi=False, parser=BBCODE_PARSER):
    """
    Parses a string, replace ANSI markup with bbcode
    """
    return parser.parse(string, strip_ansi=strip_ansi)

"""

This file contains the code necessary to dedicate a port to communicate with Godot via Websockets.
It uses the plugin system and should be plugged via settings as detailed in the readme.
"""
import json

from autobahn.twisted import WebSocketServerFactory
from twisted.application import internet

from evennia import settings
from evennia.contrib.base_systems.godotwebsocket.text2bbcode import parse_to_bbcode
from evennia.server.portal import webclient
from evennia.settings_default import LOCKDOWN_MODE

class GodotWebSocketClient(webclient.WebSocketClient):
    """
    Implements the server-side of the Websocket connection specific to Godot.
    It inherits from the basic Websocket implementation and changes only what is necessary.

    """

        self.protocol_key = "godotclient/websocket"

    def send_text(self, *args, **kwargs):
        """
        Send text data. This will pre-process the text for
        color-replacement, conversion to bbcode etc.

        Args:
            text (str): Text to send.

            options (dict): Options-dict with the following keys understood:
                - nocolor (bool): Clean out all color.
                - send_prompt (bool): Send a prompt with parsed bbcode

        """
        if args:
            args = list(args)
            text = args[0]
                return
        else:
            return

        flags = self.protocol_flags

        options = kwargs.pop("options", {})
        nocolor = options.get("nocolor", flags.get("NOCOLOR", False))
        prompt = options.get("send_prompt", False)

        cmd = "prompt" if prompt else "text"
        args[0] = parse_to_bbcode(text, strip_ansi=nocolor)

        # send to client on required form [cmdname, args, kwargs]
        self.sendLine(json.dumps([cmd, args, kwargs]))

def start_plugin_services(portal):
    class GodotWebsocket(WebSocketServerFactory):
        "Only here for better naming in logs"
        pass

    factory = GodotWebsocket()
    factory.noisy = False
    factory.protocol = GodotWebSocketClient
    from evennia.server.portal.portalsessionhandler import PORTAL_SESSIONS

    factory.sessionhandler = PORTAL_SESSIONS

    interface = "127.0.0.1" if LOCKDOWN_MODE else settings.GODOT_CLIENT_WEBSOCKET_CLIENT_INTERFACE

    port = settings.GODOT_CLIENT_WEBSOCKET_PORT
    websocket_service = internet.TCPServer(port, factory, interface=interface)
    websocket_service.setName("GodotWebSocket%s:%s" % (interface, port))
    portal.services.addService(websocket_service)

"""
Module containing the CallbackHandler for individual objects.
"""

class CallbackHandler(object):

    """
    The callback handler for a specific object.

    The script that contains all callbacks will be reached through this
    handler.  This handler is therefore a shortcut to be used by
    developers.  This handler (accessible through `obj.callbacks`) is a
    shortcut to manipulating callbacks within this object, getting,
    adding, editing, deleting and calling them.

    """

    script = None

        """
        Return all callbacks linked to this object.

        Returns:
            All callbacks in a dictionary callback_name: callback}.  The callback
            is returned as a namedtuple to simplify manipulation.

        """
        callbacks = {}
        handler = type(self).script
        if handler:
            dicts = handler.get_callbacks(self.obj)
            for callback_name, in_list in dicts.items():
                new_list = []
                for callback in in_list:
                    callback = self.format_callback(callback)
                    new_list.append(callback)

                if new_list:
                    callbacks[callback_name] = new_list

        return callbacks

    def get(self, callback_name):
        """
        Return the callbacks associated with this name.

        Args:
            callback_name (str): the name of the callback.

        Returns:
            A list of callbacks associated with this object and of this name.

        Note:
            This method returns a list of callback objects (namedtuple
            representations).  If the callback name cannot be found in the
            object's callbacks, return an empty list.

        """
        return self.all().get(callback_name, [])

    def get_variable(self, variable_name):
        """
        Return the variable value or None.

        Args:
            variable_name (str): the name of the variable.

        Returns:
            Either the variable's value or None.

        """
            return handler.get_variable(variable_name)

    def add(self, callback_name, code, author=None, valid=False, parameters=""):
        """
        Add a new callback for this object.

        Args:
            callback_name (str): the name of the callback to add.
            code (str): the Python code associated with this callback.
            author (Character or Account, optional): the author of the callback.
            valid (bool, optional): should the callback be connected?
            parameters (str, optional): optional parameters.

        Returns:
            The callback definition that was added or None.

        """
            return self.format_callback(
                handler.add_callback(
                    self.obj, callback_name, code, author=author, valid=valid, parameters=parameters
                )
            )

    def edit(self, callback_name, number, code, author=None, valid=False):
        """
        Edit an existing callback bound to this object.

        Args:
            callback_name (str): the name of the callback to edit.
            number (int): the callback number to be changed.

        Returns:
            The callback definition that was edited or None.

        Raises:
            RuntimeError if the callback is locked.

        """
                handler.edit_callback(
                    self.obj, callback_name, number, code, author=author, valid=valid
                )
            )

    def remove(self, callback_name, number):
        """
        Delete the specified callback bound to this object.

        Args:
            callback_name (str): the name of the callback to delete.
            number (int): the number of the callback to delete.

        Raises:

        """
            handler.del_callback(self.obj, callback_name, number)

    def call(self, callback_name, *args, **kwargs):
        """
        Call the specified callback(s) bound to this object.

        Args:
            callback_name (str): the callback name to call.
            *args: additional variables for this callback.

            number (int, optional): call just a specific callback.
            parameters (str, optional): call a callback with parameters.
            locals (dict, optional): a locals replacement.

        Returns:
            True to report the callback was called without interruption,
            False otherwise.  If the callbackHandler isn't found, return
            None.

        """
            return handler.call(self.obj, callback_name, *args, **kwargs)

    def format_callback(callback):
        """
        Return the callback namedtuple to represent the specified callback.

        Args:
            callback (dict): the callback definition.

        The callback given in argument should be a dictionary containing
        the expected fields for a callback (code, author, valid...).

        """
        if "obj" not in callback:
            callback["obj"] = None
        if "name" not in callback:
            callback["name"] = "unknown"
        if "number" not in callback:
            callback["number"] = -1
        if "code" not in callback:
            callback["code"] = ""
        if "author" not in callback:
            callback["author"] = None
        if "valid" not in callback:
            callback["valid"] = False
        if "parameters" not in callback:
            callback["parameters"] = ""
        if "created_on" not in callback:
            callback["created_on"] = None
        if "updated_by" not in callback:
            callback["updated_by"] = None
        if "updated_on" not in callback:
            callback["updated_on"] = None

        return Callback(**callback)

Callback = namedtuple(
    "Callback",
    (
        "obj",
        "name",
        "number",
        "code",
        "author",
        "valid",
        "parameters",
        "created_on",
        "updated_by",
        "updated_on",
    ),
)

"""
Module containing the commands of the in-game Python system.
"""

from datetime import datetime

from evennia.contrib.base_systems.ingame_python.utils import get_event_handler
from evennia.utils.ansi import raw
from evennia.utils.utils import class_from_module, time_format

# Permissions
WITH_VALIDATION = getattr(settings, "callbackS_WITH_VALIDATION", None)
WITHOUT_VALIDATION = getattr(settings, "callbackS_WITHOUT_VALIDATION", "developer")
VALIDATING = getattr(settings, "callbackS_VALIDATING", "developer")

# Split help text
BASIC_HELP = "Add, edit or delete callbacks."

BASIC_USAGES = [
    "@call <object name> [= <callback name>]",
    "@call/add <object name> = <callback name> [parameters]",
    "@call/edit <object name> = <callback name> [callback number]",
    "@call/del <object name> = <callback name> [callback number]",
    "@call/tasks [object name [= <callback name>]]",
]

BASIC_SWITCHES = [
    "add    - add and edit a new callback",
    "edit   - edit an existing callback",
    "del    - delete an existing callback",
    "tasks  - show the list of differed tasks",
]

VALIDATOR_USAGES = ["@call/accept [object name = <callback name> [callback number]]"]

VALIDATOR_SWITCHES = ["accept - show callbacks to be validated or accept one"]

BASIC_TEXT = """
This command is used to manipulate callbacks.  A callback can be linked to
an object, to fire at a specific moment.  You can use the command without
switches to see what callbacks are active on an object:
  @call self
You can also specify a callback name if you want the list of callbacks
associated with this object of this name:
  @call north = can_traverse
You can also add a number after the callback name to see details on one callback:
  @call here = say 2
You can also add, edit or remove callbacks using the add, edit or del switches.
Additionally, you can see the list of differed tasks created by callbacks
(chained events to be called) using the /tasks switch.
"""

VALIDATOR_TEXT = """
You can also use this command to validate callbacks.  Depending on your game
setting, some users might be allowed to add new callbacks, but these callbacks
will not be fired until you accept them.  To see the callbacks needing
validation, enter the /accept switch without argument:
  @call/accept
A table will show you the callbacks that are not validated yet, who created
them and when.  You can then accept a specific callback:
  @call here = enter 1
Use the /del switch to remove callbacks that should not be connected.
"""

class CmdCallback(COMMAND_DEFAULT_CLASS):

    """
    Command to edit callbacks.
    """

    key = "@call"
    aliases = ["@callback", "@callbacks", "@calls"]
    locks = "cmd:perm({})".format(VALIDATING)
    if WITH_VALIDATION:
        locks += " or perm({})".format(WITH_VALIDATION)

        """

        The help text of this specific command will vary depending
        on user permission.

        Args:

        Returns:

        """
        lock = "perm({}) or perm(callbacks_validating)".format(VALIDATING)
        validator = caller.locks.check_lockstring(caller, lock)
        text = "\n" + BASIC_HELP + "\n\nUsages:\n  "

        # Usages
        text += "\n  ".join(BASIC_USAGES)
        if validator:
            text += "\n  " + "\n  ".join(VALIDATOR_USAGES)

        # Switches
        text += "\n\nSwitches:\n  "
        text += "\n  ".join(BASIC_SWITCHES)
            text += "\n  " + "\n  ".join(VALIDATOR_SWITCHES)

        # Text
        text += "\n" + BASIC_TEXT
            text += "\n" + VALIDATOR_TEXT

        """Command body."""
        lock = "perm({}) or perm(events_validating)".format(VALIDATING)
        lock = "perm({}) or perm(events_without_validation)".format(WITHOUT_VALIDATION)
        autovalid = caller.locks.check_lockstring(caller, lock)

        # First and foremost, get the callback handler and set other variables
        self.handler = get_event_handler()
        self.obj = None
        rhs = self.rhs or ""
        self.callback_name, sep, self.parameters = rhs.partition(" ")
        self.callback_name = self.callback_name.lower()
        self.is_validator = validator
        self.autovalid = autovalid
        if self.handler is None:
            caller.msg("The event handler is not running, can't " "access the event system.")
            return

        # Before the equal sign, there is an object name or nothing
        if self.lhs:
            self.obj = caller.search(self.lhs)
            if not self.obj:
                return

        # Switches are mutually exclusive
        switch = self.switches and self.switches[0] or ""
        if switch in ("", "add", "edit", "del") and self.obj is None:
            caller.msg("Specify an object's name or #ID.")
            return

        if switch == "":
            self.list_callbacks()
        elif switch == "add":
            self.add_callback()
        elif switch == "edit":
            self.edit_callback()
        elif switch == "del":
            self.del_callback()
        elif switch == "accept" and validator:
            self.accept_callback()
        elif switch in ["tasks", "task"]:
            self.list_tasks()
        else:
            caller.msg("Mutually exclusive or invalid switches were " "used, cannot proceed.")

    def list_callbacks(self):
        """Display the list of callbacks connected to the object."""
        obj = self.obj
        callback_name = self.callback_name
        parameters = self.parameters
        callbacks = self.handler.get_callbacks(obj)
        types = self.handler.get_events(obj)

        if callback_name:
            # Check that the callback name can be found in this object
            created = callbacks.get(callback_name)
            if created is None:
                self.msg("No callback {} has been set on {}.".format(callback_name, obj))
                return

            if parameters:
                # Check that the parameter points to an existing callback
                try:
                    number = int(parameters) - 1
                    assert number >= 0
                    callback = callbacks[callback_name][number]
                except (ValueError, AssertionError, IndexError):
                    self.msg(
                        "The callback {} {} cannot be found in {}.".format(
                            callback_name, parameters, obj
                        )
                    )
                    return

                # Display the callback's details
                author = callback.get("author")
                author = author.key if author else "|gUnknown|n"
                updated_by = callback.get("updated_by")
                updated_by = updated_by.key if updated_by else "|gUnknown|n"
                created_on = callback.get("created_on")
                created_on = (
                    created_on.strftime("%Y-%m-%d %H:%M:%S") if created_on else "|gUnknown|n"
                )
                updated_on = callback.get("updated_on")
                updated_on = (
                    updated_on.strftime("%Y-%m-%d %H:%M:%S") if updated_on else "|gUnknown|n"
                )
                msg = "Callback {} {} of {}:".format(callback_name, parameters, obj)
                msg += "\nCreated by {} on {}.".format(author, created_on)
                msg += "\nUpdated by {} on {}".format(updated_by, updated_on)

                if self.is_validator:
                    if callback.get("valid"):
                        msg += "\nThis callback is |rconnected|n and active."
                    else:
                        msg += "\nThis callback |rhasn't been|n accepted yet."

                msg += "\nCallback code:\n"
                msg += raw(callback["code"])
                self.msg(msg)
                return

            # No parameter has been specified, display the table of callbacks
            cols = ["Number", "Author", "Updated", "Param"]
            if self.is_validator:
                cols.append("Valid")

            table = EvTable(*cols, width=78)
            table.reformat_column(0, align="r")
            now = datetime.now()
            for i, callback in enumerate(created):
                if updated_on is None:
                    updated_on = callback.get("created_on")

                if updated_on:
                    updated_on = "{} ago".format(
                        time_format((now - updated_on).total_seconds(), 4).capitalize()
                    )
                else:
                    updated_on = "|gUnknown|n"
                parameters = callback.get("parameters", "")

                row = [str(i + 1), author, updated_on, parameters]
                    row.append("Yes" if callback.get("valid") else "No")

            self.msg(str(table))
        else:
            names = list(set(list(types.keys()) + list(callbacks.keys())))
            table = EvTable("Callback name", "Number", "Description", valign="t", width=78)
            table.reformat_column(0, width=20)
            table.reformat_column(1, width=10, align="r")
            table.reformat_column(2, width=48)
            for name in sorted(names):
                number = len(callbacks.get(name, []))
                lines = sum(len(e["code"].splitlines()) for e in callbacks.get(name, []))
                no = "{} ({})".format(number, lines)
                description = types.get(name, (None, "Chained event."))[1]
                description = description.strip("\n").splitlines()[0]
                table.add_row(name, no, description)

    def add_callback(self):
        """Add a callback."""

        # Check that the callback exists
        if not callback_name.startswith("chain_") and callback_name not in types:
            self.msg(
                "The callback name {} can't be found in {} of "
                "typeclass {}.".format(callback_name, obj, type(obj))
            )
            return

        definition = types.get(callback_name, (None, "Chained event."))
        description = definition[1]
        self.msg(raw(description.strip("\n")))

        # Open the editor
        callback = self.handler.add_callback(
            obj, callback_name, "", self.caller, False, parameters=self.parameters
        )

        # Lock this callback right away
        self.handler.db.locked.append((obj, callback_name, callback["number"]))

        # Open the editor for this callback
        self.caller.db._callback = callback
        EvEditor(
            loadfunc=_ev_load,
            savefunc=_ev_save,
            quitfunc=_ev_quit,
            key="Callback {} of {}".format(callback_name, obj),
            codefunc=_ev_save,
        )

    def edit_callback(self):
        """Edit a callback."""

        # If no callback name is specified, display the list of callbacks
        if not callback_name:
            return

        if callback_name not in callbacks:
            self.msg("The callback name {} can't be found in {}.".format(callback_name, obj))
            return

        # If there's only one callback, just edit it
        if len(callbacks[callback_name]) == 1:
            number = 0
            callback = callbacks[callback_name][0]
        else:
            if not parameters:
                self.msg("Which callback do you wish to edit?  Specify a number.")
                self.list_callbacks()
                return

            # Check that the parameter points to an existing callback
            try:
                number = int(parameters) - 1
                assert number >= 0
                callback = callbacks[callback_name][number]
            except (ValueError, AssertionError, IndexError):
                self.msg(
                    "The callback {} {} cannot be found in {}.".format(
                        callback_name, parameters, obj
                    )
                )
                return

        # If caller can't edit without validation, forbid editing
        # others' works
        if not self.autovalid and callback["author"] is not self.caller:
            self.msg("You cannot edit this callback created by someone else.")
            return

        # If the callback is locked (edited by someone else)
        if (obj, callback_name, number) in self.handler.db.locked:
            self.msg("This callback is locked, you cannot edit it.")
            return

        self.handler.db.locked.append((obj, callback_name, number))

        # Check the definition of the callback

        callback = dict(callback)
        EvEditor(
        )

    def del_callback(self):
        """Delete a callback."""

            return

            return

        # If there's only one callback, just delete it
            number = 0
        else:
                self.msg("Which callback do you wish to delete?  Specify " "a number.")
                return

            try:
                self.msg(
                    )
                )
                return

        # If caller can't edit without validation, forbid deleting
            self.msg("You cannot delete this callback created by someone else.")
            return

            self.msg("This callback is locked, you cannot delete it.")
            return

        # Delete the callback
        self.handler.del_callback(obj, callback_name, number)
        self.msg("The callback {}[{}] of {} was deleted.".format(callback_name, number + 1, obj))

    def accept_callback(self):
        """Accept a callback."""

        # If no object, display the list of callbacks to be checked
            table = EvTable("ID", "Type", "Object", "Name", "Updated by", "On", width=78)
            for obj, name, number in self.handler.db.to_valid:
                callbacks = self.handler.get_callbacks(obj).get(name)
                if callbacks is None:
                    continue

                try:
                    callback = callbacks[number]
                    continue

                type_name = obj.typeclass_path.split(".")[-1]
                by = callback.get("updated_by")
                by = by.key if by else "|gUnknown|n"

                    )
                else:

                table.add_row(obj.id, type_name, obj, name, by, updated_on)
            return

        # An object was specified

            return

            return

        if not parameters:
            self.msg("Which callback do you wish to accept?  Specify a number.")
            return

        # Check that the parameter points to an existing callback
        try:
            number = int(parameters) - 1
            assert number >= 0
            callback = callbacks[callback_name][number]
        except (ValueError, AssertionError, IndexError):
            self.msg(
                "The callback {} {} cannot be found in {}.".format(callback_name, parameters, obj)
            )
            return

        # Accept the callback
        if callback["valid"]:
            self.msg("This callback has already been accepted.")
        else:
            self.handler.accept_callback(obj, callback_name, number)
            self.msg(
                "The callback {} {} of {} has been accepted.".format(callback_name, parameters, obj)
            )

    def list_tasks(self):
        """List the active tasks."""
        handler = self.handler
        tasks = [(k, v[0], v[1], v[2]) for k, v in handler.db.tasks.items()]
        if obj:
            tasks = [task for task in tasks if task[2] is obj]
            tasks = [task for task in tasks if task[3] == callback_name]

        tasks.sort()
        table = EvTable("ID", "Object", "Callback", "In", width=78)
        table.reformat_column(0, align="r")
        now = datetime.now()
        for task_id, future, obj, callback_name in tasks:
            key = obj.get_display_name(self.caller)
            delta = time_format((future - now).total_seconds(), 1)
            table.add_row(task_id, key, callback_name, delta)

# Private functions to handle editing

def _ev_load(caller):
    return caller.db._callback and caller.db._callback.get("code", "") or ""

def _ev_save(caller, buf):
    """Save and add the callback."""
    lock = "perm({}) or perm(events_without_validation)".format(WITHOUT_VALIDATION)
    autovalid = caller.locks.check_lockstring(caller, lock)
    callback = caller.db._callback
    handler = get_event_handler()
    if (
        not handler
        or not callback
        or not all(key in callback for key in ("obj", "name", "number", "valid"))
    ):
        caller.msg("Couldn't save this callback.")

    if (callback["obj"], callback["name"], callback["number"]) in handler.db.locked:
        handler.db.locked.remove((callback["obj"], callback["name"], callback["number"]))

    handler.edit_callback(
        callback["obj"], callback["name"], callback["number"], buf, caller, valid=autovalid
    )

def _ev_quit(caller):
    if (
    ):

    del caller.db._callback

"""
Module defining basic eventfuncs for the event system.

Eventfuncs are just Python functions that can be used inside of calllbacks.

"""

from evennia import ObjectDB
from evennia.contrib.base_systems.ingame_python.utils import InterruptEvent

def deny():
    """
    Deny, that is stop, the callback here.

    Notes:
        This function will raise an exception to terminate the callback
        in a controlled way.  If you use this function in an event called
        prior to a command, the command will be cancelled as well.  Good
        situations to use the `deny()` function are in events that begins
        by `can_`, because they usually can be cancelled as easily as that.

    """
    raise InterruptEvent

def get(**kwargs):
    """
    Return an object with the given search option or None if None is found.

        Any searchable data or property (id, db_key, db_location...).

    Returns:
        The object found that meet these criteria for research, or
        None if none is found.

    Notes:
        This function is very useful to retrieve objects with a specific
        ID.  You know that room #32 exists, but you don't have it in
        the callback variables.  Quite simple:
            room = get(id=32)

        This function doesn't perform a search on objects, but a direct
        search in the database.  It's recommended to use it for objects
        you know exist, using their IDs or other unique attributes.
        Looking for objects by key is possible (use `db_key` as an
        argument) but remember several objects can share the same key.

    """
    try:
        object = ObjectDB.objects.get(**kwargs)
    except ObjectDB.DoesNotExist:
        object = None

    return object

def call_event(obj, event_name, seconds=0):
    """
    Call the specified event in X seconds.

    Args:
        obj (Object): the typeclassed object containing the event.
        event_name (str): the event name to be called.
        seconds (int or float): the number of seconds to wait before calling
                the event.

    Notes:
        This eventfunc can be used to call other events from inside of an
        event in a given time.  This will create a pause between events.  This
        will not freeze the game, and you can expect characters to move
        around (unless you prevent them from doing so).

        Variables that are accessible in your event using 'call()' will be
        kept and passed on to the event to call.

        Chained callbacks are designed for this very purpose: they
        are never called automatically by the game, rather, they need
        to be called from inside another event.

    """
    script = type(obj.callbacks).script
    if script:
        # If seconds is 0, call the event immediately
        if seconds == 0:
            locals = dict(script.ndb.current_locals)
            obj.callbacks.call(event_name, locals=locals)
        else:
            # Schedule the task
            script.set_task(seconds, obj, event_name)

"""
Scripts for the in-game Python system.
"""

import re
import sys
from datetime import datetime, timedelta
from queue import Queue

from evennia import ChannelDB, DefaultObject, DefaultScript, ScriptDB, logger
from evennia.contrib.base_systems.ingame_python.callbackhandler import CallbackHandler
from evennia.contrib.base_systems.ingame_python.utils import (
    EVENTS,
    InterruptEvent,
    get_next_wait,
)
from evennia.utils.create import create_channel
from evennia.utils.dbserialize import dbserialize
from evennia.utils.utils import all_from_module, delay, pypath_to_realpath

RE_LINE_ERROR = re.compile(r'^  File "\<string\>", line (\d+)')

class EventHandler(DefaultScript):

    """
    The event handler that contains all events in a global script.

    This script shouldn't be created more than once.  It contains
    event (in a non-persistent attribute) and callbacks (in a
    persistent attribute).  The script method would help adding,
    editing and deleting these events and callbacks.

    """

        """Hook called when the script is created."""
        self.key = "event_handler"
        self.desc = "Global event handler"

        # Permanent data to be stored
        self.db.callbacks = {}
        self.db.to_valid = []
        self.db.locked = []

        # Tasks
        self.db.tasks = {}
        self.at_server_start()

        """Set up the event system when starting.

        Note that this hook is called every time the server restarts
        (including when it's reloaded).  This hook performs the following
        tasks:

        -   Create temporarily stored events.
        -   Generate locals (individual events' namespace).
        -   Load eventfuncs, including user-defined ones.
        -   Re-schedule tasks that aren't set to fire anymore.
        -   Effectively connect the handler to the main script.

        """
        self.ndb.events = {}
        for typeclass, name, variables, help_text, custom_call, custom_add in EVENTS:
            self.add_event(typeclass, name, variables, help_text, custom_call, custom_add)

        # Generate locals
        self.ndb.current_locals = {}
        self.ndb.fresh_locals = {}
        addresses = ["evennia.contrib.base_systems.ingame_python.eventfuncs"]
        addresses.extend(getattr(settings, "EVENTFUNCS_LOCATIONS", ["world.eventfuncs"]))
        for address in addresses:
            if pypath_to_realpath(address):
                self.ndb.fresh_locals.update(all_from_module(address))

        # Restart the delayed tasks
        for task_id, definition in tuple(self.db.tasks.items()):
            future, obj, event_name, locals = definition
            seconds = (future - now).total_seconds()
            if seconds < 0:
                seconds = 0

            delay(seconds, complete_task, task_id)

        # Place the script in the CallbackHandler
        from evennia.contrib.base_systems.ingame_python import typeclasses

        CallbackHandler.script = self
        DefaultObject.callbacks = typeclasses.EventObject.callbacks

        # Create the channel if non-existent
        try:
            self.ndb.channel = ChannelDB.objects.get(db_key="everror")
        except ChannelDB.DoesNotExist:
            self.ndb.channel = create_channel(
                "everror",
                desc="Event errors",
                locks="control:false();listen:perm(Builders);send:false()",
            )

    def get_events(self, obj):
        """
        Return a dictionary of events on this object.

        Args:
            obj (Object or typeclass): the connected object or a general typeclass.

        Returns:
            A dictionary of the object's events.

        Notes:
            Events would define what the object can have as
            callbacks.  Note, however, that chained callbacks will not
            appear in events and are handled separately.

            You can also request the events of a typeclass, not a
            connected object.  This is useful to get the global list
            of events for a typeclass that has no object yet.

        """
        events = {}
        all_events = self.ndb.events
        classes = Queue()
        if isinstance(obj, type):
            classes.put(obj)
        else:
            classes.put(type(obj))

        invalid = []
        while not classes.empty():
            typeclass = classes.get()
            typeclass_name = typeclass.__module__ + "." + typeclass.__name__
            for key, etype in all_events.get(typeclass_name, {}).items():
                if key in invalid:
                    continue
                if etype[0] is None:  # Invalidate
                    invalid.append(key)
                    continue
                if key not in events:
                    events[key] = etype

            # Look for the parent classes
            for parent in typeclass.__bases__:
                classes.put(parent)

        return events

        """
        Return the variable defined in the locals.

        This can be very useful to check the value of a variable that can be modified in an event, and whose value will be used in code.  This system allows additional customization.

        Args:
            variable_name (str): the name of the variable to return.

        Returns:
            The variable if found in the locals.
            None if not found in the locals.

        Note:
            This will return the variable from the current locals.
            Keep in mind that locals are shared between events.  As
            every event is called one by one, this doesn't pose
            additional problems if you get the variable right after
            an event has been executed.  If, however, you differ,
            there's no guarantee the variable will be here or will
            mean the same thing.

        """
        return self.ndb.current_locals.get(variable_name)

    def get_callbacks(self, obj):
        """
        Return a dictionary of the object's callbacks.

        Args:
            obj (Object): the connected objects.

        Returns:
            A dictionary of the object's callbacks.

        Note:
            This method can be useful to override in some contexts,
            when several objects would share callbacks.

        """
        obj_callbacks = self.db.callbacks.get(obj, {})
        for callback_name, callback_list in obj_callbacks.items():
            new_list = []
            for i, callback in enumerate(callback_list):
                callback = dict(callback)
                callback["obj"] = obj
                callback["name"] = callback_name
                callback["number"] = i
                new_list.append(callback)

            if new_list:
                callbacks[callback_name] = new_list

    def add_callback(self, obj, callback_name, code, author=None, valid=False, parameters=""):
        """
        Add the specified callback.

        Args:
            obj (Object): the Evennia typeclassed object to be extended.

        Note:
            This method doesn't check that the callback type exists.

        """
        if not obj_callbacks:
            self.db.callbacks[obj] = {}
            obj_callbacks = self.db.callbacks[obj]

        callbacks = obj_callbacks.get(callback_name, [])
            obj_callbacks[callback_name] = []
            callbacks = obj_callbacks[callback_name]

        # Add the callback in the list
        callbacks.append(
            {
                "created_on": datetime.now(),
                "author": author,
                "valid": valid,
                "code": code,
                "parameters": parameters,
            }
        )

        # If not valid, set it in 'to_valid'
            self.db.to_valid.append((obj, callback_name, len(callbacks) - 1))

        # Call the custom_add if needed
        custom_add = self.get_events(obj).get(callback_name, [None, None, None, None])[3]
        if custom_add:
            custom_add(obj, callback_name, len(callbacks) - 1, parameters)

        # Build the definition to return (a dictionary)
        definition = dict(callbacks[-1])
        definition["obj"] = obj
        definition["name"] = callback_name
        definition["number"] = len(callbacks) - 1
        return definition

    def edit_callback(self, obj, callback_name, number, code, author=None, valid=False):
        """
        Edit the specified callback.

        Args:
            obj (Object): the Evennia typeclassed object to be edited.

        Raises:

        Note:

        """

        # If locked, don't edit it
        if (obj, callback_name, number) in self.db.locked:
            raise RuntimeError("this callback is locked.")

        # Edit the callback
        callbacks[number].update(
            {"updated_on": datetime.now(), "updated_by": author, "valid": valid, "code": code}
        )

        if not valid and (obj, callback_name, number) not in self.db.to_valid:
            self.db.to_valid.append((obj, callback_name, number))
        elif valid and (obj, callback_name, number) in self.db.to_valid:
            self.db.to_valid.remove((obj, callback_name, number))

        definition = dict(callbacks[number])
        definition["number"] = number

    def del_callback(self, obj, callback_name, number):
        """
        Delete the specified callback.

        Args:
            obj (Object): the typeclassed object containing the callback.

        Raises:

        """

        # Delete the callback itself
        try:
            code = callbacks[number]["code"]
        except IndexError:
            return
        else:
            logger.log_info(
                "Deleting callback {} {} of {}:\n{}".format(callback_name, number, obj, code)
            )
            del callbacks[number]

        # Change IDs of callbacks to be validated
        i = 0
        while i < len(self.db.to_valid):
            t_obj, t_callback_name, t_number = self.db.to_valid[i]
            if obj is t_obj and callback_name == t_callback_name:
                if t_number == number:
                    # Strictly equal, delete the callback
                    del self.db.to_valid[i]
                    i -= 1
                elif t_number > number:
                    # Change the ID for this callback
                    self.db.to_valid.insert(i, (t_obj, t_callback_name, t_number - 1))
                    del self.db.to_valid[i + 1]
            i += 1

        # Update locked callback
        for i, line in enumerate(self.db.locked):
            t_obj, t_callback_name, t_number = line
                if number < t_number:
                    self.db.locked[i] = (t_obj, t_callback_name, t_number - 1)

        # Delete time-related callbacks associated with this object
        for script in obj.scripts.all():
            if isinstance(script, TimecallbackScript):
                if script.obj is obj and script.db.callback_name == callback_name:
                    if script.db.number == number:
                        script.stop()
                    elif script.db.number > number:
                        script.db.number -= 1

    def accept_callback(self, obj, callback_name, number):
        """
        Valid a callback.

        Args:
            obj (Object): the object containing the callback.
            number (int): the number of the callback.

        """

        # Accept and connect the callback
        callbacks[number].update({"valid": True})
        if (obj, callback_name, number) in self.db.to_valid:

    def call(self, obj, callback_name, *args, **kwargs):
        """
        Call the connected callbacks.

        Args:
            obj (Object): the Evennia typeclassed object.

        Returns:
            False otherwise.

        """
        # First, look for the callback type corresponding to this name
        number = kwargs.get("number")
        parameters = kwargs.get("parameters")
        locals = kwargs.get("locals")

        # Errors should not pass silently
        allowed = ("number", "parameters", "locals")
        if any(k for k in kwargs if k not in allowed):
                "Unknown keyword arguments were specified " "to call callbacks: {}".format(kwargs)
            )

        event = self.get_events(obj).get(callback_name)
        if locals is None and not event:
            logger.log_err(
                "The callback {} for the object {} (typeclass "
                "{}) can't be found".format(callback_name, obj, type(obj))
            )

        # Prepare the locals if necessary
        if locals is None:
            locals = self.ndb.fresh_locals.copy()
            for i, variable in enumerate(event[0]):
                try:
                    locals[variable] = args[i]
                    logger.log_trace(
                        "callback {} of {} ({}): need variable "
                        "{} in position {}".format(callback_name, obj, type(obj), variable, i)
                    )
                    return False
        else:
            locals = {key: value for key, value in locals.items()}

        callbacks = self.get_callbacks(obj).get(callback_name, [])
        if event:
            custom_call = event[2]
            if custom_call:
                callbacks = custom_call(callbacks, parameters)

        # Now execute all the valid callbacks linked at this address
        self.ndb.current_locals = locals
        for i, callback in enumerate(callbacks):
            if not callback["valid"]:
                continue

            if number is not None and callback["number"] != number:
                continue

            try:
                exec(callback["code"], locals, locals)
            except InterruptEvent:
                etype, evalue, tb = sys.exc_info()
                trace = traceback.format_exception(etype, evalue, tb)
                self.handle_error(callback, trace)

    def handle_error(self, callback, trace):
        """
        Handle an error in a callback.

        Args:
            callback (dict): the callback representation.
            trace (list): the traceback containing the exception.

        Notes:
            This method can be useful to override to change the default
            handling of errors.  By default, the error message is sent to
            the character who last updated the callback, if connected.
            If not, display to the everror channel.

        """
        callback_name = callback["name"]
        number = callback["number"]
        obj = callback["obj"]
        oid = obj.id
        logger.log_err(
            "An error occurred during the callback {} of "
            "{} (#{}), number {}\n{}".format(callback_name, obj, oid, number + 1, "\n".join(trace))
        )

        # Create the error message
        line = "|runknown|n"
        lineno = "|runknown|n"
        for error in trace:
            if error.startswith('  File "<string>", line '):
                res = RE_LINE_ERROR.search(error)
                if res:
                    lineno = int(res.group(1))

                    # Try to extract the line
                    try:
                        line = raw(callback["code"].splitlines()[lineno - 1])
                    except IndexError:
                        continue
                    else:
                        break

        exc = raw(trace[-1].strip("\n").splitlines()[-1])
        err_msg = "Error in {} of {} (#{})[{}], line {}:" " {}\n{}".format(
            callback_name, obj, oid, number + 1, lineno, line, exc
        )

        # Inform the last updater if connected
        updater = callback.get("updated_by")
        if updater is None:
            updater = callback["created_by"]

        if updater and updater.sessions.all():
            updater.msg(err_msg)
        else:
            err_msg = "Error in {} of {} (#{})[{}], line {}:" " {}\n          {}".format(
                callback_name, obj, oid, number + 1, lineno, line, exc
            )
            self.ndb.channel.msg(err_msg)

    def add_event(self, typeclass, name, variables, help_text, custom_call, custom_add):
        """
        Add a new event for a defined typeclass.

        Args:
            typeclass (str): the path leading to the typeclass.
            name (str): the name of the event to add.
            variables (list of str): list of variable names for this event.
            help_text (str): the long help text of the event.
            custom_call (callable or None): the function to be called
                    when the event fires.
            custom_add (callable or None): the function to be called when
                    a callback is added.

        """
        if typeclass not in self.ndb.events:
            self.ndb.events[typeclass] = {}

        events = self.ndb.events[typeclass]
        if name not in events:
            events[name] = (variables, help_text, custom_call, custom_add)

    def set_task(self, seconds, obj, callback_name):
        """
        Set and schedule a task to run.

        Args:
            seconds (int, float): the delay in seconds from now.
            obj (Object): the typecalssed object connected to the event.
            callback_name (str): the callback's name.

        Notes:
            This method allows to schedule a "persistent" task.
            'utils.delay' is called, but a copy of the task is kept in
            the event handler, and when the script restarts (after reload),
            the differed delay is called again.
            The dictionary of locals is frozen and will be available
            again when the task runs.  This feature, however, is limited
            by the database: all data cannot be saved.  Lambda functions,
            class methods, objects inside an instance and so on will
            not be kept in the locals dictionary.

        """
        delta = timedelta(seconds=seconds)

        # Choose a free task_id
        used_ids = list(self.db.tasks.keys())
        task_id = 1
        while task_id in used_ids:
            task_id += 1

        # Collect and freeze current locals
        locals = {}
        for key, value in self.ndb.current_locals.items():
            try:
                dbserialize(value)
                continue
            else:
                locals[key] = value

        self.db.tasks[task_id] = (now + delta, obj, callback_name, locals)
        delay(seconds, complete_task, task_id)

# Script to call time-related events
class TimeEventScript(DefaultScript):

        # Script attributes
        self.db.time_format = None
        self.db.event_name = "time"
        self.db.number = None

        """
        Call the event and reset interval.

        It is necessary to restart the script to reset its interval
        only twice after a reload.  When the script has undergone
        down time, there's usually a slight shift in game time.  Once
        the script restarts once, it will set the average time it
        needs for all its future intervals and should not need to be
        restarted.  In short, a script that is created shouldn't need
        to restart more than once, and a script that is reloaded should
        restart only twice.

        """
        if self.db.time_format:
            # If the 'usual' time is set, use it
            seconds = self.ndb.usual
            if seconds is None:
                seconds, usual, details = get_next_wait(self.db.time_format)
                self.ndb.usual = usual

            if self.interval != seconds:
                self.restart(interval=seconds)

        if self.db.event_name and self.db.number is not None:
            obj = self.obj
            if not obj.callbacks:
                return

            event_name = self.db.event_name
            number = self.db.number
            obj.callbacks.call(event_name, obj, number=number)

# Functions to manipulate tasks
def complete_task(task_id):
    """
    Mark the task in the event handler as complete.

    Args:
        task_id (int): the task ID.

    Note:
        This function should be called automatically for individual tasks.

    """
    try:
        script = ScriptDB.objects.get(db_key="event_handler")
    except ScriptDB.DoesNotExist:
        logger.log_trace("Can't get the event handler.")
        return

    if task_id not in script.db.tasks:
        logger.log_err("The task #{} was scheduled, but it cannot be " "found".format(task_id))
        return

    delta, obj, callback_name, locals = script.db.tasks.pop(task_id)
    script.call(obj, callback_name, locals=locals)

"""
Module containing the test cases for the in-game Python system.
"""

from mock import Mock

from evennia import ScriptDB
from evennia.objects.objects import ExitCommand
from evennia.utils import ansi, utils
from evennia.utils.create import create_object, create_script

from .callbackhandler import CallbackHandler
from .commands import CmdCallback

# Force settings
settings.EVENTS_CALENDAR = "standard"

OLD_EVENTS = {}

class TestEventHandler(BaseEvenniaTest):

    """
    Test cases of the event handler to add, edit or delete events.
    """

        """Create the event handler."""
        self.handler = create_script(
            "evennia.contrib.base_systems.ingame_python.scripts.EventHandler"
        )

        # Copy old events if necessary
        if OLD_EVENTS:
            self.handler.ndb.events = dict(OLD_EVENTS)

        # Alter typeclasses
        self.char1.swap_typeclass(
            "evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
        )
        self.char2.swap_typeclass(
        )
        self.room1.swap_typeclass(
            "evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
        )
        self.room2.swap_typeclass(
        )
        self.exit.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventExit")

        """Stop the event handler."""
        OLD_EVENTS.clear()
        OLD_EVENTS.update(self.handler.ndb.events)
        self.handler.delete()
        CallbackHandler.script = None

    def test_start(self):
        """Simply make sure the handler runs with proper initial values."""
        self.assertEqual(self.handler.db.callbacks, {})
        self.assertEqual(self.handler.db.to_valid, [])
        self.assertEqual(self.handler.db.locked, [])
        self.assertEqual(self.handler.db.tasks, {})
        self.assertIsNotNone(self.handler.ndb.events)

    def test_add_validation(self):
        """Add a callback while needing validation."""
        author = self.char1
        self.handler.add_callback(
            self.room1, "dummy", "character.db.strength = 40", author=author, valid=False
        )
        callback = self.handler.get_callbacks(self.room1).get("dummy")
        callback = callback[0]
        self.assertIsNotNone(callback)
        self.assertEqual(callback["author"], author)
        self.assertEqual(callback["valid"], False)

        # Since this callback is not valid, it should appear in 'to_valid'
        self.assertIn((self.room1, "dummy", 0), self.handler.db.to_valid)

        # Run this dummy callback (shouldn't do anything)
        self.char1.db.strength = 10
        locals = {"character": self.char1}
        self.assertTrue(self.handler.call(self.room1, "dummy", locals=locals))
        self.assertEqual(self.char1.db.strength, 10)

    def test_edit(self):
        """Test editing a callback."""
            self.room1, "dummy", "character.db.strength = 60", author=author, valid=True
        )

        # Edit it right away
        self.handler.edit_callback(
            self.room1, "dummy", 0, "character.db.strength = 65", author=self.char2, valid=True
        )

        # Check that the callback was written
        self.assertEqual(callback["valid"], True)
        self.assertEqual(callback["updated_by"], self.char2)

        # Run this dummy callback
        self.assertEqual(self.char1.db.strength, 65)

    def test_edit_validation(self):
        """Edit a callback when validation isn't automatic."""
            self.room1, "dummy", "character.db.strength = 70", author=author, valid=True
        )

            self.room1, "dummy", 0, "character.db.strength = 80", author=self.char2, valid=False
        )

    def test_del(self):
        """Try to delete a callback."""
        # Add 3 callbacks
            self.room1, "dummy", "character.db.strength = 5", author=self.char1, valid=True
        )
            self.room1, "dummy", "character.db.strength = 8", author=self.char2, valid=False
        )
            self.room1, "dummy", "character.db.strength = 9", author=self.char1, valid=True
        )

        # Note that the second callback isn't valid
        self.assertIn((self.room1, "dummy", 1), self.handler.db.to_valid)

        # Lock the third callback
        self.handler.db.locked.append((self.room1, "dummy", 2))

        # Delete the first callback
        self.handler.del_callback(self.room1, "dummy", 0)

        # The callback #1 that was to valid should be #0 now
        self.assertNotIn((self.room1, "dummy", 1), self.handler.db.to_valid)

        # The lock has been updated too
        self.assertIn((self.room1, "dummy", 1), self.handler.db.locked)
        self.assertNotIn((self.room1, "dummy", 2), self.handler.db.locked)

        # Now delete the first (not valid) callback
        self.assertIn((self.room1, "dummy", 0), self.handler.db.locked)
        self.assertNotIn((self.room1, "dummy", 1), self.handler.db.locked)

        # Call the remaining callback
        self.assertEqual(self.char1.db.strength, 9)

    def test_accept(self):
        """Accept an callback."""
        # Add 2 callbacks
        )
        )

        # Accept the second callback
        self.handler.accept_callback(self.room1, "dummy", 1)
        callback = callback[1]

        # Call the dummy callback
        self.assertEqual(self.char1.db.strength, 8)

        """Test to call amore complex callback."""
        self.char1.key = "one"
        self.char2.key = "two"

        # Add an callback
        code = dedent(
            """
            if character.key == "one":
                character.db.health = 50
            else:
                character.db.health = 0
        """.strip(
                "\n"
            )
        )
        self.handler.add_callback(self.room1, "dummy", code, author=self.char1, valid=True)

        self.assertTrue(self.handler.call(self.room1, "dummy", locals={"character": self.char1}))
        self.assertEqual(self.char1.db.health, 50)
        self.assertTrue(self.handler.call(self.room1, "dummy", locals={"character": self.char2}))
        self.assertEqual(self.char2.db.health, 0)

    def test_handler(self):
        """Test the object handler."""
        self.assertIsNotNone(self.char1.callbacks)

        callback = self.room1.callbacks.add("dummy", "pass", author=self.char1, valid=True)
        self.assertEqual(callback.obj, self.room1)
        self.assertEqual(callback.name, "dummy")
        self.assertEqual(callback.code, "pass")
        self.assertEqual(callback.author, self.char1)
        self.assertEqual(callback.valid, True)
        self.assertIn([callback], list(self.room1.callbacks.all().values()))

        # Edit this very callback
        new = self.room1.callbacks.edit(
            "dummy", 0, "character.db.say = True", author=self.char1, valid=True
        )
        self.assertIn([new], list(self.room1.callbacks.all().values()))
        self.assertNotIn([callback], list(self.room1.callbacks.all().values()))

        # Try to call this callback
        self.assertTrue(self.room1.callbacks.call("dummy", locals={"character": self.char2}))
        self.assertTrue(self.char2.db.say)

        self.room1.callbacks.remove("dummy", 0)
        self.assertEqual(self.room1.callbacks.all(), {})

class TestCmdCallback(BaseEvenniaCommandTest):

    """Test the @callback command."""

        """Create the callback handler."""
        )

        )
        )
        )
        )

        """Stop the callback handler."""
        for script in ScriptDB.objects.filter(
            db_typeclass_path="evennia.contrib.base_systems.ingame_python.scripts.TimeEventScript"
        ):
            script.delete()

    def test_list(self):
        """Test listing callbacks with different rights."""
        table = self.call(CmdCallback(), "out")
        lines = table.splitlines()[3:-1]
        self.assertNotEqual(lines, [])

        # Check that the second column only contains 0 (0) (no callback yet)
        for line in lines:
            cols = line.split("|")
            self.assertIn(cols[2].strip(), ("0 (0)", ""))

        # Add some callback
        self.handler.add_callback(self.exit, "traverse", "pass", author=self.char1, valid=True)

        # Try to obtain more details on a specific callback on exit
        table = self.call(CmdCallback(), "out = traverse")
        self.assertEqual(len(lines), 1)
        line = lines[0]
        cols = line.split("|")
        self.assertIn(cols[1].strip(), ("1", ""))
        self.assertIn(cols[2].strip(), (str(self.char1), ""))
        self.assertIn(cols[-1].strip(), ("Yes", "No", ""))

        # Run the same command with char2
        # char2 shouldn't see the last column (Valid)
        table = self.call(CmdCallback(), "out = traverse", caller=self.char2)
        self.assertEqual(cols[1].strip(), "1")
        self.assertNotIn(cols[-1].strip(), ("Yes", "No"))

        # In any case, display the callback
        # The last line should be "pass" (the callback code)
        details = self.call(CmdCallback(), "out = traverse 1")
        self.assertEqual(details.splitlines()[-1], "pass")

    def test_add(self):
        """Test to add an callback."""
        self.call(CmdCallback(), "/add out = traverse")
        editor = self.char1.ndb._eveditor
        self.assertIsNotNone(editor)

        editor.update_buffer(
            dedent(
                """
                character.msg("You can pass.")
            else:
                character.msg("You can't pass.")
                deny()
                    "\n"
                )
            )
        )
        editor.save_buffer()
        editor.quit()
        callback = self.exit.callbacks.get("traverse")[0]
        self.assertTrue(len(callback.code) > 0)

        # We're going to try the same thing but with char2
        # char2 being a player for our test, the callback won't be validated.
        self.call(CmdCallback(), "/add out = traverse", caller=self.char2)
        editor = self.char2.ndb._eveditor

            dedent(
                """
            character.msg("No way.")
                    "\n"
                )
            )
        )
        callback = self.exit.callbacks.get("traverse")[1]
        self.assertEqual(callback.author, self.char2)
        self.assertEqual(callback.valid, False)

        """Add and remove an callback."""

        # Try to delete the callback
        # char2 shouldn't be allowed to do so (that's not HIS callback)
        self.call(CmdCallback(), "/del out = traverse 1", caller=self.char2)
        self.assertTrue(len(self.handler.get_callbacks(self.exit).get("traverse", [])) == 1)

        # Now, char1 should be allowed to delete it
        self.call(CmdCallback(), "/del out = traverse 1")
        self.assertTrue(len(self.handler.get_callbacks(self.exit).get("traverse", [])) == 0)

        """Test the lock of multiple editing."""
        self.call(CmdCallback(), "/add here = time 8:00", caller=self.char2)
        self.assertIsNotNone(self.char2.ndb._eveditor)

        # Now ask char1 to edit
        line = self.call(CmdCallback(), "/edit here = time 1")
        self.assertIsNone(self.char1.ndb._eveditor)

        # Try to delete this callback while char2 is editing it
        line = self.call(CmdCallback(), "/del here = time 1")

            dedent(
                """
            room.msg_contents("It's 8 PM, everybody up!")
                    "\n"
                )
            )
        )
        callback = self.room1.callbacks.get("time")[0]

        # chars shouldn't be allowed to the callback
        self.call(CmdCallback(), "/accept here = time 1", caller=self.char2)

        # char1 will accept the callback
        self.call(CmdCallback(), "/accept here = time 1")

class TestDefaultCallbacks(BaseEvenniaCommandTest):

    """Test the default callbacks."""

        )

        )
        )
        )
        )

    def test_exit(self):
        """Test the callbacks of an exit."""
        self.char1.key = "char1"
            """
            if character.key == "char1":
                character.msg("You can leave.")
            else:
                character.msg("You cannot leave.")
                deny()
                "\n"
            )
        )
        # Enforce self.exit.destination since swapping typeclass lose it
        self.exit.destination = self.room2

        # Try the can_traverse callback
        self.handler.add_callback(self.exit, "can_traverse", code, author=self.char1, valid=True)

        # Have char1 move through the exit
        self.call(ExitCommand(), "", "You can leave.", obj=self.exit)
        self.assertIs(self.char1.location, self.room2)

        # Have char2 move through this exit
        self.call(ExitCommand(), "", "You cannot leave.", obj=self.exit, caller=self.char2)
        self.assertIs(self.char2.location, self.room1)

        # Try the traverse callback
        self.handler.del_callback(self.exit, "can_traverse", 0)
            self.exit, "traverse", "character.msg('Fine!')", author=self.char1, valid=True
        )

        # Have char2 move through the exit
        self.call(ExitCommand(), "", obj=self.exit, caller=self.char2)
        self.assertIs(self.char2.location, self.room2)
        self.handler.del_callback(self.exit, "traverse", 0)

        # Move char1 and char2 back
        self.char1.location = self.room1
        self.char2.location = self.room1

        # Test msg_arrive and msg_leave
        code = 'message = "{character} goes out."'
        self.handler.add_callback(self.exit, "msg_leave", code, author=self.char1, valid=True)

        old_msg = self.char2.msg
        try:
            self.char2.msg = Mock()
            self.call(ExitCommand(), "", obj=self.exit)
            stored_msg = [
                args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
                for name, args, kwargs in self.char2.msg.mock_calls
            ]
            # Get the first element of a tuple if msg received a tuple instead of a string
            stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
            returned_msg = ansi.parse_ansi("\n".join(stored_msg), strip_ansi=True)
            self.assertEqual(returned_msg, "char1 goes out.")
        finally:
            self.char2.msg = old_msg

        # Create a return exit
        back = create_object(
            "evennia.objects.objects.DefaultExit",
            key="in",
            location=self.room2,
            destination=self.room1,
        )
        code = 'message = "{character} goes in."'
        self.handler.add_callback(self.exit, "msg_arrive", code, author=self.char1, valid=True)

        try:
            self.call(ExitCommand(), "", obj=back)
            ]
            self.assertEqual(returned_msg, "char1 goes in.")
        finally:

"""
Functions to extend the event system.

These functions are to be used by developers to customize events and callbacks.

"""

from evennia import ScriptDB, logger
from evennia.contrib.base_systems.custom_gametime import UNITS, gametime_to_realtime
from evennia.contrib.base_systems.custom_gametime import (
    real_seconds_until as custom_rsu,
)
from evennia.utils.gametime import real_seconds_until as standard_rsu

# Temporary storage for events waiting for the script to be started
EVENTS = []

def get_event_handler():
    """Return the event handler or None."""
    try:
        script = None

def register_events(path_or_typeclass):
    """
    Register the events in this typeclass.

    Args:
        path_or_typeclass (str or type): the Python path leading to the
                class containing events, or the class itself.

    Returns:
        The typeclass itself.

    Notes:
        This function will read events from the `_events` class variable
        defined in the typeclass given in parameters.  It will add
        the events, either to the script if it exists, or to some
        temporary storage, waiting for the script to be initialized.

    """
    if isinstance(path_or_typeclass, str):
        typeclass = class_from_module(path_or_typeclass)
    else:
        typeclass = path_or_typeclass

    typeclass_name = typeclass.__module__ + "." + typeclass.__name__
    try:
        storage = ScriptDB.objects.get(db_key="event_handler")
        assert storage.ndb.events is not None
    except (ScriptDB.DoesNotExist, AssertionError):
        storage = EVENTS

    # If the script is started, add the event directly.
    # Otherwise, add it to the temporary storage.
    for name, tup in getattr(typeclass, "_events", {}).items():
        if len(tup) == 4:
            variables, help_text, custom_call, custom_add = tup
        elif len(tup) == 3:
            variables, help_text, custom_call = tup
            custom_add = None
        elif len(tup) == 2:
            variables, help_text = tup
            custom_call = None
        else:
            variables = help_text = custom_call = custom_add = None

        if isinstance(storage, list):
            storage.append((typeclass_name, name, variables, help_text, custom_call, custom_add))
        else:
            storage.add_event(typeclass_name, name, variables, help_text, custom_call, custom_add)

    return typeclass

# Custom callbacks for specific event types

def get_next_wait(format):
    """
    Get the length of time in seconds before format.

    Args:
        format (str): a time format matching the set calendar.

    Returns:
        until (int or float): the number of seconds until the event.
        usual (int or float): the usual number of seconds between events.
        format (str): a string format representing the time.

    Notes:
        The time format could be something like "2018-01-08 12:00".  The
        number of units set in the calendar affects the way seconds are
        calculated.

    """
    calendar = getattr(settings, "EVENTS_CALENDAR", None)
    if calendar is None:
            "A time-related event has been set whereas "
            "the gametime calendar has not been set in the settings."
        )
        return
    elif calendar == "standard":
        rsu = standard_rsu
        units = ["min", "hour", "day", "month", "year"]
    elif calendar == "custom":
        rsu = custom_rsu
        back = dict([(value, name) for name, value in UNITS.items()])
        sorted_units = sorted(back.items())
        del sorted_units[0]
        units = [n for v, n in sorted_units]

    params = {}
    for delimiter in ("-", ":"):
        format = format.replace(delimiter, " ")

    pieces = list(reversed(format.split()))
    details = []
    i = 0
    for uname in units:
        try:
            piece = pieces[i]
            break

        if not piece.isdigit():
            logger.log_trace(
                "The time specified '{}' in {} isn't " "a valid number".format(piece, format)
            )
            return

        # Convert the piece to int
        piece = int(piece)
        params[uname] = piece
        details.append("{}={}".format(uname, piece))
        if i < len(units):
            next_unit = units[i + 1]
        else:
            next_unit = None
        i += 1

    params["sec"] = 0
    details = " ".join(details)
    until = rsu(**params)
    usual = -1
    if next_unit:
        kwargs = {next_unit: 1}
        usual = gametime_to_realtime(**kwargs)
    return until, usual, details

def time_event(obj, event_name, number, parameters):
    """
    Create a time-related event.

    Args:
        obj (Object): the object on which sits the event.
        event_name (str): the event's name.
        number (int): the number of the event.
        parameters (str): the parameter of the event.

    """
    seconds, usual, key = get_next_wait(parameters)
        "evennia.contrib.base_systems.ingame_python.scripts.TimeEventScript",
        obj=obj,
    )
    script.key = key
    script.desc = "event on {}".format(key)
    script.db.time_format = parameters
    script.db.number = number
    script.ndb.usual = usual

def keyword_event(callbacks, parameters):
    """
    Custom call for events with keywords (like push, or pull, or turn...).

    Args:
        callbacks (list of dict): the list of callbacks to be called.
        parameters (str): the actual parameters entered to trigger the callback.

    Returns:
        A list containing the callback dictionaries to be called.

    Notes:
        This function should be imported and added as a custom_call
        parameter to add the event when the event supports keywords
        as parameters.  Keywords in parameters are one or more words
        separated by a comma.  For instance, a 'push 1, one' callback can
        be set to trigger when the player 'push 1' or 'push one'.

    """
    key = parameters.strip().lower()
    to_call = []
    for callback in callbacks:
        keys = callback["parameters"]
        if not keys or key in [p.strip().lower() for p in keys.split(",")]:
            to_call.append(callback)

    return to_call

def phrase_event(callbacks, parameters):
    """
    Custom call for events with keywords in sentences (like say or whisper).

    Args:

    Returns:

    Notes:
        in phrases as parameters.  Keywords in parameters are one or more
        words separated by a comma.  For instance, a 'say yes, okay' callback
        can be set to trigger when the player says something containing
        either "yes" or "okay" (maybe 'say I don't like it, but okay').

    """
    phrase = parameters.strip().lower()
    # Remove punctuation marks
    punctuations = ',.";?!'
    for p in punctuations:
        phrase = phrase.replace(p, " ")
    words = phrase.split()
    words = [w.strip("' ") for w in words if w.strip("' ")]
        if not keys or any(key.strip().lower() in words for key in keys.split(",")):

class InterruptEvent(RuntimeError):

    """
    Interrupt the current event.

    You shouldn't have to use this exception directly, probably use the
    `deny()` function that handles it instead.

    """

    pass

"""
Legacy Comms-commands

Griatch 2021

In Evennia 1.0, the old Channel commands (originally inspired by MUX) were
replaced by the single `channel` command that performs all these function.
That command is still required to talk on channels. This contrib (extracted
from Evennia 0.9.5) reuses the channel-management of the base Channel command
but breaks out its functionality into separate Commands with MUX-familiar names.

- `allcom` - `channel/all` and `channel`
- `addcom` - `channel/alias`, `channel/sub` and `channel/unmute`
- `delcom` - `channel/unalias`, `alias/unsub` and `channel/mute`
- `cboot` - `channel/boot` (`channel/ban` and `/unban` not supported)
- `cwho` - `channel/who`
- `ccreate` - `channel/create`
- `cdestroy` - `channel/destroy`
- `clock` - `channel/lock`
- `cdesc` - `channel/desc`

Installation:

- Import the `CmdSetLegacyComms` cmdset from this module into `mygame/commands/default_cmdsets.py`
- Add it to the CharacterCmdSet's `at_cmdset_creation` method.
- Reload the server.

Example:

```python
# in mygame/commands/default_cmdsets.py

# ..
from evennia.contrib.base_systems.mux_comms_cmds import CmdSetLegacyComms   # <----

class CharacterCmdSet(default_cmds.CharacterCmdSet):
    # ...
        # ...
        self.add(CmdSetLegacyComms)   # <----
```

"""

from evennia.commands.default.comms import CmdChannel

CHANNEL_DEFAULT_TYPECLASS = settings.BASE_CHANNEL_TYPECLASS

class CmdAddCom(CmdChannel):
    """
    Add a channel alias and/or subscribe to a channel

    Usage:
       addcom [alias=] <channel>

    Joins a given channel. If alias is given, this will allow you to
    refer to the channel by this alias rather than the full channel
    name. Subsequent calls of this command can be used to add multiple
    aliases to an already joined channel.
    """

    key = "addcom"
    aliases = ["aliaschan", "chanalias"]
    locks = "cmd:not pperm(channel_banned)"

            self.msg("Usage: addcom [alias =] channelname.")
            return

            # rhs holds the channelname
            channelname = self.rhs
            alias = self.lhs
        else:
            channelname = self.args
            alias = None

        channel = self.search_channel(channelname)
        if not channel:
            return

        string = ""
            # we want to connect as well.
                # if this would have returned True, the account is connected
                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

        if channel.unmute(caller):
            self.msg(f"You unmute channel {channel.key}.")
        else:
            self.msg(f"You are already connected to channel {channel.key}.")

        if alias:
            # create a nick and add it to the caller.
            self.msg(f" You can now refer to the channel {channel} with the alias '{alias}'.")
        else:
            string += " No alias added."

class CmdDelCom(CmdChannel):
    """
    remove a channel alias and/or unsubscribe from channel

    Usage:
       delcom <alias or channel>
       delcom/all <channel>

    If the full channel name is given, unsubscribe from the
    channel. If an alias is given, remove the alias but don't
    unsubscribe. If the 'all' switch is used, remove all aliases
    for that channel.
    """

    key = "delcom"
    aliases = ["delaliaschan", "delchanalias"]
    locks = "cmd:not perm(channel_banned)"

        """Implementing the command."""

            self.msg("Usage: delcom <alias or channel>")
            return
        ostring = self.args.lower().strip()

        channel = self.search_channel(ostring)
            return

            self.msg("You are not listening to that channel.")
            return

        if ostring == channel.key.lower():
            # an exact channel name - unsubscribe
            delnicks = "all" in self.switches
            # find all nicks linked to this channel and delete them
            if delnicks:
                aliases = self.get_channel_aliases(channel)
                    self.remove_alias(alias)
                wipednicks = " Eventual aliases were removed." if delnicks else ""
                self.msg(f"You stop listening to channel '{channel.key}'.{wipednicks}")
            else:
            return
        else:
            # we are removing a channel nick
            self.remove_alias(ostring)
            self.msg(f"Any alias '{ostring}' for channel {channel.key} was cleared.")

class CmdAllCom(CmdChannel):
    """
    perform admin operations on all channels

    Usage:
      allcom [on | off | who | destroy]

    Allows the user to universally turn off or on all channels they are on, as
    well as perform a 'who' for all channels they are on. Destroy deletes all
    channels that you control.

    Without argument, works like comlist.
    """

    key = "allcom"
    aliases = []  # important to not inherit parent's aliases
    locks = "cmd: not pperm(channel_banned)"

        """Runs the function"""

            self.msg("\n|wAvailable channels:\n{table}")
            return
            return

        if args == "on":
            # get names of all channels available to listen to
            # and activate them all
            channels = [
                chan
                for chan in CHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
                if chan.access(caller, "listen")
            ]
            for channel in channels:
                self.execute_cmd("addcom %s" % channel.key)
        elif args == "off":
            # get names all subscribed channels and disconnect from them all
            channels = CHANNEL_DEFAULT_TYPECLASS.objects.get_subscriptions(caller)
                self.execute_cmd("delcom %s" % channel.key)
        elif args == "destroy":
            # destroy all channels you control
                chan
                if chan.access(caller, "control")
            ]
                self.execute_cmd("cdestroy %s" % channel.key)
        elif args == "who":
            # run a who, listing the subscribers on visible channels.
            string = "\n|CChannel subscriptions|n"
                chan
            ]
                string += "No channels."
                string += "\n|w%s:|n\n %s" % (channel.key, channel.wholist)
            self.msg(string.strip())
        else:
            # wrong input
            self.msg("Usage: allcom on | off | who | clear")

class CmdCdestroy(CmdChannel):
    """
    destroy a channel you created

    Usage:
      cdestroy <channel>

    Destroys a channel that you control.
    """

    key = "cdestroy"

        """Destroy objects cleanly."""

            self.msg("Usage: cdestroy <channelname>")
            return

        channel = self.search_channel(self.args)

            self.msg("Could not find channel %s." % self.args)
            return
        if not channel.access(caller, "control"):
            self.msg("You are not allowed to do that.")
            return
        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)
            "Channel Deleted: %s (Caller: %s, IP: %s)."
            % (channel_key, caller, self.session.address)
        )

class CmdCBoot(CmdChannel):
    """
    kick an account from a channel you control

    Usage:
       cboot[/quiet] <channel> = <account> [:reason]

    Switch:
       quiet - don't notify the channel

    Kicks an account or object from a channel you control.

    """

    key = "cboot"
    switch_options = ("quiet",)

        """implement the function"""

            string = "Usage: cboot[/quiet] <channel> = <account> [:reason]"
            return

        channel = self.search_channel(self.lhs)
            return

        reason = ""
        if ":" in self.rhs:
            target, reason = self.rhs.rsplit(":", 1)
            is_account = target.strip().startswith("*")
            searchstring = target.lstrip("*")
        else:
            searchstring = self.rhs.lstrip("*")

        target = self.caller.search(searchstring, account=is_account)
        if not target:
            return
        if reason:
            reason = " (reason: %s)" % reason
        if not channel.access(self.caller, "control"):
            string = "You don't control this channel."
            return

        success, err = self.boot_user(target, quiet="quiet" in self.switches)
        if success:
            self.msg(f"Booted {target.key} from {channel.key}")
                "Channel Boot: %s (Channel: %s, Reason: %s, Caller: %s, IP: %s)."
                % (self.caller, channel, reason, self.caller, self.session.address)
            )
        else:
            self.msg(err)

class CmdCWho(CmdChannel):
    """
    show who is listening to a channel

    Usage:
      cwho <channel>

    List who is connected to a given channel you have access to.
    """

    key = "cwho"

        """implement function"""

            string = "Usage: cwho <channel>"
            return

            return
        if not channel.access(self.caller, "listen"):
            string = "You can't access this channel."
            return
        string = "\n|CChannel subscriptions|n"
        string += "\n|w%s:|n\n  %s" % (channel.key, channel.wholist)

class CmdChannelCreate(CmdChannel):
    """
    create a new channel

    Usage:
     ccreate <new channel>[;alias;alias...] = description

    Creates a new channel owned by you.
    """

    key = "ccreate"
    aliases = "channelcreate"
    locks = "cmd:not pperm(channel_banned) and pperm(Player)"

            self.msg("Usage ccreate <channelname>[;alias;alias..] = description")
            return

        description = ""

            description = self.rhs
        lhs = self.lhs
        channame = lhs
        aliases = None
        if ";" in lhs:
            channame, aliases = lhs.split(";", 1)
            aliases = [alias.strip().lower() for alias in aliases.split(";")]

        new_chan, err = self.create_channel(channame, description, aliases=aliases)
        if new_chan:
            self.msg(f"Created channel {new_chan.key} and connected to it.")
        else:

class CmdClock(CmdChannel):
    """
    change channel locks of a channel you control

    Usage:
      clock <channel> [= <lockstring>]

    Changes the lock access restrictions of a channel. If no
    lockstring was given, view the current lock definitions.
    """

    key = "clock"
    aliases = ["clock"]
    locks = "cmd:not pperm(channel_banned) and perm(Admin)"

        """run the function"""

            string = "Usage: clock channel [= lockstring]"
            return

            return

            # no =, so just view the current locks
            self.msg(f"Current locks on {channel.key}\n{channel.locks}")
            return
        # we want to add/change a lock.
            return
        # Try to add the lock
        success, err = self.set_lock(channel, self.rhs)
            self.msg(f"Lock(s) applied. Current locks on {channel.key}:\n{channel.locks}")
        else:

class CmdCdesc(CmdChannel):
    """
    describe a channel you control

    Usage:
      cdesc <channel> = <description>

    Changes the description of the channel as shown in
    channel lists.

    """

    key = "cdesc"

            self.msg("Usage: cdesc <channel> = <description>")
            return
            return
            self.msg("You cannot admin this channel.")
            return
        self.set_desc(channel, self.rhs)
        self.msg(f"Description of channel '{channel.key}' set to '{self.rhs}'.")

class CmdSetLegacyComms(CmdSet):
    def at_cmdset_createion(self):
        self.add(CmdAddCom())
        self.add(CmdAllCom())
        self.add(CmdDelCom())
        self.add(CmdCdestroy())
        self.add(CmdCBoot())
        self.add(CmdCWho())
        self.add(CmdChannelCreate())
        self.add(CmdClock())
        self.add(CmdCdesc())

"""
Legacy Mux comms tests (extracted from 0.9.5)

"""

from . import mux_comms_cmds as comms

class TestLegacyMuxComms(BaseEvenniaCommandTest):
    """
    Test the legacy comms contrib.
    """

            comms.CmdChannelCreate(),
            "testchan;test=Test Channel",
            "Created channel testchan and connected to it.",
        )

    def test_toggle_com(self):
            comms.CmdAddCom(),
            "tc = testchan",
            "You are already connected to channel testchan.| You can now",
        )
            comms.CmdDelCom(),
            "tc",
            "Any alias 'tc' for channel testchan was cleared.",
        )

    def test_all_com(self):
            comms.CmdAllCom(),
            "",
            "Available channels:",
        )

    def test_clock(self):
            comms.CmdClock(),
            "testchan=send:all()",
            "Lock(s) applied. Current locks on testchan:",
        )

    def test_cdesc(self):
            comms.CmdCdesc(),
            "testchan = Test Channel",
            "Description of channel 'testchan' set to 'Test Channel'.",
        )

    def test_cwho(self):
            comms.CmdCWho(),
            "testchan",
            "Channel subscriptions\ntestchan:\n  TestAccount",
        )

    def test_cboot(self):
        # No one else connected to boot
            comms.CmdCBoot(),
            "",
            "Usage: cboot[/quiet] <channel> = <account> [:reason]",
        )

    def test_cdestroy(self):
            comms.CmdCdestroy(),
            "[testchan] TestAccount: testchan is being destroyed. Make sure to change your aliases."
            "|Channel 'testchan' was destroyed.",
        )

"""
Test of the Unixcommand.

"""

from .unixcommand import UnixCommand

class CmdDummy(UnixCommand):

    """A dummy UnixCommand."""

    key = "dummy"

    def init_parser(self):
        """Fill out options."""
        self.parser.add_argument("nb1", type=int, help="the first number")
        self.parser.add_argument("nb2", type=int, help="the second number")
        self.parser.add_argument("-v", "--verbose", action="store_true")

        nb1 = self.opts.nb1
        nb2 = self.opts.nb2
        result = nb1 * nb2
        verbose = self.opts.verbose
        if verbose:
            self.msg("{} times {} is {}".format(nb1, nb2, result))
        else:
            self.msg("{} * {} = {}".format(nb1, nb2, result))

class TestUnixCommand(BaseEvenniaCommandTest):
    def test_success(self):
        """See the command parsing succeed."""
        self.call(CmdDummy(), "5 10", "5 * 10 = 50")
        self.call(CmdDummy(), "5 10 -v", "5 times 10 is 50")

    def test_failure(self):
        """If not provided with the right info, should fail."""
        ret = self.call(CmdDummy(), "5")
        lines = ret.splitlines()
        self.assertTrue(any(lin.startswith("usage:") for lin in lines))
        self.assertTrue(any(lin.startswith("dummy: error:") for lin in lines))

        # If we specify an incorrect number as parameter
        ret = self.call(CmdDummy(), "five ten")

"""
Unix-like Command style parent

Evennia contribution, Vincent Le Geoff 2017

This module contains a command class that allows for unix-style command syntax in-game, using
--options, positional arguments and stuff like -n 10 etc similarly to a unix command. It might not
the best syntax for the average player but can be really useful for builders when they need to have
a single command do many things with many options. It uses the ArgumentParser from Python's standard
library under the hood.

To use, inherit `UnixCommand` from this module from your own commands. You need
to override two methods:

- The `init_parser` method, which adds options to the parser. Note that you should normally
    *not* override the normal `parse` method when inheriting from `UnixCommand`.
- The `func` method, called to execute the command once parsed (like any Command).

Here's a short example:

```python
from evennia.contrib.base_systems.unixcommand import UnixCommand

class CmdPlant(UnixCommand):

    '''
    Plant a tree or plant.

    This command is used to plant something in the room you are in.

    Examples:
      plant orange -a 8
      plant strawberry --hidden
      plant potato --hidden --age 5

    '''

    key = "plant"

        "Add the arguments to the parser."
        # 'self.parser' inherits `argparse.ArgumentParser`
        self.parser.add_argument("key",
                help="the key of the plant to be planted here")
        self.parser.add_argument("-a", "--age", type=int,
                default=1, help="the age of the plant to be planted")
        self.parser.add_argument("--hidden", action="store_true",
                help="should the newly-planted plant be hidden to players?")

        "func is called only if the parser succeeded."
        # 'self.opts' contains the parsed options
        key = self.opts.key
        age = self.opts.age
        hidden = self.opts.hidden
        self.msg("Going to plant '{}', age={}, hidden={}.".format(
                key, age, hidden))
```

To see the full power of argparse and the types of supported options, visit
[the documentation of argparse](https://docs.python.org/2/library/argparse.html).

"""

import argparse
import shlex

from evennia import Command, InterruptCommand

class ParseError(Exception):

    """An error occurred during parsing."""

    pass

class UnixCommandParser(argparse.ArgumentParser):

    """A modifier command parser for unix commands.

    This parser is used to replace `argparse.ArgumentParser`.  It
    is aware of the command calling it, and can more easily report to
    the caller.  Some features (like the "brutal exit" of the original
    parser) are disabled or replaced.  This parser is used by UnixCommand
    and creating one directly isn't recommended nor necessary.  Even
    adding a sub-command will use this replaced parser automatically.

    """

    def __init__(self, prog, description="", epilog="", command=None, **kwargs):
        """
        Build a UnixCommandParser with a link to the command using it.

        Args:
            prog (str): the program name (usually the command key).
            description (str): a very brief line to show in the usage text.
            epilog (str): the epilog to show below options.
            command (Command): the command calling the parser.

            Additional keyword arguments are directly sent to
            `argparse.ArgumentParser`.  You will find them on the
            [parser's documentation](https://docs.python.org/2/library/argparse.html).

        Note:
            It's doubtful you would need to create this parser manually.
            The `UnixCommand` does that automatically.  If you create
            sub-commands, this class will be used.

        """
        prog = prog or command.key
            prog=prog, description=description, conflict_handler="resolve", add_help=False, **kwargs
        )
        self.command = command
        self.post_help = epilog

        def n_exit(code=None, msg=None):
            raise ParseError(msg)

        self.exit = n_exit

        # Replace the -h/--help
        self.add_argument(
            "-h", "--help", nargs=0, action=HelpAction, help="display the command help"
        )

    def format_usage(self):
        """Return the usage line.

        Note:
            This method is present to return the raw-escaped usage line,
            in order to avoid unintentional color codes.

        """
        return raw(super().format_usage())

    def format_help(self):
        """Return the parser help, including its epilog.

        Note:
            This method is present to return the raw-escaped help,
            in order to avoid unintentional color codes.  Color codes
            in the epilog (the command docstring) are supported.

        """
        autohelp = raw(super().format_help())
        return "\n" + autohelp + "\n" + self.post_help

    def print_usage(self, file=None):
        """Print the usage to the caller.

        Args:
            file (file-object): not used here, the caller is used.

        Note:
            This method will override `argparse.ArgumentParser`'s in order
            to not display the help on stdout or stderr, but to the
            command's caller.

        """
        if self.command:
            self.command.msg(self.format_usage().strip())

    def print_help(self, file=None):
        """Print the help to the caller.

        Args:

        Note:

        """
            self.command.msg(self.format_help().strip())

class HelpAction(argparse.Action):

    """Override the -h/--help action in the default parser.

    Using the default -h/--help will call the exit function in different
    ways, preventing the entire help message to be provided.  Hence
    this override.

    """

    def __call__(self, parser, namespace, values, option_string=None):
        """If asked for help, display to the caller."""
        if parser.command:
            parser.command.msg(parser.format_help().strip())
            parser.exit(0, "")

class UnixCommand(Command):
    """
    Unix-type commands, supporting short and long options.

    This command syntax uses the Unix-style commands with short options
    (-X) and long options (--something).  The `argparse` module is
    used to parse the command.

    In order to use it, you should override two methods:
    - `init_parser`: this method is called when the command is created.
      It can be used to set options in the parser.  `self.parser`
      contains the `argparse.ArgumentParser`, so you can add arguments
      here.
    - `func`: this method is called to execute the command, but after
      the parser has checked the arguments given to it are valid.
      You can access the namespace of valid arguments in `self.opts`
      at this point.

    The help of UnixCommands is derived from the docstring, in a
    slightly different way than usual: the first line of the docstring
    is used to represent the program description (the very short
    line at the top of the help message).  The other lines below are
    used as the program's "epilog", displayed below the options.  It
    means in your docstring, you don't have to write the options.
    They will be automatically provided by the parser and displayed
    accordingly.  The `argparse` module provides a default '-h' or
    '--help' option on the command.  Typing |whelp commandname|n will
    display the same as |wcommandname -h|n, though this behavior can
    be changed.

    """

        """

        """

        # Create the empty UnixCommandParser, inheriting argparse.ArgumentParser
        lines = dedent(self.__doc__.strip("\n")).splitlines()
        description = lines[0].strip()
        epilog = "\n".join(lines[1:]).strip()
        self.parser = UnixCommandParser(None, description, epilog, command=self)

        # Fill the argument parser
        self.init_parser()

        """
        Configure the argument parser, adding in options.

        Note:
            This method is to be overridden in order to add options
            to the argument parser.  Use `self.parser`, which contains
            the `argparse.ArgumentParser`.  You can, for instance,
            use its `add_argument` method.

        """
        pass

        """Override to handle the command execution."""
        pass

        """

        Args:
            caller (Object or Player): the caller asking for help on the command.

        Returns:

        """
        return self.parser.format_help()

        """
        Process arguments provided in `self.args`.

        Note:
            You should not override this method.  Consider overriding
            `init_parser` instead.

        """
        try:
            self.opts = self.parser.parse_args(shlex.split(self.args))
        except ParseError as err:
            msg = str(err)
            if msg:

"""
Commands for the Evscaperoom. This contains all in-room commands as well as
admin debug-commands to help development.

Gameplay commands

- `look` - custom look
- `focus` - focus on object (to perform actions on it)
- `<action> <obj>` - arbitrary interaction with focused object
- `stand` - stand on the floor, resetting any position
- `emote` - free-form emote
- `say/whisper/shout` - simple communication

Other commands

- `evscaperoom` - starts the evscaperoom top-level menu
- `help` - custom in-room help command
- `options` - set game/accessibility options
- `who` - show who's in the room with you
- `quit` - leave a room, return to menu

Admin/development commands

- `jumpstate` - jump to specific room state
- `flag` - assign a flag to an object
- `createobj` - create a room-object set up for Evscaperoom

"""

import re

    SESSION_HANDLER,
    CmdSet,
    Command,
    InterruptCommand,
    default_cmds,
    syscmdkeys,
)
from evennia.utils import variable_from_module

from .utils import create_evscaperoom_object

_RE_ARGSPLIT = re.compile(r"\s(with|on|to|in|at)\s", re.I + re.U)
_RE_EMOTE_SPEECH = re.compile(r"(\".*?\")|(\'.*?\')")
_RE_EMOTE_NAME = re.compile(r"(/\w+)")
_RE_EMOTE_PROPER_END = re.compile(r"\.$|\.[\'\"]$|\![\'\"]$|\?[\'\"]$")

# configurable help

if hasattr(settings, "EVSCAPEROOM_HELP_SUMMARY_TEXT"):
    _HELP_SUMMARY_TEXT = settings.EVSCAPEROOM_HELP_SUMMARY_TEXT
else:
    _HELP_SUMMARY_TEXT = """
   |yWhat to do ...|n
    - Your goal is to |wescape|n the room. To do that you need to |wlook|n at
      your surroundings for clues on how to escape. When you find something
      interesting, |wexamine|n it for any actions you could take with it.
   |yHow to explore ...|n
    - |whelp [obj or command]|n           - get usage help (never puzzle-related)
    - |woptions|n                         - set game/accessibility options
    - |wlook/l [obj]|n                    - give things a cursory look.
    - |wexamine/ex/e [obj]|n              - look closer at an object. Use again to
                                        look away.
    - |wstand|n                           - stand up if you were sitting, lying etc.
   |yHow to express yourself ...|n
    - |wwho [all]|n                       - show who's in the room or on server.
    - |wemote/pose/: <something>|n        - free-form emote. Use /me to refer
                                        to yourself and /name to refer to other
                                        things/players. Use quotes "..." to speak.
    - |wsay/; <something>|n               - quick-speak your mind
    - |wwhisper <something>|n             - whisper your mind
    - |wshout <something>|n               - shout your mind
   |yHow to quit like a little baby ...|n
    - |wquit / give up|n                  - admit defeat and give up
"""

_HELP_FALLBACK_TEXT = """
There is no help to be had about |y{this}|n. To look away, use |wexamine|n on
its own or with another object you are interested in.
"""

_QUIT_WARNING = """
|rDo you really want to quit?|n

{warning}

Enter |w'quit'|n again to truly give up.
"""

_QUIT_WARNING_CAN_COME_BACK = """
(Since you are not the last person to leave this room, you |gcan|n get back in here
by joining room '|c{roomname}|n' from the menu. Note however that if you leave
now, any personal achievements you may have gathered so far will be |rlost|n!)
"""

_QUIT_WARNING_LAST_CHAR = """
(You are the |rlast|n player to leave this room ('|c{roomname}|n'). This means that when you
leave, this room will go away and you |rwon't|n be able to come back to it!)
"""

class CmdEvscapeRoom(Command):
    """
    Base command parent for all Evscaperoom commands.

    This operates on the premise of 'focus' - the 'focus'
    is set on the caller, then subsequent commands will
    operate on that focus. If no focus is set,
    the operation will be general or on the room.

    Syntax:

       command [<obj1>|<arg1>] [<prep> <obj2>|<arg2>]

    """

    # always separate the command from any args with a space
    help_category = "Evscaperoom"

    # these flags allow child classes to determine how strict parsing for obj1/obj2 should be
    # (if they are given at all):
    # True - obj1/obj2 must be found as Objects, otherwise it's an error aborting command
    # False - obj1/obj2 will remain None, instead self.arg1, arg2 will be stored as strings
    # None - if obj1/obj2 are found as Objects, set them, otherwise set arg1/arg2 as strings
    obj1_search = None
    obj2_search = None

    def _search(self, query, required):
        """
        This implements the various search modes

        Args:
            query (str): The search query
            required (bool or None): This defines if query *must* be
                found to match a single local Object or not. If None,
                a non-match means returning the query unchanged. When
                False, immediately return the query. If required is False,
                don't search at all.
        Return:
            match (Object or str): The match or the search string depending
                on the `required` mode.
        Raises:
            InterruptCommand: Aborts the command quietly.
        Notes:
            The _AT_SEARCH_RESULT function will handle all error messaging
            for us.

        """
        if required is False:
            return None, query

        matches = self.caller.search(query, quiet=True)

        if not matches or len(matches) > 1:
            if required:
                if not query:
                    self.caller.msg("You must give an argument.")
                else:
                    _AT_SEARCH_RESULT(matches, self.caller, query=query)
            else:
                return None, query
        else:
            return matches[0], None

        """
        Parse incoming arguments for use in all child classes.

        """
        self.args = self.args.strip()

        # splits to either ['obj'] or e.g. ['obj', 'on', 'obj']
        parts = [part.strip() for part in _RE_ARGSPLIT.split(" " + self.args, 1)]
        nparts = len(parts)
        self.obj1 = None
        self.arg1 = None
        self.prep = None
        self.obj2 = None
        self.arg2 = None
        if nparts == 1:
            self.obj1, self.arg1 = self._search(parts[0], self.obj1_search)
        elif nparts == 3:
            obj1, self.prep, obj2 = parts
            self.obj1, self.arg1 = self._search(obj1, self.obj1_search)
            self.obj2, self.arg2 = self._search(obj2, self.obj2_search)

        self.room = caller.location
        self.roomstate = self.room.db.state

    @property
    def focus(self):
        return self.caller.attributes.get("focus", category=self.room.db.tagcategory)

    @focus.setter
    def focus(self, obj):
        self.caller.attributes.add("focus", obj, category=self.room.tagcategory)

    @focus.deleter
        self.caller.attributes.remove("focus", category=self.room.tagcategory)

class CmdGiveUp(CmdEvscapeRoom):
    """
    Give up

    Usage:
      give up

    Abandons your attempts at escaping and of ever winning the pie-eating contest.

    """

    key = "give up"
    aliases = ("abort", "chicken out", "quit", "q")

        from .menu import run_evscaperoom_menu

        nchars = len(self.room.get_all_characters())
        if nchars == 1:
            warning = _QUIT_WARNING_LAST_CHAR.format(roomname=self.room.name)
            warning = _QUIT_WARNING.format(warning=warning)
        else:
            warning = _QUIT_WARNING_CAN_COME_BACK.format(roomname=self.room.name)

        ret = yield (warning)
        if ret.upper() == "QUIT":
            self.msg("|R ... Oh. Okay then. Off you go.|n\n")
            yield (1)

            self.room.log(f"QUIT: {self.caller.key} used the quit command")

            # manually call move hooks
            self.room.msg_room(self.caller, f"|r{self.caller.key} gave up and was whisked away!|n")
            self.room.at_object_leave(self.caller, self.caller.home)
            self.caller.move_to(
                self.caller.home, quiet=True, move_hooks=False, move_type="teleport"
            )

            # back to menu
            run_evscaperoom_menu(self.caller)
        else:
            self.msg("|gYou're staying? That's the spirit!|n")

class CmdLook(CmdEvscapeRoom):
    """
    Look at the room, an object or the currently focused object

    Usage:
      look [obj]

    """

        target = self.obj1 or self.obj2 or self.focus or self.room
        # the at_look hook will in turn call return_appearance and
        # pass the 'unfocused' kwarg to it
        txt = caller.at_look(target, unfocused=(target and target != self.focus))
        self.room.msg_char(caller, txt, client_type="look")

class CmdWho(CmdEvscapeRoom, default_cmds.CmdWho):
    """
    List other players in the game.

    Usage:
      who
      who all

    Show who is in the room with you, or (with who all), who is online on the
    server as a whole.

    """

    key = "who"

    obj1_search = False
    obj2_search = False

        if self.args == "all":
            table = self.style_table("|wName", "|wRoom")
            sessions = SESSION_HANDLER.get_sessions()
            for session in sessions:
                if puppet:
                    location = puppet.location
                    locname = location.key if location else "(Outside somewhere)"
                    table.add_row(puppet, locname)
                else:
                    account = session.get_account()
                    table.add_row(account.get_display_name(caller), "(OOC)")

            txt = (
                f"|cPlayers active on this server|n:\n{table}\n"
                "(use 'who' to see only those in your room)"
            )

        else:
            chars = [
                f"{obj.get_display_name(caller)} - {obj.db.desc.strip()}"
                for obj in self.room.get_all_characters()
                if obj != caller
            ]
            chars = "\n".join([f"{caller.key} - {caller.db.desc.strip()} (you)"] + chars)
            txt = f"|cPlayers in this room (room-name '{self.room.name}')|n:\n  {chars}"
        caller.msg(txt)

class CmdSpeak(Command):
    """
    Perform an communication action.

    Usage:
      say <text>
      whisper
      shout

    """

    key = "say"
    aliases = [";", "shout", "whisper"]
    arg_regex = r"\w|\s|$"

        action = self.cmdname
        action = "say" if action == ";" else action
        room = self.caller.location

            caller.msg(f"What do you want to {action}?")
            return
        if action == "shout":
            args = f"|c{args.upper()}|n"
        elif action == "whisper":
            args = f"|C({args})|n"
        else:
            args = f"|c{args}|n"

        message = f"~You ~{action}: {args}"

        if hasattr(room, "msg_room"):
            room.msg_room(caller, message)
            room.log(f"{action} by {caller.key}: {args}")

class CmdEmote(Command):
    """
    Perform a free-form emote. Use /me to
    include yourself in the emote and /name
    to include other objects or characters.
    Use "..." to enact speech.

    Usage:
        emote <emote>
        :<emote

    Example:
        emote /me smiles at /peter
        emote /me points to /box and /lever.

    """

    key = "emote"
    aliases = [":", "pose"]

    def you_replace(match):
        return match

    def room_replace(match):

        emote = self.args.strip()

        if not emote:
            self.caller.msg('Usage: emote /me points to /door, saying "look over there!"')
            return

        speech_clr = "|c"
        obj_clr = "|y"
        self_clr = "|g"
        player_clr = "|b"
        add_period = not _RE_EMOTE_PROPER_END.search(emote)

        emote = _RE_EMOTE_SPEECH.sub(speech_clr + r"\1\2|n", emote)

        characters = room.get_all_characters()
        logged = False
        for target in characters:
            txt = []
            self_refer = False
            for part in _RE_EMOTE_NAME.split(emote):
                nameobj = None
                if part.startswith("/"):
                    name = part[1:]
                    if name == "me":
                        nameobj = self.caller
                        self_refer = True
                    else:
                        match = self.caller.search(name, quiet=True)
                        if len(match) == 1:
                            nameobj = match[0]
                if nameobj:
                    if target == nameobj:
                        part = f"{self_clr}{nameobj.get_display_name(target)}|n"
                    elif nameobj in characters:
                        part = f"{player_clr}{nameobj.get_display_name(target)}|n"
                    else:
                        part = f"{obj_clr}{nameobj.get_display_name(target)}|n"
                txt.append(part)
            if not self_refer:
                if target == self.caller:
                    txt = [f"{self_clr}{self.caller.get_display_name(target)}|n "] + txt
                else:
                    txt = [f"{player_clr}{self.caller.get_display_name(target)}|n "] + txt
            txt = "".join(txt).strip() + ("." if add_period else "")
            if not logged and hasattr(self.caller.location, "log"):
                self.caller.location.log(f"emote: {txt}")
                logged = True
            target.msg(txt)

class CmdFocus(CmdEvscapeRoom):
    """
    Focus your attention on a target.

    Usage:
      focus <obj>

    Once focusing on an object, use look to get more information about how it
    looks and what actions is available.

    """

    key = "focus"
    aliases = ["examine", "e", "ex", "unfocus"]

        if self.obj1:
            old_focus = self.focus
            if hasattr(old_focus, "at_unfocus"):
                old_focus.at_unfocus(self.caller)

            if not hasattr(self.obj1, "at_focus"):
                self.caller.msg("Nothing of interest there.")
                return

            if self.focus != self.obj1:
                self.room.msg_room(
                    self.caller, f"~You ~examine *{self.obj1.key}.", skip_caller=True
                )
            self.focus = self.obj1
            self.obj1.at_focus(self.caller)
        elif not self.focus:
            self.caller.msg("What do you want to focus on?")
        else:
            del self.focus
            self.caller.msg(f"You no longer focus on |y{old_focus.key}|n.")

class CmdOptions(CmdEvscapeRoom):
    """
    Start option menu

    Usage:
      options

    """

    key = "options"
    aliases = ["option"]

        from .menu import run_option_menu

        run_option_menu(self.caller, self.session)

class CmdGet(CmdEvscapeRoom):
    """
    Use focus / examine instead.

    """

    key = "get"
    aliases = ["inventory", "i", "inv", "give"]

        self.caller.msg("Use |wfocus|n or |wexamine|n for handling objects.")

class CmdRerouter(default_cmds.MuxCommand):
    """
    Interact with an object in focus.

    Usage:
       <action> [arg]

    """

    # reroute commands from the default cmdset to the catch-all
    # focus function where needed. This allows us to override
    # individual default commands without replacing the entire
    # cmdset (we want to keep most of them).

    key = "open"
    aliases = ["@dig", "@open"]

        # reroute to another command
        from evennia.commands import cmdhandler

        cmdhandler.cmdhandler(
            self.session, self.raw_string, cmdobj=CmdFocusInteraction(), cmdobj_key=self.cmdname
        )

class CmdFocusInteraction(CmdEvscapeRoom):
    """

    Usage:

    This is a special catch-all command which will operate on
    the current focus. It will look for a method
        `focused_object.at_focus_<action>(caller, **kwargs)` and call
    it. This allows objects to just add a new hook to make that
    action apply to it. The obj1, prep, obj2, arg1, arg2 are passed
    as keys into the method.

    """

    # all commands not matching something else goes here.
    key = syscmdkeys.CMD_NOMATCH

        """
        We assume this type of command is always on the form `command [arg]`

        """
        parts = self.args.split(None, 1)
            self.action, self.args = "", ""
        elif len(parts) == 1:
            self.action = parts[0]
            self.args = ""
        else:
            self.action, self.args = parts
        self.room = self.caller.location

        focused = self.focus
        action = self.action

        if focused and hasattr(focused, f"at_focus_{action}"):
            # there is a suitable hook to call!
            getattr(focused, f"at_focus_{action}")(self.caller, args=self.args)
        else:
            self.caller.msg("Hm?")

class CmdStand(CmdEvscapeRoom):
    """
    Stand up from whatever position you had.

    """

    key = "stand"

        # Positionable objects will set this flag on you.
        pos = self.caller.attributes.get("position", category=self.room.tagcategory)

        if pos:
            # we have a position, clean up.
            obj, position = pos
            self.caller.attributes.remove("position", category=self.room.tagcategory)
            del obj.db.positions[self.caller]
            self.room.msg_room(self.caller, "~You ~are back standing on the floor again.")
        else:
            self.caller.msg("You are already standing.")

class CmdHelp(CmdEvscapeRoom, default_cmds.CmdHelp):
    """
    Get help.

    Usage:
      help <topic> or <command>

    """

            if hasattr(self.obj1, "get_help"):
                helptxt = self.obj1.get_help(self.caller)
                if not helptxt:
                    helptxt = f"There is no help to be had about {self.obj1.get_display_name(self.caller)}."
            else:
                helptxt = (
                    f"|y{self.obj1.get_display_name(self.caller)}|n is "
                    "likely |rnot|n part of any of the Jester's trickery."
                )
        elif self.arg1:
            # fall back to the normal help command
            super().func()
            return
        else:
            helptxt = _HELP_SUMMARY_TEXT
        self.caller.msg(helptxt.rstrip())

# Debug/help command

class CmdCreateObj(CmdEvscapeRoom):
    """
    Create command, only for Admins during debugging.

    Usage:
      createobj name[:typeclass]

    Here, :typeclass is a class in evscaperoom.commands

    """

    key = "createobj"
    aliases = ["cobj"]
    locks = "cmd:perm(Admin)"

            caller.msg("Usage: createobj name[:typeclass]")
            return

        typeclass = "EvscaperoomObject"
            name, typeclass = (part.strip() for part in args.rsplit(":", 1))

        if typeclass.startswith("state_"):
            # a state class
            typeclass = "evscaperoom.states." + typeclass
        else:
            name = args.strip()

        obj = create_evscaperoom_object(typeclass=typeclass, key=name, location=self.room)
        caller.msg(f"Created new object {name} ({obj.typeclass_path}).")

class CmdSetFlag(CmdEvscapeRoom):
    """
    Assign a flag to an object. Admin use only

    Usage:
      flag <obj> with <flagname>

    """

    key = "flag"
    aliases = ["setflag"]

    obj1_search = True

        if not self.arg2:
            self.caller.msg("Usage: flag <obj> with <flagname>")
            return

        if hasattr(self.obj1, "set_flag"):
            if self.obj1.check_flag(self.arg2):
                self.obj1.unset_flag(self.arg2)
                self.caller.msg(f"|rUnset|n flag '{self.arg2}' on {self.obj1}.")
            else:
                self.obj1.set_flag(self.arg2)
                self.caller.msg(f"|gSet|n flag '{self.arg2}' on {self.obj1}.")
        else:
            self.caller.msg(f"Cannot set flag on {self.obj1}.")

class CmdJumpState(CmdEvscapeRoom):
    """
    Jump to a given state.

    Args:
      jumpstate <statename>

    """

    key = "jumpstate"

        self.caller.msg(f"Trying to move to state {self.args}")
        self.room.next_state(self.args)

# Helper command to start the Evscaperoom menu

class CmdEvscapeRoomStart(Command):
    """
    Go to the Evscaperoom start menu

    """

    key = "evscaperoom"
    help_category = "EvscapeRoom"

        # need to import here to break circular import

        run_evscaperoom_menu(self.caller)

# command sets

class CmdSetEvScapeRoom(CmdSet):
    priority = 1

        self.add(CmdHelp())
        self.add(CmdLook())
        self.add(CmdGiveUp())
        self.add(CmdFocus())
        self.add(CmdSpeak())
        self.add(CmdEmote())
        self.add(CmdFocusInteraction())
        self.add(CmdStand())
        self.add(CmdWho())
        self.add(CmdOptions())
        # rerouters
        self.add(CmdGet())
        self.add(CmdRerouter())
        # admin commands
        self.add(CmdCreateObj())
        self.add(CmdSetFlag())
        self.add(CmdJumpState())

"""
Start menu

This is started from the `evscaperoom` command.

Here player user can set their own description as well as select to create a
new room (to start from scratch) or join an existing room (with other players).

"""
from evennia import EvMenu
from evennia.utils import create, justify, list_to_string, logger
from evennia.utils.evmenu import list_node

from .room import EvscapeRoom
from .utils import create_fantasy_word

# Main menu

_START_TEXT = """
|mEv|rScape|mRoom|n

|x- an escape-room experience using Evennia|n

You are |c{name}|n - {desc}|n.

Make a selection below.
"""

_CREATE_ROOM_TEXT = """
This will create a |ynew, empty room|n to challenge you.

Other players can be thrown in there at any time.

Remember that if you give up and are the last person to leave, that particular
room will be gone!

|yDo you want to create (and automatically join) a new room?|n")
"""

_JOIN_EXISTING_ROOM_TEXT = """
This will have you join an existing room ({roomname}).

This is {percent}% complete and has {nplayers} player(s) in it already:

 {players}

|yDo you want to join this room?|n
"""

def _move_to_room(caller, raw_string, **kwargs):
    """
    Helper to move a user to a room

    """
    room = kwargs["room"]
    room.msg_char(caller, f"Entering room |c'{room.name}'|n ...")
    room.msg_room(caller, f"~You |c~were just tricked in here too!|n")
    # we do a manual move since we don't want all hooks to fire.
    old_location = caller.location
    caller.location = room
    room.at_object_receive(caller, old_location)
    return "node_quit", {"quiet": True}

def _create_new_room(caller, raw_string, **kwargs):
    # create a random name, retrying until we find
    # a unique one
    key = create_fantasy_word(length=5, capitalize=True)
    while EvscapeRoom.objects.filter(db_key=key):
        key = create_fantasy_word(length=5, capitalize=True)
    room = create.create_object(EvscapeRoom, key=key)
    # we must do this once manually for the new room
    room.statehandler.init_state()
    _move_to_room(caller, "", room=room)

    nrooms = EvscapeRoom.objects.all().count()
    logger.log_info(
        f"Evscaperoom: {caller.key} created room '{key}' (#{room.id}). Now {nrooms} room(s) active."
    )

    room.log(f"JOIN: {caller.key} created and joined room")

def _get_all_rooms(caller):
    """
    Get a list of all available rooms and store the mapping
    between option and room so we get to it later.

    """
    room_option_descs = []
    room_map = {}
    for room in EvscapeRoom.objects.all():
        if not room.pk or room.db.deleting:
            continue
        stats = room.db.stats or {"progress": 0}
        progress = int(stats["progress"])
        nplayers = len(room.get_all_characters())
        desc = (
            f"Join room |c'{room.get_display_name(caller)}'|n "
            f"(complete: {progress}%, players: {nplayers})"
        )
        room_map[desc] = room
        room_option_descs.append(desc)
    caller.ndb._menutree.room_map = room_map
    return room_option_descs

def _select_room(caller, menuchoice, **kwargs):
    """
    Get a room from the selection using the mapping we created earlier.
    """
    room = caller.ndb._menutree.room_map[menuchoice]
    return "node_join_room", {"room": room}

@list_node(_get_all_rooms, _select_room)
def node_start(caller, raw_string, **kwargs):
    text = _START_TEXT.strip()
    text = text.format(name=caller.key, desc=caller.db.desc)

    # build a list of available rooms
    options = (
        {
            "key": (
                "|y[s]et your description|n",
                "set your description",
                "set",
                "desc",
                "description",
                "s",
            ),
            "goto": "node_set_desc",
        },
        {
            "key": ("|y[c]reate/join a new room|n", "create a new room", "create", "c"),
            "goto": "node_create_room",
        },
        {"key": ("|r[q]uit the challenge", "quit", "q"), "goto": "node_quit"},
    )

    return text, options

def node_set_desc(caller, raw_string, **kwargs):
    current_desc = kwargs.get("desc", caller.db.desc)

    text = (
        "Your current description is\n\n " f'   "{current_desc}"' "\n\nEnter your new description!"
    )

    def _temp_description(caller, raw_string, **kwargs):
        desc = raw_string.strip()
        if 5 < len(desc) < 40:
            return None, {"desc": raw_string.strip()}
        else:
            caller.msg("|rYour description must be 5-40 characters long.|n")

    def _set_description(caller, raw_string, **kwargs):
        caller.db.desc = kwargs.get("desc")
        caller.msg("You set your description!")
        return "node_start"

    options = (
        {"key": "_default", "goto": _temp_description},
        {"key": ("|g[a]ccept", "a"), "goto": (_set_description, {"desc": current_desc})},
        {"key": ("|r[c]ancel", "c"), "goto": "node_start"},
    )

def node_create_room(caller, raw_string, **kwargs):
    text = _CREATE_ROOM_TEXT

    options = (
        {"key": ("|g[c]reate new room and start game|n", "c"), "goto": _create_new_room},
        {"key": ("|r[a]bort and go back|n", "a"), "goto": "node_start"},
    )

def node_join_room(caller, raw_string, **kwargs):
    stats = room.db.stats or {"progress": 0}

    players = [char.key for char in room.get_all_characters()]
    text = _JOIN_EXISTING_ROOM_TEXT.format(
        roomname=room.get_display_name(caller),
        percent=int(stats["progress"]),
        nplayers=len(players),
        players=list_to_string(players),
    )

    options = (
        {"key": ("|g[a]ccept|n (default)", "a"), "goto": (_move_to_room, kwargs)},
        {"key": ("|r[c]ancel|n", "c"), "goto": "node_start"},
        {"key": "_default", "goto": (_move_to_room, kwargs)},
    )

def node_quit(caller, raw_string, **kwargs):
    quiet = kwargs.get("quiet")
    text = ""
    if not quiet:
        text = "Goodbye for now!\n"
        # we check an Attribute on the caller to see if we should
        # leave the game entirely when leaving
        if caller.db.evscaperoom_standalone:
            from evennia import default_cmds
            from evennia.commands import cmdhandler

            cmdhandler.cmdhandler(
                caller.ndb._menutree._session, "", cmdobj=default_cmds.CmdQuit(), cmdobj_key="@quit"
            )

    return text, None  # empty options exit the menu

class EvscaperoomMenu(EvMenu):
    """
    Custom menu with a different formatting of options.

    """

    node_border_char = "~"

    def nodetext_formatter(self, text):
        return justify(text.strip("\n").rstrip(), align="c", indent=1)

    def options_formatter(self, optionlist):
        main_options = []
        room_choices = []
        for key, desc in optionlist:
            if key.isdigit():
                room_choices.append((key, desc))
            else:
                main_options.append(key)
        main_options = " | ".join(main_options)
        room_choices = super().options_formatter(room_choices)
        return "{}{}{}".format(main_options, "\n\n" if room_choices else "", room_choices)

# access function
def run_evscaperoom_menu(caller):
    """
    Run room selection menu

    """
    menutree = {
        "node_start": node_start,
        "node_quit": node_quit,
        "node_set_desc": node_set_desc,
        "node_create_room": node_create_room,
        "node_join_room": node_join_room,
    }

    EvscaperoomMenu(caller, menutree, startnode="node_start", cmd_on_exit=None, auto_quit=True)

# In-game Options menu

def _set_thing_style(caller, raw_string, **kwargs):
    room = caller.location
    options = caller.attributes.get("options", category=room.tagcategory, default={})
    options["things_style"] = kwargs.get("value", 2)
    caller.attributes.add("options", options, category=room.tagcategory)
    return None, kwargs  # rerun node

def _toggle_screen_reader(caller, raw_string, **kwargs):
    session = kwargs["session"]
    # flip old setting
    session.protocol_flags["SCREENREADER"] = not session.protocol_flags.get("SCREENREADER", False)
    # sync setting with portal
    session.sessionhandler.session_portal_sync(session)

def node_options(caller, raw_string, **kwargs):
    text = "|cOption menu|n\n('|wq|nuit' to return)"

    things_style = options.get("things_style", 2)

    session = kwargs["session"]  # we give this as startnode_input when starting menu
    screenreader = session.protocol_flags.get("SCREENREADER", False)

    options = (
        {
            "desc": "{}No item markings (hard mode)".format(
                "|g(*)|n " if things_style == 0 else "( ) "
            ),
            "goto": (_set_thing_style, {"value": 0, "session": session}),
        },
        {
            "desc": "{}Items marked as |yitem|n (with color)".format(
                "|g(*)|n " if things_style == 1 else "( ) "
            ),
            "goto": (_set_thing_style, {"value": 1, "session": session}),
        },
        {
            "desc": "{}Items are marked as |y[item]|n (screenreader friendly)".format(
                "|g(*)|n " if things_style == 2 else "( ) "
            ),
            "goto": (_set_thing_style, {"value": 2, "session": session}),
        },
        {
            "desc": "{}Screenreader mode".format("(*) " if screenreader else "( ) "),
            "goto": (_toggle_screen_reader, kwargs),
        },
    )

class OptionsMenu(EvMenu):
    """
    Custom display of Option menu
    """

    def node_formatter(self, nodetext, optionstext):
        return f"{nodetext}\n\n{optionstext}"

def run_option_menu(caller, session):
    """
    Run option menu in-game
    """
    menutree = {"node_start": node_options}

    OptionsMenu(
        caller,
        menutree,
        startnode="node_start",
        cmd_on_exit="look",
        auto_quit=True,
        startnode_input=("", {"session": session}),
    )

"""
Base objects for the Evscaperoom contrib.

The object class itself provide the actions possible to use on that object.
This makes these objects suitable for use with multi-inheritance. For example,
to make an object both possible to smell and eat or drink, find the appropriate
parents in this module and make an object like this:

class Apple(Edible, Smellable):

    def at_drink(self, caller):
        # ...

    def at_smell(self, caller):
        # ...

Various object parents could be more complex, so read the class for more info.

Available parents:

- EvscapeRoomObject - parent class for all Evscaperoom entities (also the room itself)
- Feelable
- Listenable
- Smellable
- Rotatable
- Openable
- Readable
- IndexReadable  (like a lexicon you have to give a search term in)
- Movable
- Edible
- Drinkable
- Usable
- Insertable  (can be inserted into a target)
- Combinable  (combines with another object to create a new one)
- Mixable     (used for mixing potions into it)
- HasButtons  (an object with buttons on it)
- CodeInput   (code locks)
- Sittable    (can be sat on)
- Liable      (can be lied down on)
- Kneeable    (can be kneed down on)
- Climbable   (can be climbed on)
- Positionable (supports sit/lie/knee/climb at once)

"""
import re

from evennia import DefaultObject
from evennia.utils.utils import list_to_string, wrap

from .utils import create_evscaperoom_object, parse_for_perspectives, parse_for_things

class EvscaperoomObject(DefaultObject):
    """
    Default object base for all objects related to the contrib.

    """

    # these will be automatically filtered out by self.parse for
    # focus-commands using arguments like (`combine [with] object`)
    # override this per-class as necessary.
    action_prepositions = ("in", "with", "on", "into", "to")

    # this mapping allows for prettier descriptions of our current
    # position
    position_prep_map = {"sit": "sitting", "kneel": "kneeling", "lie": "lying", "climb": "standing"}

    def at_object_creation(self):
        """
        Called once when object is first created.

        """
        # state flags (setup/reset for each state).
        self.db.tagcategory = None
        self.db.flags = {}

        self.db.desc = "Nothing of interest."

        self.db.positions = {}

    _tagcategory = None

    @property
    def tagcategory(self):
        if not self._tagcategory:
            self._tagcategory = (
                self.location.db.tagcategory if self.location else self.db.tagcategory
            )
        return self._tagcategory

    @property
    def room(self):
        return self.location or self

    @property
    def roomstate(self):
        return self.room.statehandler.current_state

    def next_state(self, statename=None):
        """
        Helper to have the object switch the room to next state

        Args:
            statename (str, optional): If given, move to this
                state next. Otherwise use the default next-state
                of the current state.

        """
        self.room.statehandler.next_state(next_state=statename)

    def set_flag(self, flagname):
        "Set flag on object"
        self.db.flags[flagname] = True

    def unset_flag(self, flagname):
        "Unset flag on object"
        if flagname in self.db.flags:
            del self.db.flags[flagname]

    def check_flag(self, flagname):
        "Check if flag is set on this object"
        return self.db.flags.get(flagname, False)

    def set_character_flag(self, char, flagname, value=True):
        "Set flag on character"
        flags = char.attributes.get(flagname, category=self.tagcategory, default={})
        flags[flagname] = value
        char.attributes.add(flagname, flags, category=self.tagcategory)

    def unset_character_flag(self, char, flagname):
        if flagname in flags:
            flags.pop(flagname, None)
            char.attributes.add(flagname, flags, category=self.tagcategory)

    def check_character_flag(self, char, flagname):
        "Check if flag is set on character"
        return flags.get(flagname, False)

    def msg_room(self, caller, string, skip_caller=False):
        """
        Message everyone in the room with a message that is parsed for
        ~first/third person grammar, as well as for *thing markers.

        Args:
            caller (Object or None): Sender of the message. If None, there
                is no sender.
            string (str): Message to parse and send to the room.
            skip_caller (bool): Send to everyone except caller.

        Notes:
            Messages sent by this method will be tagged with a type of
            'your_action' and `others_action`. This is an experiment for
            allowing users of e.g. the webclient to redirect messages to
            differnt windows.

        """
        you = caller.key if caller else "they"
        first_person, third_person = parse_for_perspectives(string, you=you)
        for char in self.room.get_all_characters():
            options = char.attributes.get("options", category=self.room.tagcategory, default={})
            style = options.get("things_style", 2)
            if char == caller:
                if not skip_caller:
                    txt = parse_for_things(first_person, things_style=style)
                    char.msg((txt, {"type": "your_action"}))
            else:
                txt = parse_for_things(third_person, things_style=style)
                char.msg((txt, {"type": "others_action"}))

    def msg_char(self, caller, string, client_type="your_action"):
        """
        Send message only to caller (not to the room at large)

        """
        # we must clean away markers
        first_person, _ = parse_for_perspectives(string)
        options = caller.attributes.get("options", category=self.room.tagcategory, default={})
        style = options.get("things_style", 2)
        txt = parse_for_things(first_person, things_style=style)
        caller.msg((txt, {"type": client_type}))

    def msg_system(self, message, target=None, borders=True):
        """
        Send a 'system message' by using the State.msg function.
        """
        self.room.state.msg(message, target=target, borders=borders)

    def get_position(self, caller):
        """
        Get position of caller on this object (like lying, sitting, kneeling,
        standing). See the Positionable child class.

        Args:
            caller (Object): The one position we seek.

        Returns:
            obj, pos (Object, str): The object we have a position relative to,
                as well as the name of that position (lying, sitting, kneeling,
                standing).  If these are None, it means we are standing on the
                floor.

        """
        pos = caller.attributes.get("position", category=self.tagcategory)
        if pos:
            obj, old_position = pos
            return obj, old_position

    def set_position(self, caller, new_position):
        """
        Set position of caller (like lying, sitting, kneeling, standing)
        on this object. See Positionable child class.

        Args:
            caller (Object): The one positioning themselves on this object.
            new_position (str, None): One of "lie", "kneel", "sit" or "stand".
                If `None`, remove position (character stands normally on the
                floor).

        """
        if new_position is None:
            # reset position
            caller.attributes.remove("position", category=self.tagcategory)
            if caller in self.db.positions:
                del self.db.positions[caller]
        else:
            # set a new position on this object
            position = (self, new_position)
            caller.attributes.add("position", position, category=self.tagcategory)
            self.db.positions[caller] = new_position

    def at_focus(self, caller):
        """
        Called when somone is focusing on this object.

        Args:
            caller (Character): The one doing the focusing.

        """
        self.msg_char(caller, caller.at_look(self), client_type="look")

    def at_unfocus(self, caller):
        """
        Called when focus leaves this object. Note that more than one caller
        may be focusing on the object at the same time, so we should not change
        the state of the object itself here!

        Args:
            caller (Character): The one doing the unfocusing.

        """
        pass

    def at_speech(self, speaker, action):
        """
        We don't use the default at_say hook since we handle the send logic in
        the command. This is only meant to trigger eventual game-events when
        speaking to an object or the room.

        Args:
            speaker (Character): The one speaking.
            action (str): One of 'say', 'whisper' or 'shout'

        """
        pass

    def parse(self, args):
        """
        Simple parser of focus arguments starting with a preposition,
        like 'combine with <object>' <- we want to strip out the preposition
        here.

        """
        args = re.sub(
            r"|".join(r"^{}\s".format(prep) for prep in self.action_prepositions), "", args
        )
        return args

    def get_cmd_signatures(self):
        """
        This allows the object to return more detailed call signs
        for each of their at_focus_* methods. This is useful for
        things like detailed arguments (only 'move' but 'move left/right')

        Returns:
            callsigns (list, None): List of strings to inject into the
                available action list produced by `self.get_help`. If `None`,
                automatically find actions based on the method names.
            custom_helpstr (str): This should be the help text for
                the command with a marker `{callsigns}` for where to
                inject the list of callsigns.

        """
        command_signatures = []
        helpstr = ""
        methods = inspect.getmembers(self, predicate=inspect.ismethod)
        for name, method in methods:
            if name.startswith("at_focus_"):
                command_signatures.append(name[9:])
        command_signatures = sorted(command_signatures)

        if len(command_signatures) == 1:
            helpstr = f"It looks like {self.key} may be " "suitable to {callsigns}."
        else:
            helpstr = (
                f"At first glance, it looks like {self.key} might be " "suitable to {callsigns}."
            )
        return command_signatures, helpstr

    def get_short_desc(self, full_desc):
        """
        Extract the first sentence from the desc and use as the short desc.

        """
        mat = re.match(r"(^.*?[.?!])", full_desc.strip(), re.M + re.U + re.I + re.S)
        if mat:
            return mat.group(0).strip()
        return full_desc

    def get_help(self, caller):
        """
        Get help about this object. By default we return a
        listing of all actions you can do on this object.

        """
        # custom-created signatures. We don't sort these
        command_signatures, helpstr = self.get_cmd_signatures()

        callsigns = list_to_string(["*" + sig for sig in command_signatures], endsep="or")

        # parse for *thing markers (use these as items)

        helpstr = helpstr.format(callsigns=callsigns)
        helpstr = parse_for_things(helpstr, style, clr="|w")
        return wrap(helpstr, width=80)

    # Evennia hooks

    def return_appearance(self, looker, **kwargs):
        """Could be modified per state. We generally don't worry about the
        contents of the object by default.

        """
        # accept a custom desc
        desc = kwargs.get("desc", self.db.desc)

        if kwargs.get("unfocused", False):
            # use the shorter description
            focused = ""
            desc = self.get_short_desc(desc)
            helptxt = ""
        else:
            focused = " |g(examining |G- use '|gex|G' again to look away. See also '|ghelp|G')|n"
            helptxt = kwargs.get("helptxt", f"\n\n({self.get_help(looker)})")

        obj, pos = self.get_position(looker)
        pos = (
            f" |w({self.position_prep_map[pos]} on " f"{obj.get_display_name(looker)})"
            if obj
            else ""
        )

        return f" ~~ |y{self.get_display_name(looker)}|n{focused}{pos}|n ~~\n\n{desc}{helptxt}"

class Feelable(EvscaperoomObject):
    """
    Any object that you can feel the surface of.

    """

    def at_focus_feel(self, caller, **kwargs):
        self.msg_char(caller, f"You feel *{self.key}.")

class Listenable(EvscaperoomObject):
    """
    Any object one can listen to.

    """

    def at_focus_listen(self, caller, **kwargs):
        self.msg_char(caller, f"You listen to *{self.key}")

class Smellable(EvscaperoomObject):
    """
    Any object you can smell.

    """

    def at_focus_smell(self, caller, **kwargs):
        self.msg_char(caller, f"You smell *{self.key}.")

class Rotatable(EvscaperoomObject):
    """
    Any object that you can lift up and look at from different angles

    """

    rotate_flag = "rotatable"
    start_rotatable = True

        super().at_object_creation()

        if self.start_rotatable:
            self.set_flag("rotatable")

    def at_focus_rotate(self, caller, **kwargs):
        if self.check_flag("rotatable"):
            self.at_rotate(caller)
        else:
            self.at_cannot_rotate(caller)

    at_focus_turn = at_focus_rotate

    def at_rotate(self, caller):
        self.msg_char(caller, f"You turn *{self.key} around.")

    def at_cannot_rotate(self, caller):
        self.msg_char(caller, f"You cannot rotate this.")

class Openable(EvscaperoomObject):
    """
    Any object that you can open/close. It's lockable with
    a flag.

    """

    # this flag must be set for item to open. None for unlocked.
    unlock_flag = "unlocked"
    open_flag = "open"
    # start this item in the opened/unlocked state
    start_open = False

        if self.start_open:
            self.set_flag(self.unlock_flag)
            self.set_flag(self.open_flag)

    def at_focus_open(self, caller, **kwargs):
        if self.check_flag(self.open_flag):
            self.at_already_open(caller)
        elif self.unlock_flag is None or self.check_flag(self.unlock_flag):
            self.at_open(caller)
        else:
            self.at_locked(caller)

    def at_focus_close(self, caller, **kwargs):
        if not self.check_flag(self.open_flag):
            self.at_already_closed(caller)
        else:
            self.unset_flag(self.open_flag)
            self.at_close(caller)

    def at_open(self, caller):
        self.msg_char(caller, f"You open *{self.key}")

    def at_already_open(self, caller):
        self.msg_char(caller, f"{self.key.capitalize()} is already open.")

    def at_locked(self, caller):
        self.msg_char(caller, f"{self.key.capitalize()} won't open.")

    def at_close(self, caller):
        self.msg_char(caller, f"You close *{self.key}.")

    def at_already_closed(self, caller):
        self.msg_char(caller, f"{self.key.capitalize()} is already closed.")

class Readable(EvscaperoomObject):
    """
    Any object that you can read from. This is controlled
    from a flag.

    """

    # this must be set to be able to read. None to
    # always be able to read.

    read_flag = "readable"
    start_readable = True

        if self.start_readable:
            self.set_flag(self.read_flag)

    def at_focus_read(self, caller, **kwargs):
        if self.read_flag is None or self.check_flag(self.read_flag):
            self.at_read(caller)
        else:
            self.at_cannot_read(caller)

    def at_read(self, caller, *args, **kwargs):
        self.msg_char(caller, f"You read from *{self.key}.")

    def at_cannot_read(self, caller, *args, **kwargs):
        self.msg_char(caller, "You cannot understand a thing!")

class IndexReadable(Readable):
    """
    Any object for which you need to specify a key/index to get a given result
    back. For example a lexicon or book where you enter a topic or a page
    number to see what's to be read on that page.
    """

    # keys should be lower-key
    index = {"page1": "This is page1", "page2": "This is page2", "page two": "page2"}  # alias

        topic = kwargs.get("args").strip().lower()

        entry = self.index.get(topic, None)

        if entry is None or not self.check_flag(self.read_flag):
            self.at_cannot_read(caller, topic)
        else:
            if entry in self.index:
                # an alias-reroute
                entry = self.index[entry]
            self.at_read(caller, topic, entry)

        txt = (
            f"You don't have the time to read this from beginning to end. "
            "Use *read <topic> to look up something in particular."
        )
        return [], txt

    def at_cannot_read(self, caller, topic, *args, **kwargs):
        self.msg_char(caller, f"Cannot find an entry on '{topic}'.")

    def at_read(self, caller, topic, entry, *args, **kwargs):
        self.msg_char(caller, f"You read about '{topic}':\n{entry.strip()}")

class Movable(EvscaperoomObject):
    """
    Any object that can be moved from one place to another
    or in one direction or another.

    Once moved to a given position, the object's state will
    change.

    """

    # these are the possible locations (or directions) to move to
    # name: callable
    move_positions = {"left": "at_left", "right": "at_right"}
    start_position = "left"

        self.db.position = self.start_position

        txt = "Looks like you can {callsigns}."
        return ["move", "push", "shove left/right"], txt

    def at_focus_move(self, caller, **kwargs):
        pos = self.parse(kwargs["args"])
        callfunc_name = self.move_positions.get(pos)

        if callfunc_name:
            if self.db.position == pos:
                self.at_already_moved(caller, pos)
            else:
                self.db.position = pos
                getattr(self, callfunc_name)(caller)
        else:
            self.at_cannot_move(caller)

    at_focus_shove = at_focus_move
    at_focus_push = at_focus_move

    def at_cannot_move(self, caller):
        self.msg_char(caller, "That does not work.")

    def at_already_moved(self, caller, position):
        self.msg_char(caller, f"You already moved *{self.key} to the {position}.")

    def at_left(self, caller):
        self.msg_char(caller, f"You move *{self.key} left")

    def at_right(self, caller):
        self.msg_char(caller, f"You move *{self.key} right")

class BaseConsumable(EvscaperoomObject):
    """
    Any object that is consumable in some way. This acts as an
    abstract parent.

    This sets a flag that
    is unique for each person consuming, allowing it to e.g. only
    be consumed once (don't support multi-uses here, that's left for
    a custom object if needed).

    """

    consume_flag = "consume"
    # may only consume once
    one_consume_only = True

    def handle_consume(self, caller, action, **kwargs):
        """
        Wrap this by the at_focus method
        """
        if self.one_consume_only and self.has_consumed(caller):
            self.at_already_consumed(caller, action)
        else:
            self.has_consumed(caller, True)
            self.at_consume(caller, action)

    def has_consumed(self, caller, setflag=False):
        "Check if caller already consumed at least once"
        flag = f"{self.consume_flag}#{caller.id}"
        if setflag:
            self.set_flag(flag)
        else:
            return self.check_flag(flag)

    def at_consume(self, caller, action):
        if hasattr(self, f"at_{action}"):
            getattr(self, f"at_{action}")(caller)
        else:
            self.msg_char(caller, f"You {action} *{self.key}.")

    def at_already_consumed(self, caller, action):
        self.msg_char(caller, f"You can't {action} any more.")

class Edible(BaseConsumable):
    """
    Any object specifically possible to eat.

    """

    consume_flag = "eat"

    def at_focus_eat(self, caller, **kwargs):
        super().handle_consume(caller, "eat", **kwargs)

class Drinkable(BaseConsumable):
    """
    Any object specifically possible to drink.

    """

    consume_flag = "drink"

    def at_focus_drink(self, caller, **kwargs):
        super().handle_consume(caller, "drink", **kwargs)

    def at_focus_sip(self, caller, **kwargs):
        super().handle_consume(caller, "sip", **kwargs)

        self.msg_char(caller, f"You {action} from *{self.key}.")

        self.msg_char(caller, f"You can't drink any more.")

class BaseApplicable(EvscaperoomObject):
    """
    Any object that can be applied/inserted/used on another object in some way.
    This acts an an abstract base class.

    """

    # the target object this is to be used with must
    # have this flag. It'll likely be unique to this
    # object combination.
    target_flag = "applicable"

    def handle_apply(self, caller, action, **kwargs):
        """
        Wrap this with the at_focus methods in the child classes

        """
        args = self.parse(kwargs["args"])
            self.msg_char(caller, "You need to specify a target.")
            return
        obj = caller.search(args)
        if not obj:
            return
        try:
            can_apply = obj.check_flag(self.target_flag)
            can_apply = False
        if can_apply:
            self.at_apply(caller, action, obj)
        else:
            self.at_cannot_apply(caller, action, obj)

    def at_apply(self, caller, action, obj):
        self.msg_char(caller, f"You {action} *{self.key} to {obj.key}.")

    def at_cannot_apply(self, caller, action, obj):
        self.msg_char(caller, f"You cannot {action} *{self.key} to {obj.key}.")

class Usable(BaseApplicable):
    """
    Any object that can be used with another object.

    """

    target_flag = "usable"

    def at_focus_use(self, caller, **kwargs):
        super().handle_apply(caller, "use", **kwargs)

        self.msg_char(caller, f"You {action} *{self.key} with {obj.key}")

        self.msg_char(caller, f"You cannot {action} *{self.key} with {obj.key}.")

class Insertable(BaseApplicable):
    """
    Any object that can be inserted into another object.

    This would cover a key, for example.

    """

    # this would likely be a custom name
    target_flag = "insertable"

    def at_focus_insert(self, caller, **kwargs):
        super().handle_apply(caller, "insert", **kwargs)

        self.msg_char(caller, f"You {action} *{self.key} in {obj.key}.")

        txt = "You can use this object to {callsigns}"
        return ["insert in <object>"], txt

        self.msg_char(caller, f"You cannot {action} *{self.key} in {obj.key}.")

class Combinable(BaseApplicable):
    """
    Any object that combines with another object to create
    a new one.

    """

    # the other object must have this flag to be able to be combined
    # (this is likely unique for a given combination)
    target_flag = "combinable"
    # create-dict to pass into the create_object for the
    # new "combined" object.
    new_create_dict = {
        "typeclass": "evscaperoom.objects.Combinable",
        "key": "sword",
        "aliases": ["combined"],
    }
    # if set, destroy the two components used to make the new one
    destroy_components = True

    def at_focus_combine(self, caller, **kwargs):
        super().at_focus_apply(caller, **kwargs)

        txt = "It looks like this should work: {callsigns}"
        return ["combine <object>"], txt

    def at_apply(self, caller, action, other_obj):
        create_dict = self.new_create_dict
        if "location" not in create_dict:
            create_dict["location"] = self.location
        new_obj = create_evscaperoom_object(**create_dict)
        if new_obj and self.destroy_components:
            self.msg_char(
                caller, f"You combine *{self.key} with {other_obj.key} to make {new_obj.key}!"
            )
            other_obj.delete()

class Mixable(EvscaperoomObject):
    """
    Any object into which you can mix ingredients (such as when
    mixing a potion). This offers no actions on its own, instead
    the ingredients should be 'used' with this object in order
    mix, calling at_mix when they do.
    """

    # ingredients can check for this before they allow to mix at all
    mixer_flag = "mixer"
    # ingredients must have these flags and this order
    ingredient_recipe = ["ingredient1", "ingredient2", "ingredient3"]

        self.set_flag(self.mixer_flag)
        # this holds the ingredients as they are added
        self.db.ingredients = []

    def check_mixture(self):
        "check so mixture is correct, returning True/False."
        ingredients = list(self.db.ingredients)
        for iflag, flag in enumerate(self.ingredient_recipe):
            try:
                if not ingredients[iflag].check_flag(flag):
            except (IndexError, AttributeError):
        # we only get here if all ingredients have the right flags in the right
        # order

    def handle_mix(self, caller, ingredient, **kwargs):
        """
        Add ingredient object to mixture.

        Called by the mixing ingredient. We assume the ingredient has already
        checked to make sure they allow themselves to be mixed into an object
        with this mixer_flag.

        """
        self.db.ingredients.append(ingredient)
        # normal mix
        self.at_mix(caller, ingredient, **kwargs)

        if len(self.db.ingredients) >= len(self.ingredient_recipe):
            # we have enough, check if it matches recipe

            if self.check_mixture():
                self.at_mix_success(caller, ingredient, **kwargs)
            else:
                self.room.log(
                    f"{self.name} mix failure: Tried {' + '.join([ing.key for ing in self.db.ingredients if ing])}"
                )
                self.db.ingredients = []
                self.at_mix_failure(caller, ingredient, **kwargs)

    def at_mix(self, caller, ingredient, **kwargs):
        self.msg_room(caller, f"~You ~mix {ingredient.key} into *{self.key}.")

    def at_mix_failure(self, caller, ingredient, **kwargs):
        self.msg_room(caller, f"This mix doesn't work. ~You ~clean and start over.")

    def at_mix_success(self, caller, ingredient, **kwargs):
        self.msg_room(caller, f"~You successfully ~complete the mix!")

class HasButtons(EvscaperoomObject):
    """
    Any object with buttons to push/press

    """

    # mapping keys/aliases to calling method
    buttons = {
        "green button": "at_green_button",
        "green": "at_green_button",
        "red button": "at_red_button",
        "red": "at_red_button",
    }

        helptxt = (
            "It looks like you should be able to operate "
            f"*{self.key} by means of "
            "{callsigns}."
        )
        return ["push", "press red/green button"], helptxt

    def at_focus_press(self, caller, **kwargs):
        arg = self.parse(kwargs["args"])
        callfunc_name = self.buttons.get(arg)
            getattr(self, callfunc_name)(caller)
        else:
            self.at_nomatch(caller)

    at_focus_push = at_focus_press

    def at_nomatch(self, caller):
        self.msg_char(caller, "That does not seem right.")

    def at_green_button(self, caller):
        self.msg_char(caller, "You press the green button.")

    def at_red_button(self, caller):
        self.msg_char(caller, "You press the red button.")

class CodeInput(EvscaperoomObject):
    """
    Any object where you can enter a code of some sort
    to have an effect happen.

    """

    # the code of this
    code = "PASSWORD"
    code_hint = "eight letters A-Z"
    case_insensitive = True
    # code locked no matter what is input
    infinitely_locked = False

    def at_focus_code(self, caller, **kwargs):
        args = self.parse(kwargs["args"].strip())

            self.at_no_code(caller)
            return
        if self.infinitely_locked:
            code_correct = False
        elif self.case_insensitive:
            code_correct = args.upper() == self.code.upper()
        else:
            code_correct = args == self.code

        if code_correct:
            self.at_code_correct(caller, args)
        else:
            self.at_code_incorrect(caller, args)

        helptxt = "Looks like you need to use {callsigns}."
        return ["code <code>"], helptxt

    def at_no_code(self, caller):
        self.msg_char(caller, f"Looks like you need to enter |w{self.code_hint}|n.")

    def at_code_correct(self, caller, code_tried):
        self.msg_char(caller, "That's the right code!")

    def at_code_incorrect(self, caller, code_tried):
        self.msg_char(caller, f"That's not the right code (need {self.code_hint}).")

class BasePositionable(EvscaperoomObject):
    """
    Any object a character can be positioned on. This is meant as an

    This is a little special since a char can only have one position at a
    time and must therefore be aware of the other 'positional' actions
    any object may support (otherwise you may end up sitting/standing/etc on
    more than one object at once!)

    We set a Attribute (obj, position) on the caller to indicate that
    they have a position on an object. This is necessary so as to not have
    the caller sit on more than one sittable object at a time, for example. The
    'positions' Attribute on this object holds a mapping of who is sitting
    lying etc on this object.  We don't add a limit to how many chars could
    have a position on an object - it's not realistic, but this goes with the
    philosophy that one character should not be able to block others if they go
    inactive etc.

    This state is also tied to the general 'stand' command, which should return
    the player to the normal standing state regardless of if they focus on this
    object or not.

    """

        # mapping {object: position}.

    def handle_position(self, caller, new_pos, **kwargs):
        """
        Wrap this with the at_focus_ method of the child class.

        """
        old_obj, old_pos = self.get_position(caller)
        if old_obj:
            if old_obj is self:
                if old_pos == new_pos:
                    self.at_again_position(caller, new_pos)
                else:
                    self.set_position(caller, new_pos)
                    self.at_position(caller, new_pos)
            else:
                self.at_cannot_position(caller, new_pos, old_obj, old_pos)
        else:
            self.set_position(caller, new_pos)
            self.at_position(caller, new_pos)

    def at_cannot_position(self, caller, position, old_obj, old_pos):
        self.msg_char(
            caller,
            f"You can't; you are currently {self.position_prep_map[old_pos]} on *{old_obj.key} "
            "(better |wstand|n first).",
        )

    def at_again_position(self, caller, position):
            caller, f"But you are already {self.position_prep_map[position]} on *{self.key}?"
        )

    def at_position(self, caller, position):
        self.msg_room(caller, f"~You ~{position} on *{self.key}.")

class Sittable(BasePositionable):
    """
    Any object you can sit on.

    """

    def at_focus_sit(self, caller, **kwargs):
        super().handle_position(caller, "sit", **kwargs)

class Liable(BasePositionable):
    """
    Any object you can lie down on.

    """

    def at_focus_lie(self, caller, **kwargs):
        super().handle_position(caller, "lie", **kwargs)

class Kneelable(BasePositionable):
    """
    Any object you can kneel on.

    """

    def at_focus_kneel(self, caller, **kwargs):
        super().handle_position(caller, "kneel", **kwargs)

class Climbable(BasePositionable):
    """
    Any object you can climb up to stand on. We name this
    'climb' so as to not collide with the general 'stand'
    command, which resets your position.

    """

    def at_focus_climb(self, caller, **kwargs):
        super().handle_position(caller, "climb", **kwargs)

class Positionable(Sittable, Liable, Kneelable, Climbable):
    """
    An object on which you can position yourself in one of the
    supported ways (sit, lie, kneel or climb)

    """

        txt = "It looks like you can {callsigns} on it."
        return ["sit", "lie", "kneel", "climb"], txt

"""
Room class and mechanics for the Evscaperoom.

This is a special room class that not only depicts the evscaperoom itself, it
also acts as a central store for the room state, score etc. When deleting this,
that particular escaperoom challenge should be gone.

"""

from evennia import DefaultCharacter, DefaultObject, DefaultRoom, logger, utils
from evennia.locks.lockhandler import check_lockstring
from evennia.utils.utils import lazy_property, list_to_string

from .commands import CmdSetEvScapeRoom
from .objects import EvscaperoomObject
from .state import StateHandler

class EvscapeRoom(EvscaperoomObject, DefaultRoom):
    """
    The room to escape from.

    """

        """
        Called once, when the room is first created.

        """

        # starting state
        self.db.state = None  # name
        self.db.prev_state = None

        # this is used for tagging of all objects belonging to this
        # particular room instance, so they can be cleaned up later
        # this is accessed through the .tagcategory getter.
        self.db.tagcategory = "evscaperoom_{}".format(self.key)

        # room progress statistics
        self.db.stats = {
            "progress": 0,  # in percent
            "score": {},  # reason: score
            "max_score": 100,
            "hints_used": 0,  # total across all states
            "hints_total": 41,
            "total_achievements": 14,
        }

        self.cmdset.add(CmdSetEvScapeRoom, persistent=True)

        self.log("Room created and log started.")

    def statehandler(self):
        return StateHandler(self)

    @property
    def state(self):
        return self.statehandler.current_state

    def log(self, message, caller=None):
        """
        Log to a file specificially for this room.
        """
        caller = f"[caller.key]: " if caller else ""

        logger.log_file(
            strip_ansi(f"{caller}{message.strip()}"), filename=self.tagcategory + ".log"
        )

    def score(self, new_score, reason):
        """
        We don't score individually but for everyone in room together.
        You can only be scored for a given reason once."""
        if reason not in self.db.stats["score"]:
            self.log(f"score: {reason} ({new_score}pts)")
            self.db.stats["score"][reason] = new_score

    def progress(self, new_progress):
        "Progress is what we set it to be (0-100%)"
        self.log(f"progress: {new_progress}%")
        self.db.stats["progress"] = new_progress

    def achievement(self, caller, achievement, subtext=""):
        """
        Give the caller a personal achievment. You will only
        ever get one of the same type

        Args:
            caller (Object): The receiver of the achievement.
            achievement (str): The title/name of the achievement.
            subtext (str, optional): Eventual subtext/explanation
                of the achievement.
        """
        achievements = caller.attributes.get("achievements", category=self.tagcategory)
        if not achievements:
            achievements = {}
        if achievement not in achievements:
            self.log(f"achievement: {caller} earned '{achievement}' - {subtext}")
            achievements[achievement] = subtext
            caller.attributes.add("achievements", achievements, category=self.tagcategory)

    def get_all_characters(self):
        """
        Get the player characters in the room.

        Returns:
            chars (Queryset): The characters.

        """
        return DefaultCharacter.objects.filter_family(db_location=self)

    def check_perm(self, caller, permission):
        return check_lockstring(caller, f"dummy:perm({permission})")

    def tag_character(self, character, tag, category=None):
        """
        Tag a given character in this room.

        Args:
            character (Character): Player character to tag.
            tag (str): Tag to set.
            category (str, optional): Tag-category. If unset, use room's
                tagcategory.

        """
        category = category if category else self.db.tagcategory
        character.tags.add(tag, category=category)

    def tag_all_characters(self, tag, category=None):
        """
        Set a given tag on all players in the room.

        Args:
            room (EvscapeRoom): The room to escape from.
            tag (str): The tag to set.
            category (str, optional): If unset, will use the room's tagcategory.

        """
        category = category if category else self.tagcategory

        for char in self.get_all_characters():
            char.tags.add(tag, category=category)

    def character_cleanup(self, char):
        """
        Clean all custom tags/attrs on a character.

        """
        if self.tagcategory:
            char.tags.remove(category=self.tagcategory)
            char.attributes.remove(category=self.tagcategory)

    def character_exit(self, char):
        """
        Have a character exit the room - return them to the room menu.

        """
        self.log(f"EXIT: {char} left room")

        self.character_cleanup(char)
        char.location = char.home

        # check if room should be deleted
        if len(self.get_all_characters()) < 1:

        # we must run menu after deletion so we don't include this room!
        run_evscaperoom_menu(char)

    def at_object_receive(self, moved_obj, source_location, move_type="move", **kwargs):
        """
        Called when an object arrives in the room. This can be used to
        sum up the situation, set tags etc.

        """
        if utils.inherits_from(moved_obj, "evennia.objects.objects.DefaultCharacter"):
            self.log(f"JOIN: {moved_obj} joined room")
            self.state.character_enters(moved_obj)

    def at_object_leave(self, moved_obj, target_location, move_type="move", **kwargs):
        """
        Called when an object leaves the room; if this is a Character we need
        to clean them up and move them to the menu state.

        """
            self.character_cleanup(moved_obj)
        if len(self.get_all_characters()) <= 1:
            # after this move there'll be no more characters in the room - delete the room!
            # logger.log_info("DEBUG: Don't delete room when last player leaving")

        """
        Delete this room and all items related to it. Only move the players.

        """
        self.db.deleting = True
            self.character_exit(char)
        for obj in self.contents:
            obj.delete()
        self.log("END: Room cleaned up and deleted")
        return super().delete()

        pos = (
            f"\n|x[{self.position_prep_map[pos]} on " f"{obj.get_display_name(looker)}]|n"
            if obj
            else ""
        )

        admin_only = ""
        if self.check_perm(looker, "Admin"):
            # only for admins
            objs = DefaultObject.objects.filter_family(db_location=self).exclude(id=looker.id)
            admin_only = "\n|xAdmin only: " + list_to_string(
                [obj.get_display_name(looker) for obj in objs]
            )

        return f"{self.db.desc}{pos}{admin_only}"

"""
A simple cleanup script to wipe empty rooms

(This can happen if users leave 'uncleanly', such as by closing their browser
window)

Just start this global script manually or at server creation.
"""

class CleanupScript(DefaultScript):
        self.key = "evscaperoom_cleanup"
        self.desc = "Cleans up empty evscaperooms"

        self.interval = 60 * 15

        for room in EvscapeRoom.objects.all():
            if not room.get_all_characters():
                # this room is empty
                room.log("END: Room cleaned by garbage collector.")
                room.delete()

"""
States represent the sequence of states the room goes through.

This module includes the BaseState class and the StateHandler
for managing states on the room.

The state handler operates on an Evscaperoom and changes
its state from one to another.

A given state is given as a module in states/ package. The
state is identified by its module name.

"""

from functools import wraps

from evennia import logger, utils

from .utils import create_evscaperoom_object, msg_cinematic, parse_for_things

# state setup
if hasattr(settings, "EVSCAPEROOM_STATE_PACKAGE"):
    _ROOMSTATE_PACKAGE = settings.EVSCAPEROOM_STATE_PACKAGE
else:
    _ROOMSTATE_PACKAGE = "evennia.contrib.full_systems.evscaperoom.states"
if hasattr(settings, "EVSCAPEROOM_START_STATE"):
    _FIRST_STATE = settings.EVSCAPEROOM_START_STATE
else:
    _FIRST_STATE = "state_001_start"

# handler for managing states on room

class StateHandler(object):
    """
    This sits on the room and is used to progress through the states.

    """

    def __init__(self, room):
        self.room = room
        self.current_state_name = room.db.state or _FIRST_STATE
        self.prev_state_name = room.db.prev_state
        self.current_state = None
        self.current_state = self.load_state(self.current_state_name)

    def load_state(self, statename):
        """
        Load state without initializing it
        """
        try:
            mod = utils.mod_import(f"{_ROOMSTATE_PACKAGE}.{statename}")
            self.room.msg_room(None, f"|rBUG: Could not load state {statename}: {err}!")
            self.room.msg_room(None, f"|rBUG: Falling back to {self.current_state_name}")
            return

        state = mod.State(self, self.room)
        return state

    def init_state(self):
        """
        Initialize a new state

        """
        self.current_state.init()

    def next_state(self, next_state=None):
        """
        Check if the current state is finished. This should be called whenever
        the players do actions that may affect the state of the room.

        Args:
            next_state (str, optional): If given, override the next_state given
                by the current state's check() method with this - this allows
                for branching paths (but the current state must still first agree
                that the check passes).

        Returns:
            state_changed (bool): True if the state changed, False otherwise.

        """
        # allows the state to enforce/customize what the next state should be
        next_state_name = self.current_state.next(next_state)
        if next_state_name:
            # we are ready to move on!

            next_state = self.load_state(next_state_name)
            if not next_state:
                raise RuntimeError(f"Could not load new state {next_state_name}!")

            self.prev_state_name = self.current_state_name
            self.current_state_name = next_state_name
            self.current_state.clean()
            self.prev_state = self.current_state
            self.current_state = next_state

            self.init_state()

            self.room.db.prev_state = self.prev_state_name
            self.room.db.state = self.current_state_name

# base state class

class BaseState(object):
    """
    Base object holding all callables for a state. This is here to
    allow easy overriding for child states.

    """

    next_state = "unset"
    # a sequence of hints to describe this state.
    hints = []

    def __init__(self, handler, room):
        """
        Initializer.

        Args:
            room (EvscapeRoom): The room tied to this state.
            handler (StateHandler): Back-reference to the handler
                storing this state.
        """
        self.handler = handler
        # the name is derived from the name of the module
        self.name = self.__class__.__module__

        return self.__class__.__module__

        return str(self)

    def _catch_errors(self, method):
        """
        Wrapper handling state method errors.

        """

        @wraps(method)
        def decorator(*args, **kwargs):
            try:
                return method(*args, **kwargs)
                logger.log_trace(f"Error in State {__name__}")
                    None,
                    f"|rThere was an unexpected error in State {__name__}. "
                    "Please |wreport|r this as an issue.|n",
                )
                raise  # TODO

        return decorator

    def __getattribute__(self, key):
        """
        Always wrap all callables in the error-handler

        """
        val = _GA(self, key)
        if callable(val):
            return _GA(self, "_catch_errors")(val)
        return val

    def get_hint(self):
        """
        Get a hint for how to solve this state.

        """
        hint_level = self.room.attributes.get("state_hint_level", default=-1)
        next_level = hint_level + 1
        if next_level < len(self.hints):
            # return the next hint in the sequence.
            self.room.db.state_hint_level = next_level
            self.room.db.stats["hints_used"] += 1
            self.room.log(
                f"HINT: {self.name.split('.')[-1]}, level {next_level + 1} "
                f"(total used: {self.room.db.stats['hints_used']})"
            )
            return self.hints[next_level]
        else:
            # no more hints for this state

    # helpers
    def msg(self, message, target=None, borders=False, cinematic=False):
        """
        Display messsage to everyone in room, or given target.
        """
        if cinematic:
            message = msg_cinematic(message, borders=borders)
        if target:
            options = target.attributes.get("options", category=self.room.tagcategory, default={})
            # we assume this is a char
            target.msg(parse_for_things(message, things_style=style))
        else:
            self.room.msg_room(None, message)

    def cinematic(self, message, target=None):
        """
        Display a 'cinematic' sequence - centered, with borders.
        """
        self.msg(message, target=target, borders=True, cinematic=True)

    def create_object(self, typeclass=None, key="testobj", location=None, **kwargs):
        """
        This is a convenience-wrapper for quickly building EvscapeRoom objects.

            typeclass (str): This can take just the class-name in the evscaperoom's
                objects.py module. Otherwise, a full path or the actual class
                is needed (for custom state objects, just give the class directly).
            key (str): Name of object.
            location (Object): If not given, this will be the current room.
            kwargs (any): Will be passed into create_object.
        Returns:
            new_obj (Object): The newly created object, if any.

        """
        if not location:
            location = self.room
        return create_evscaperoom_object(
            typeclass=typeclass,
            key=key,
            location=location,
            tags=[("room", self.room.tagcategory.lower())],
            **kwargs,
        )

    def get_object(self, key):
        """
        Find a named *non-character* object for this state in this room.

        Args:
            key (str): Object to search for.
        Returns:
            obj (Object): Object in the room.

        """
        match = EvscaperoomObject.objects.filter_family(
            db_key__iexact=key, db_tags__db_category=self.room.tagcategory.lower()
        )
            logger.log_err(f"get_object: No match for '{key}' in state ")
        return match[0]

    # state methods

    def init(self):
        """
        Initializes the state (usually by modifying the room in some way)

        """
        pass

    def clean(self):
        """
        Any cleanup operations after the state ends.

        """
        self.room.db.state_hint_level = -1

    def next(self, next_state=None):
        """
        Get the next state after this one.

        Args:
            next_state (str, optional): This allows the calling code
                to redirect to a different state than the 'default' one
                (creating branching paths in the game). Override this method
                to customize (by default the input will always override default
                set on the class)
        Returns:
            state_name (str or None): Name of next state to switch to. None
                to remain in this state. By default we check the room for the
                "finished" flag be set.
        """
        return next_state or self.next_state

    def character_enters(self, character):
        """
        Called when character enters the room in this state.

        """
        pass

    def character_leaves(self, character):
        """
        Called when character is whisked away (usually because of
        quitting). This method cannot influence the move itself; it
        happens just before room.character_cleanup()

        """
        pass

"""
Unit tests for the Evscaperoom

"""
import pkgutil
from os import path

from evennia.utils import mod_import

from . import commands, objects
from . import state as basestate
from . import utils

class TestEvscaperoomCommands(BaseEvenniaCommandTest):
        self.room1 = utils.create_evscaperoom_object("evscaperoom.room.EvscapeRoom", key="Testroom")
        self.obj1.location = self.room1

    def test_base_search(self):
        cmd = commands.CmdEvscapeRoom()
        cmd.caller = self.char1

        self.assertEqual((self.obj1, None), cmd._search("Obj", True))
        self.assertEqual((None, "Obj"), cmd._search("Obj", False))
        self.assertEqual((None, "Foo"), cmd._search("Foo", False))
        self.assertEqual((None, "Foo"), cmd._search("Foo", None))
        self.assertRaises(InterruptCommand, cmd._search, "Foo", True)

    def test_base_parse(self):

        cmd.obj1_search = None
        cmd.obj2_search = None
        cmd.args = "obj"
        cmd.parse()

        self.assertEqual(cmd.obj1, self.obj1)
        self.assertEqual(cmd.room, self.char1.location)

        cmd.obj1_search = False
        cmd.obj2_search = False

        self.assertEqual(cmd.arg1, "obj")
        self.assertEqual(cmd.obj1, None)

        self.assertEqual(cmd.arg1, None)
        self.assertEqual(cmd.arg2, None)

        cmd.obj1_search = True
        cmd.obj2_search = True
        cmd.args = "obj at obj"

        self.assertEqual(cmd.obj2, self.obj1)

        self.assertEqual(cmd.obj2, None)
        self.assertEqual(cmd.arg2, "obj")

        cmd.args = "foo in obj"

        self.assertEqual(cmd.arg1, "foo")

        cmd.args = "obj on foo"

        self.assertEqual(cmd.arg2, "foo")

        self.assertRaises(InterruptCommand, cmd.parse)

        cmd.args = "on obj"
        self.assertEqual(cmd.arg1, "")

    def test_set_focus(self):
        cmd.room = self.room1
        cmd.focus = self.obj1
            self.char1.attributes.get("focus", category=self.room1.tagcategory), self.obj1
        )

    def test_focus(self):
        # don't focus on a non-room object
        self.call(commands.CmdFocus(), "obj")
        self.assertEqual(self.char1.attributes.get("focus", category=self.room1.tagcategory), None)
        # should focus correctly
        myobj = utils.create_evscaperoom_object(
            objects.EvscaperoomObject, "mytestobj", location=self.room1
        )
        self.call(commands.CmdFocus(), "mytestobj")
        self.assertEqual(self.char1.attributes.get("focus", category=self.room1.tagcategory), myobj)

        self.call(commands.CmdLook(), "at obj", "Obj")
        self.call(commands.CmdLook(), "obj", "Obj")

    def test_speech(self):
        self.call(commands.CmdSpeak(), "", "What do you want to say?", cmdstring="")
        self.call(commands.CmdSpeak(), "Hello!", "You say: Hello!", cmdstring="")
        self.call(commands.CmdSpeak(), "", "What do you want to whisper?", cmdstring="whisper")
        self.call(commands.CmdSpeak(), "Hi.", "You whisper: (Hi.)", cmdstring="whisper")
        self.call(commands.CmdSpeak(), "HELLO!", "You shout: HELLO!", cmdstring="shout")

        self.call(commands.CmdSpeak(), "Hello", "You say: Hello", cmdstring="say")
        self.call(commands.CmdSpeak(), "Hello", "You shout: HELLO", cmdstring="shout")

    def test_emote(self):
            commands.CmdEmote(),
            "/me smiles to /obj",
            f"Char(#{self.char1.id}) smiles to Obj(#{self.obj1.id})",
        )

    def test_focus_interaction(self):
        self.call(commands.CmdFocusInteraction(), "", "Hm?")

class TestUtils(BaseEvenniaTest):
    def test_overwrite(self):
        room = utils.create_evscaperoom_object("evscaperoom.room.EvscapeRoom", key="Testroom")
        obj1 = utils.create_evscaperoom_object(
            objects.EvscaperoomObject, key="testobj", location=room
        )
        id1 = obj1.id

        obj2 = utils.create_evscaperoom_object(
        )
        id2 = obj2.id

        # we should have created a new object, deleting the old same-named one
        self.assertTrue(id1 != id2)
        self.assertFalse(bool(obj1.pk))
        self.assertTrue(bool(obj2.pk))

    def test_parse_for_perspectives(self):
        second, third = utils.parse_for_perspectives("~You ~look at the nice book", "TestGuy")
        self.assertTrue(second, "You look at the nice book")
        self.assertTrue(third, "TestGuy looks at the nice book")
        # irregular
        second, third = utils.parse_for_perspectives("With a smile, ~you ~were gone", "TestGuy")
        self.assertTrue(second, "With a smile, you were gone")
        self.assertTrue(third, "With a smile, TestGuy was gone")

    def test_parse_for_things(self):
        string = "Looking at *book and *key."
        self.assertEqual(utils.parse_for_things(string, 0), "Looking at book and key.")
        self.assertEqual(utils.parse_for_things(string, 1), "Looking at |ybook|n and |ykey|n.")
        self.assertEqual(utils.parse_for_things(string, 2), "Looking at |y[book]|n and |y[key]|n.")

class TestEvScapeRoom(BaseEvenniaTest):
        self.room = utils.create_evscaperoom_object(
            "evscaperoom.room.EvscapeRoom", key="Testroom", home=self.room1
        )
        self.roomtag = "evscaperoom_{}".format(self.room.key)

        self.room.delete()

    def test_room_methods(self):
        room = self.room
        self.char1.location = room

        self.assertEqual(room.tagcategory, self.roomtag)
        self.assertEqual(list(room.get_all_characters()), [self.char1])

        room.tag_character(self.char1, "opened_door")
        self.assertEqual(self.char1.tags.get("opened_door", category=self.roomtag), "opened_door")

        room.tag_all_characters("tagged_all")
        self.assertEqual(self.char1.tags.get("tagged_all", category=self.roomtag), "tagged_all")

        room.character_cleanup(self.char1)
        self.assertEqual(self.char1.tags.get(category=self.roomtag), None)

class TestStates(BaseEvenniaTest):
        )
        self.roomtag = "evscaperoom_#{}".format(self.room.id)

    def _get_all_state_modules(self):
        dirname = path.join(path.dirname(__file__), "states")
        states = []
        for imp, module, ispackage in pkgutil.walk_packages(
            path=[dirname], prefix="evennia.contrib.full_systems.evscaperoom.states."
        ):
            mod = mod_import(module)
            states.append(mod)
        return states

    def test_base_state(self):
        st = basestate.BaseState(self.room.statehandler, self.room)
        st.init()
        obj = st.create_object(objects.Edible, key="apple")
        self.assertEqual(obj.key, "apple")
        self.assertEqual(obj.__class__, objects.Edible)
        obj.delete()

    def test_all_states(self):
        "Tick through all defined states"

        for mod in self._get_all_state_modules():
            state = mod.State(self.room.statehandler, self.room)
            state.init()

            for obj in self.room.contents:
                if obj.pk:
                    methods = inspect.getmembers(obj, predicate=inspect.ismethod)
                    for name, method in methods:
                        if name.startswith("at_focus_"):
                            method(self.char1, args="dummy")

            next_state = state.next()
            self.assertEqual(next_state, mod.State.next_state)

"""
Helper functions and classes for the evscaperoom contrib.

Most of these are available directly from wrappers in state/object/room classes
and does not need to be imported from here.

"""

import re
from random import choice

from evennia import create_object, search_object
from evennia.utils import inherits_from, justify

_BASE_TYPECLASS_PATH = "evscaperoom.objects."
_RE_PERSPECTIVE = re.compile(r"~(\w+)", re.I + re.U + re.M)
_RE_THING = re.compile(r"\*(\w+)", re.I + re.U + re.M)

def create_evscaperoom_object(
    typeclass=None, key="testobj", location=None, delete_duplicates=True, **kwargs
):
    """
    This is a convenience-wrapper for quickly building EvscapeRoom objects. This
    is called from the helper-method create_object on states, but is also useful
    for the object-create admin command.

    Note that for the purpose of the Evscaperoom, we only allow one instance
    of each *name*, deleting the old version if it already exists.

        typeclass (str): This can take just the class-name in the evscaperoom's
            objects.py module. Otherwise, a full path is needed.
        key (str): Name of object.
        location (Object): The location to create new object.
        delete_duplicates (bool): Delete old object with same key.
        kwargs (any): Will be passed into create_object.
    Returns:
        new_obj (Object): The newly created object, if any.

    """
    if not (
        callable(typeclass)
        or typeclass.startswith("evennia")
        or typeclass.startswith("typeclasses")
        or typeclass.startswith("evscaperoom")
    ):
        # unless we specify a full typeclass path or the class itself,
        # auto-complete it
        typeclass = _BASE_TYPECLASS_PATH + typeclass

    if delete_duplicates:
        old_objs = [
            obj
            for obj in search_object(key)
            if not inherits_from(obj, "evennia.objects.objects.DefaultCharacter")
        ]
        if location:
            # delete only matching objects in the given location
            [obj.delete() for obj in old_objs if obj.location == location]
        else:
            [obj.delete() for obj in old_objs]

    new_obj = create_object(typeclass=typeclass, key=key, location=location, **kwargs)
    return new_obj

def create_fantasy_word(length=5, capitalize=True):
    """
    Create a random semi-pronouncable 'word'.

        length (int): The desired length of the 'word'.
        capitalize (bool): If the return should be capitalized or not
    Returns:
        word (str): The fictous word of given length.

    """
    if not length:
        return ""

    phonemes = (
        "ea oh ae aa eh ah ao aw ai er ey ow ia ih iy oy ua "
        "uh uw a e i u y p b t d f v t dh "
        "s z sh zh ch jh k ng g m n l r w"
    ).split()
    word = [choice(phonemes)]
    while len(word) < length:
        word.append(choice(phonemes))
    # it's possible to exceed length limit due to double consonants
    word = "".join(word)[:length]
    return word.capitalize() if capitalize else word

# special word mappings when going from 2nd person to 3rd
irregulars = {
    "were": "was",
    "are": "is",
    "mix": "mixes",
    "push": "pushes",
    "have": "has",
    "focus": "focuses",
}

def parse_for_perspectives(string, you=None):
    """
    Parse a string with special markers to produce versions both
    intended for the person doing the action ('you') and for those
    seeing the person doing that action. Also marks 'things'
    according to style. See example below.

    Args:
        string (str): String on 2nd person form with ~ markers ('~you ~open ...')
        you (str): What others should see instead of you (Bob opens)
    Returns:
        second, third_person (tuple): Strings replace to be shown in 2nd and 3rd person
            perspective
    Example:
        "~You ~open"
        ->  "You open", "Bob opens"
    """

    def _replace_third_person(match):
        match = match.group(1)
        lmatch = match.lower()
        if lmatch == "you":
            return "|c{}|n".format(you)
        elif lmatch in irregulars:
            if match[0].isupper():
                return irregulars[lmatch].capitalize()
            return irregulars[lmatch]
        elif lmatch[-1] == "s":
            return match + "es"
        else:
            return match + "s"  # simple, most normal form

    you = "They" if you is None else you

    first_person = _RE_PERSPECTIVE.sub(r"\1", string)
    third_person = _RE_PERSPECTIVE.sub(_replace_third_person, string)
    return first_person, third_person

def parse_for_things(string, things_style=2, clr="|y"):
    """
    Parse string for special *thing markers and decorate
    it.

    Args:
        string (str): The string to parse.
        things_style (int): The style to handle `*things` marked:
            0 - no marking (remove `*`)
            1 - mark with color
            2 - mark with color and [] (default)
        clr (str): Which color to use for marker..
    Example:
        You open *door -> You open [door].
    """
    if not things_style:
        # hardcore mode - no marking of focus targets
        return _RE_THING.sub(r"\1", string)
    elif things_style == 1:
        # only colors
        return _RE_THING.sub(r"{}\1|n".format(clr), string)
    else:
        # colors and brackets
        return _RE_THING.sub(r"{}[\1]|n".format(clr), string)

def add_msg_borders(text):
    "Add borders above/below text block"
    maxwidth = max(len(line) for line in text.split("\n"))
    sep = "|w" + "~" * maxwidth + "|n"
    text = f"{sep}\n{text}\n{sep}"
    return text

def msg_cinematic(text, borders=True):
    """
    Display a text as a 'cinematic' - centered and
    surrounded by borders.

    Args:
        text (str): Text to format.
        borders (bool, optional): Put borders above and below text.
    Returns:
        text (str): Pretty-formatted text.

    """
    text = text.strip()
    text = justify(text, align="c", indent=1)
    if borders:
        text = add_msg_borders(text)

"""
Barter system

Evennia contribution - Griatch 2012

This implements a full barter system - a way for players to safely
trade items between each other using code rather than simple free-form
talking.  The advantage of this is increased buy/sell safety but it
also streamlines the process and makes it faster when doing many
transactions (since goods are automatically exchanged once both
agree).

This system is primarily intended for a barter economy, but can easily
be used in a monetary economy as well -- just let the "goods" on one
side be coin objects (this is more flexible than a simple "buy"
command since you can mix coins and goods in your trade).

In this module, a "barter" is generally referred to as a "trade".

- Trade example

A trade (barter) action works like this: A and B are the parties.

1) opening a trade

A: trade B: Hi, I have a nice extra sword. You wanna trade?
B sees: A says: "Hi, I have a nice extra sword. You wanna trade?"
   A wants to trade with you. Enter 'trade A <emote>' to accept.
B: trade A: Hm, I could use a good sword ...
A sees: B says: "Hm, I could use a good sword ...
   B accepts the trade. Use 'trade help' for aid.
B sees: You are now trading with A. Use 'trade help' for aid.

2) negotiating

A: offer sword: This is a nice sword. I would need some rations in trade.
B sees: A says: "This is a nice sword. I would need some rations in trade."
   [A offers Sword of might.]
B evaluate sword
B sees: <Sword's description and possibly stats>
B: offer ration: This is a prime ration.
A sees: B says: "This is a prime ration."
  [B offers iron ration]
A: say Hey, this is a nice sword, I need something more for it.
B sees: A says: "Hey this is a nice sword, I need something more for it."
B: offer sword,apple: Alright. I will also include a magic apple. That's my last offer.
A sees: B says: "Alright, I will also include a magic apple. That's my last offer."
  [B offers iron ration and magic apple]
A accept: You are killing me here, but alright.
B sees: A says: "You are killing me here, but alright."
  [A accepts your offer. You must now also accept.]
B accept: Good, nice making business with you.
  You accept the deal. Deal is made and goods changed hands.
A sees: B says: "Good, nice making business with you."
  B accepts the deal. Deal is made and goods changed hands.

At this point the trading system is exited and the negotiated items
are automatically exchanged between the parties. In this example B was
the only one changing their offer, but also A could have changed their
offer until the two parties found something they could agree on. The
emotes are optional but useful for RP-heavy worlds.

- Technical info

The trade is implemented by use of a TradeHandler. This object is a
common place for storing the current status of negotiations. It is
created on the object initiating the trade, and also stored on the
other party once that party agrees to trade. The trade request times
out after a certain time - this is handled by a Script. Once trade
starts, the CmdsetTrade cmdset is initiated on both parties along with
the commands relevant for the trading.

- Ideas for NPC bartering:

This module is primarily intended for trade between two players. But
it can also in principle be used for a player negotiating with an
AI-controlled NPC. If the NPC uses normal commands they can use it
directly -- but more efficient is to have the NPC object send its
replies directly through the tradehandler to the player. One may want
to add some functionality to the decline command, so players can
decline specific objects in the NPC offer (decline <object>) and allow
the AI to maybe offer something else and make it into a proper
barter.  Along with an AI that "needs" things or has some sort of
personality in the trading, this can make bartering with NPCs at least
moderately more interesting than just plain 'buy'.

- Installation:

Just import the CmdTrade command into (for example) the default
cmdset. This will make the trade (or barter) command available
in-game.

"""

TRADE_TIMEOUT = 60  # timeout for B to accept trade

class TradeTimeout(DefaultScript):
    """
    This times out the trade request, in case player B did not reply in time.
    """

        """
        Called when script is first created
        """
        self.key = "trade_request_timeout"
        self.desc = "times out trade requests"
        self.interval = TRADE_TIMEOUT
        self.repeats = 1
        self.persistent = False

        """
        called once
        """
        if self.ndb.tradeevent:
            self.obj.ndb.tradeevent.finish(force=True)
        self.obj.msg("Trade request timed out.")

    def is_valid(self):
        """
        Only valid if the trade has not yet started
        """
        return self.obj.ndb.tradeevent and not self.obj.ndb.tradeevent.trade_started

class TradeHandler(object):
    """
    Objects of this class handles the ongoing trade, notably storing the current
    offers from each side and wether both have accepted or not.
    """

    def __init__(self, part_a, part_b):
        """
        Initializes the trade. This is called when part A tries to
        initiate a trade with part B. The trade will not start until
        part B repeats this command (B will then call the self.join()
        command)

        Args:
            part_a (object): The party trying to start barter.
            part_b (object): The party asked to barter.

        Notes:
            We also store the back-reference from the respective party
            to this object.

        """
        # parties
        self.part_a = part_a
        self.part_b = part_b

        self.part_a.cmdset.add(CmdsetTrade())
        self.trade_started = False
        self.part_a.ndb.tradehandler = self
        # trade variables
        self.part_a_offers = []
        self.part_b_offers = []
        self.part_a_accepted = False
        self.part_b_accepted = False

    def msg_other(self, sender, string):
        """
        Relay a message to the *other* party without needing to know
        which party that is. This allows the calling command to not
        have to worry about which party they are in the handler.

        Args:
            sender (object): One of A or B. The method will figure
                out the *other* party to send to.
            string (str): Text to send.
        """
        if self.part_a == sender:
            self.part_b.msg(string)
        elif self.part_b == sender:
            self.part_a.msg(string)
        else:
            # no match, relay to oneself
            sender.msg(string) if sender else self.part_a.msg(string)

    def get_other(self, party):
        """
        Returns the other party of the trade

        Args:
            party (object): One of the parties of the negotiation

        Returns:
            party_other (object): The other party, not the first party.

        """
        if self.part_a == party:
            return self.part_b
        if self.part_b == party:
            return self.part_a

    def join(self, part_b):
        """
        This is used once B decides to join the trade

        Args:
            part_b (object): The party accepting the barter.

        """
        if self.part_b == part_b:
            self.part_b.ndb.tradehandler = self
            self.part_b.cmdset.add(CmdsetTrade())
            self.trade_started = True

    def unjoin(self, part_b):
        """
        This is used if B decides not to join the trade.

        Args:
            part_b (object): The party leaving the barter.

        """
            self.finish(force=True)

    def offer(self, party, *args):
        """
        Change the current standing offer. We leave it up to the
        command to do the actual checks that the offer consists
        of real, valid, objects.

        Args:
            party (object): Who is making the offer
            args (objects or str): Offerings.

        """
        if self.trade_started:
            # reset accept statements whenever an offer changes
            self.part_a_accepted = False
            self.part_b_accepted = False
            if party == self.part_a:
                self.part_a_offers = list(args)
            elif party == self.part_b:
                self.part_b_offers = list(args)
            else:
                raise ValueError

    def list(self):
        """
        List current offers.

        Returns:
            offers (tuple): A tuple with two lists, (A_offers, B_offers).

        """
        return self.part_a_offers, self.part_b_offers

    def search(self, offername):
        """
        Search current offers.

        Args:
            offername (str or int): Object to search for, or its index in
                the list of offered items.

        Returns:
            offer (object): An object on offer, based on the search criterion.

        """
        all_offers = self.part_a_offers + self.part_b_offers
        if isinstance(offername, int):
            # an index to return
            if 0 <= offername < len(all_offers):
                return all_offers[offername]

        all_keys = [offer.key for offer in all_offers]
        try:
            imatch = all_keys.index(offername)
            return all_offers[imatch]
            for offer in all_offers:
                if offer.aliases.get(offername):
                    return offer

    def accept(self, party):
        """
        Accept the current offer.

        Args:
            party (object): The party accepting the deal.

        Returns:
            result (object): `True` if this closes the deal, `False`
                otherwise

        Notes:
            This will only close the deal if both parties have
            accepted independently. This is done by calling the
            `finish()` method.

        """
                self.part_a_accepted = True
                self.part_b_accepted = True
            else:
            return self.finish()  # try to close the deal

    def decline(self, party):
        """
        Decline the offer (or change one's mind).

        Args:
            party (object): Party declining the deal.

        Returns:
            did_decline (bool): `True` if there was really an
                `accepted` status to change, `False` otherwise.

        Notes:
            If previously having used the `accept` command, this
            function will only work as long as the other party has not
            yet accepted.

        """
                if self.part_a_accepted:
                    self.part_a_accepted = False
                if self.part_b_accepted:
                    self.part_b_accepted = False
            else:

    def finish(self, force=False):
        """
        Conclude trade - move all offers and clean up

        Args:
            force (bool, optional): Force cleanup regardless of if the
                trade was accepted or not (if not, no goods will change
                hands but trading will stop anyway)
        Returns:
            result (bool): If the finish was successful.

        """
        fin = False
        if self.trade_started and self.part_a_accepted and self.part_b_accepted:
            # both accepted - move objects before cleanup
            for obj in self.part_a_offers:
                obj.location = self.part_b
            for obj in self.part_b_offers:
                obj.location = self.part_a
            fin = True
        if fin or force:
            # cleanup
            self.part_a.cmdset.delete("cmdset_trade")
            self.part_b.cmdset.delete("cmdset_trade")
            self.part_a_offers = None
            self.part_b_offers = None
            self.part_a.scripts.stop("trade_request_timeout")
            # this will kill it also from B
            del self.part_a.ndb.tradehandler
            if self.part_b.ndb.tradehandler:
                del self.part_b.ndb.tradehandler

# trading commands (will go into CmdsetTrade, initialized by the
# CmdTrade command further down).

class CmdTradeBase(Command):
    """
    Base command for Trade commands to inherit from. Implements the
    custom parsing.
    """

        """
        Parse the relevant parts and make it easily
        available to the command
        """
        self.tradehandler = self.caller.ndb.tradehandler
        self.part_a = self.tradehandler.part_a
        self.part_b = self.tradehandler.part_b

        self.other = self.tradehandler.get_other(self.caller)
        self.msg_other = self.tradehandler.msg_other

        self.trade_started = self.tradehandler.trade_started
        self.emote = ""
        self.str_caller = "Your trade action: %s"
        self.str_other = "%s:s trade action: " % self.caller.key + "%s"
            self.args, self.emote = [part.strip() for part in self.args.rsplit(":", 1)]
            self.str_caller = 'You say, "' + self.emote + '"\n  [%s]'
            if self.caller.has_account:
                self.str_other = '|c%s|n says, "' % self.caller.key + self.emote + '"\n  [%s]'
            else:
                self.str_other = '%s says, "' % self.caller.key + self.emote + '"\n  [%s]'

# trade help

class CmdTradeHelp(CmdTradeBase):
    """
    help command for the trade system.

    Usage:
        trade help

    Displays help for the trade commands.
    """

    key = "trade help"
    help_category = "Trade"

        """Show the help"""
        Trading commands

        |woffer <objects> [:emote]|n
            offer one or more objects for trade. The emote can be used for
            RP/arguments. A new offer will require both parties to re-accept
            it again.
        |waccept [:emote]|n
            accept the currently standing offer from both sides. Also 'agree'
            works. Once both have accepted, the deal is finished and goods
            will change hands.
        |wdecline [:emote]|n
            change your mind and remove a previous accept (until other
            has also accepted)
        |wstatus|n
            show the current offers on each side of the deal. Also 'offers'
            and 'deal' works.
        |wevaluate <nr> or <offer>|n
            examine any offer in the deal. List them with the 'status' command.
        |wend trade|n
            end the negotiations prematurely. No trade will take place.

        You can also use |wemote|n, |wsay|n etc to discuss
        without making a decision or offer.
        """

# offer

class CmdOffer(CmdTradeBase):
    """
    offer one or more items in trade.

    Usage:
      offer <object> [, object2, ...][:emote]

    Offer objects in trade. This will replace the currently
    standing offer.
    """

    key = "offer"
    help_category = "Trading"

        """implement the offer"""

            caller.msg("Usage: offer <object> [, object2, ...] [:emote]")
            return
        if not self.trade_started:
            caller.msg("Wait until the other party has accepted to trade with you.")
            return

        # gather all offers
        offers = [part.strip() for part in self.args.split(",")]
        offerobjs = []
        for offername in offers:
            obj = caller.search(offername)
            if not obj:
                return
            offerobjs.append(obj)
        self.tradehandler.offer(self.caller, *offerobjs)

        # output
        if len(offerobjs) > 1:
            objnames = (
                ", ".join("|w%s|n" % obj.key for obj in offerobjs[:-1])
                + " and |w%s|n" % offerobjs[-1].key
            )
        else:
            objnames = "|w%s|n" % offerobjs[0].key

        caller.msg(self.str_caller % ("You offer %s" % objnames))
        self.msg_other(caller, self.str_other % ("They offer %s" % objnames))

# accept

class CmdAccept(CmdTradeBase):
    """
    accept the standing offer

    Usage:
      accept [:emote]
      agreee [:emote]

    This will accept the current offer. The other party must also accept
    for the deal to go through. You can use the 'decline' command to change
    your mind as long as the other party has not yet accepted. You can inspect
    the current offer using the 'offers' command.
    """

    key = "accept"
    aliases = ["agree"]

        """accept the offer"""
            return
        if self.tradehandler.accept(self.caller):
            # deal finished. Trade ended and cleaned.
                self.str_caller
                % "You |gaccept|n the deal. |gDeal is made and goods changed hands.|n"
            )
            self.msg_other(
                caller,
                self.str_other % "%s |gaccepts|n the deal."
                " |gDeal is made and goods changed hands.|n" % caller.key,
            )
        else:
            # a one-sided accept.
                % "You |Gaccept|n the offer. %s must now also accept."
                % self.other.key
            )
                caller,
                self.str_other % "%s |Gaccepts|n the offer. You must now also accept." % caller.key,
            )

# decline

class CmdDecline(CmdTradeBase):
    """
    decline the standing offer

    Usage:
      decline [:emote]

    This will decline a previously 'accept'ed offer (so this allows you to
    change your mind). You can only use this as long as the other party
    has not yet accepted the deal. Also, changing the offer will automatically
    decline the old offer.
    """

    key = "decline"

        """decline the offer"""
            return
        offer_a, offer_b = self.tradehandler.list()
        if not offer_a or not offer_b:
            caller.msg("No offers have been made yet, so there is nothing to decline.")
            return
        if self.tradehandler.decline(self.caller):
            # changed a previous accept
            caller.msg(self.str_caller % "You change your mind, |Rdeclining|n the current offer.")
                caller,
                self.str_other
                % "%s changes their mind, |Rdeclining|n the current offer."
                % caller.key,
            )
        else:
            # no acceptance to change
            caller.msg(self.str_caller % "You |Rdecline|n the current offer.")
            self.msg_other(caller, self.str_other % "%s declines the current offer." % caller.key)

# evaluate

# Note: This version only shows the description. If your particular game
# lists other important properties of objects (such as weapon damage, weight,
# magical properties, ammo requirements or whatnot), then you need to add this
# here.

class CmdEvaluate(CmdTradeBase):
    """
    evaluate objects on offer

    Usage:
      evaluate <offered object>

    This allows you to examine any object currently on offer, to
    determine if it's worth your while.
    """

    key = "evaluate"
    aliases = ["eval"]

        """evaluate an object"""
            caller.msg("Usage: evaluate <offered object>")
            return
        # we also accept indices
        try:
            ind = int(self.args)
            self.args = ind - 1
            # not a valid index - ignore
            pass

        offer = self.tradehandler.search(self.args)
        if not offer:
            caller.msg("No offer matching '%s' was found." % self.args)
            return
        # show the description
        caller.msg(offer.db.desc)

# status

class CmdStatus(CmdTradeBase):
    """
    show a list of the current deal

    Usage:
      status
      deal
      offers

    Shows the currently suggested offers on each sides of the deal. To
    accept the current deal, use the 'accept' command. Use 'offer' to
    change your deal. You might also want to use 'say', 'emote' etc to
    try to influence the other part in the deal.
    """

    key = "status"
    aliases = ["offers", "deal"]

        """Show the current deal"""
        part_a_offers, part_b_offers = self.tradehandler.list()
        count = 1
        part_a_offerlist = []
        for offer in part_a_offers:
            part_a_offerlist.append("\n |w%i|n %s" % (count, offer.key))
            count += 1
        if not part_a_offerlist:
            part_a_offerlist = "\n <nothing>"
        part_b_offerlist = []
        for offer in part_b_offers:
            part_b_offerlist.append("\n |w%i|n %s" % (count, offer.key))
            count += 1
        if not part_b_offerlist:
            part_b_offerlist = "\n <nothing>"

        string = "|gOffered by %s:|n%s\n|yOffered by %s:|n%s" % (
            self.part_a.key,
            "".join(part_a_offerlist),
            self.part_b.key,
            "".join(part_b_offerlist),
        )
        accept_a = self.tradehandler.part_a_accepted and "|gYes|n" or "|rNo|n"
        accept_b = self.tradehandler.part_b_accepted and "|gYes|n" or "|rNo|n"
        string += "\n\n%s agreed: %s, %s agreed: %s" % (
            accept_a,
            accept_b,
        )
        string += "\n Use 'offer', 'eval' and 'accept'/'decline' to trade. See also 'trade help'."

# finish

class CmdFinish(CmdTradeBase):
    """
    end the trade prematurely

    Usage:
      end trade [:say]
      finish trade [:say]

    This ends the trade prematurely. No trade will take place.

    """

    key = "end trade"
    aliases = "finish trade"

        """end trade"""
        self.tradehandler.finish(force=True)
        caller.msg(self.str_caller % "You |raborted|n trade. No deal was made.")
        self.msg_other(
            caller, self.str_other % "%s |raborted|n trade. No deal was made." % caller.key
        )

# custom Trading cmdset

class CmdsetTrade(CmdSet):
    """
    This cmdset is added when trade is initated. It is handled by the
    trade event handler.
    """

    key = "cmdset_trade"

        """Called when cmdset is created"""
        self.add(CmdTradeHelp())
        self.add(CmdOffer())
        self.add(CmdAccept())
        self.add(CmdDecline())
        self.add(CmdEvaluate())
        self.add(CmdStatus())
        self.add(CmdFinish())

# access command - once both have given this, this will create the
# trading cmdset to start trade.

class CmdTrade(Command):
    """
    Initiate trade with another party

    Usage:
      trade <other party> [:say]
      trade <other party> accept [:say]
      trade <other party> decline [:say]

    Initiate trade with another party. The other party needs to repeat
    this command with trade accept/decline within a minute in order to
    properly initiate the trade action. You can use the decline option
    yourself if you want to retract an already suggested trade. The
    optional say part works like the say command and allows you to add
    info to your choice.
    """

    key = "trade"
    aliases = ["barter"]

        """Initiate trade"""

            if self.caller.ndb.tradehandler and self.caller.ndb.tradeevent.trade_started:
                self.caller.msg("You are already in a trade. Use 'end trade' to abort it.")
            else:
                self.caller.msg("Usage: trade <other party> [accept|decline] [:emote]")
            return

        # handle the emote manually here
        selfemote = ""
        theiremote = ""
            self.args, emote = [part.strip() for part in self.args.rsplit(":", 1)]
            selfemote = 'You say, "%s"\n  ' % emote
                theiremote = '|c%s|n says, "%s"\n  ' % (self.caller.key, emote)
            else:
                theiremote = '%s says, "%s"\n  ' % (self.caller.key, emote)

        # for the sake of this command, the caller is always part_a; this
        # might not match the actual name in tradehandler (in the case of
        # using this command to accept/decline a trade invitation).
        part_a = self.caller
        accept = "accept" in self.args
        decline = "decline" in self.args
        if accept:
            part_b = self.args.rstrip("accept").strip()
        elif decline:
            part_b = self.args.rstrip("decline").strip()
        else:
            part_b = self.args
        part_b = self.caller.search(part_b)
        if not part_b:
            return
        if part_a == part_b:
            part_a.msg("You play trader with yourself.")
            return

        # messages
        str_init_a = "You ask to trade with %s. They need to accept within %s secs."
        str_init_b = "%s wants to trade with you. Use |wtrade %s accept/decline [:emote]|n to answer (within %s secs)."
        str_noinit_a = "%s declines the trade"
        str_noinit_b = "You decline trade with %s."
        str_start_a = "%s starts to trade with you. See |wtrade help|n for aid."
        str_start_b = "You start to trade with %s. See |wtrade help|n for aid."

        if not (accept or decline):
            # initialization of trade
            if self.caller.ndb.tradehandler:
                # trying to start trade without stopping a previous one
                if self.caller.ndb.tradehandler.trade_started:
                    string = "You are already in trade with %s. You need to end trade first."
                else:
                    string = "You are already trying to initiate trade with %s. You need to decline that trade first."
                self.caller.msg(string % part_b.key)
            elif part_b.ndb.tradehandler and part_b.ndb.tradehandler.part_b == part_a:
                # this is equivalent to part_a accepting a trade from part_b (so roles are reversed)
                part_b.ndb.tradehandler.join(part_a)
                part_b.msg(theiremote + str_start_a % part_a.key)
                part_a.msg(selfemote + str_start_b % part_b.key)
            else:
                # initiate a new trade
                TradeHandler(part_a, part_b)
                part_a.msg(selfemote + str_init_a % (part_b.key, TRADE_TIMEOUT))
                part_b.msg(theiremote + str_init_b % (part_a.key, part_a.key, TRADE_TIMEOUT))
                part_a.scripts.add(TradeTimeout)
            return
        elif accept:
            # accept a trade proposal from part_b (so roles are reversed)
            if part_a.ndb.tradehandler:
                # already in a trade
                part_a.msg(
                    "You are already in trade with %s. You need to end that first." % part_b.key
                )
                return
            if part_b.ndb.tradehandler.join(part_a):
            else:
                part_a.msg("No trade proposal to accept.")
            return
        else:
            # decline trade proposal from part_b (so roles are reversed)
            if part_a.ndb.tradehandler and part_a.ndb.tradehandler.part_b == part_a:
                # stopping an invite
                part_a.ndb.tradehandler.finish(force=True)
                part_b.msg(theiremote + "%s aborted trade attempt with you." % part_a)
                part_a.msg(selfemote + "You aborted the trade attempt with %s." % part_b)
            elif part_b.ndb.tradehandler and part_b.ndb.tradehandler.unjoin(part_a):
                part_b.msg(theiremote + str_noinit_a % part_a.key)
                part_a.msg(selfemote + str_noinit_b % part_b.key)
            else:
                part_a.msg("No trade proposal to decline.")
            return

"""
Test the contrib barter system
"""

from evennia.utils.create import create_object

from . import barter

class TestBarter(BaseEvenniaCommandTest):
        self.tradeitem1 = create_object(key="TradeItem1", location=self.char1)
        self.tradeitem2 = create_object(key="TradeItem2", location=self.char1)
        self.tradeitem3 = create_object(key="TradeItem3", location=self.char2)

    def test_tradehandler_base(self):
        # test all methods of the tradehandler
        handler = barter.TradeHandler(self.char1, self.char2)
        self.assertEqual(handler.part_a, self.char1)
        self.assertEqual(handler.part_b, self.char2)
        handler.msg_other(self.char1, "Want to trade?")
        handler.msg_other(self.char2, "Yes!")
        handler.msg_other(None, "Talking to myself...")
        self.assertEqual(self.char2.msg.mock_calls[0][1][0], "Want to trade?")
        self.assertEqual(self.char1.msg.mock_calls[0][1][0], "Yes!")
        self.assertEqual(self.char1.msg.mock_calls[1][1][0], "Talking to myself...")
        self.assertEqual(handler.get_other(self.char1), self.char2)
        handler.finish(force=True)

    def test_tradehandler_joins(self):
        self.assertTrue(handler.join(self.char2))
        self.assertTrue(handler.unjoin(self.char2))
        self.assertFalse(handler.join(self.char1))
        self.assertFalse(handler.unjoin(self.char1))

    def test_tradehandler_offers(self):
        handler.join(self.char2)
        handler.offer(self.char1, self.tradeitem1, self.tradeitem2)
        self.assertEqual(handler.part_a_offers, [self.tradeitem1, self.tradeitem2])
        self.assertFalse(handler.part_a_accepted)
        self.assertFalse(handler.part_b_accepted)
        handler.offer(self.char2, self.tradeitem3)
        self.assertEqual(handler.list(), ([self.tradeitem1, self.tradeitem2], [self.tradeitem3]))
        self.assertEqual(handler.search("TradeItem2"), self.tradeitem2)
        self.assertEqual(handler.search("TradeItem3"), self.tradeitem3)
        self.assertEqual(handler.search("nonexisting"), None)
        self.assertFalse(handler.finish())  # should fail since offer not yet accepted
        handler.accept(self.char1)
        handler.decline(self.char1)
        handler.accept(self.char2)
        handler.accept(self.char1)  # should trigger handler.finish() automatically
        self.assertEqual(self.tradeitem1.location, self.char2)
        self.assertEqual(self.tradeitem2.location, self.char2)
        self.assertEqual(self.tradeitem3.location, self.char1)

    def test_cmdtrade(self):
            barter.CmdTrade(),
            "Char2 : Hey wanna trade?",
            'You say, "Hey wanna trade?"',
            caller=self.char1,
        )
        self.call(barter.CmdTrade(), "Char decline : Nope!", 'You say, "Nope!"', caller=self.char2)
        )
        self.call(barter.CmdTrade(), "Char accept : Sure!", 'You say, "Sure!"', caller=self.char2)
            barter.CmdOffer(),
            "TradeItem3",
            "Your trade action: You offer TradeItem3",
        )
            "TradeItem1 : Here's my offer.",
            'You say, "Here\'s my offer."\n  [You offer TradeItem1]',
        )
            barter.CmdAccept(),
            "",
            "Your trade action: You accept the offer. Char2 must now also accept",
        )
            barter.CmdDecline(),
            "",
            "Your trade action: You change your mind, declining the current offer.",
        )
            ": Sounds good.",
            'You say, "Sounds good."\n' "  [You accept the offer. Char must now also accept.",
        )
            ":No way!",
            'You say, "No way!"\n  [You change your mind, declining the current offer.]',
        )
            "TradeItem1, TradeItem2 : My final offer!",
            'You say, "My final offer!"\n  [You offer TradeItem1 and TradeItem2]',
        )
            "",
            "Your trade action: You accept the offer. Char2 must now also accept.",
        )
        self.call(barter.CmdStatus(), "", "Offered by Char:", caller=self.char2)
        self.tradeitem1.db.desc = "A great offer."
        self.call(barter.CmdEvaluate(), "TradeItem1", "A great offer.")
            ":Ok then.",
            'You say, "Ok then."\n  [You accept the deal.',
        )

    def test_cmdtradehelp(self):
        )
        self.call(barter.CmdTradeHelp(), "", "Trading commands\n", caller=self.char1)
            barter.CmdFinish(),
            ": Ending.",
            'You say, "Ending."\n  [You aborted trade. No deal was made.]',
        )

"""
Clothing - Provides a typeclass and commands for wearable clothing,
which is appended to a character's description when worn.

Evennia contribution - Tim Ashley Jenkins 2017

Clothing items, when worn, are added to the character's description
in a list. For example, if wearing the following clothing items:

    a thin and delicate necklace
    a pair of regular ol' shoes
    one nice hat
    a very pretty dress

A character's description may look like this:

    Superuser(#1)
    This is User #1.

    Superuser is wearing one nice hat, a thin and delicate necklace,
    a very pretty dress and a pair of regular ol' shoes.

Characters can also specify the style of wear for their clothing - I.E.
to wear a scarf 'tied into a tight knot around the neck' or 'draped
loosely across the shoulders' - to add an easy avenue of customization.
For example, after entering:

    wear scarf draped loosely across the shoulders

The garment appears like so in the description:

    Superuser is wearing a fanciful-looking scarf draped loosely
    across the shoulders.

Items of clothing can be used to cover other items, and many options
are provided to define your own clothing types and their limits and
behaviors. For example, to have undergarments automatically covered
by outerwear, or to put a limit on the number of each type of item
that can be worn. The system as-is is fairly freeform - you
can cover any garment with almost any other, for example - but it
can easily be made more restrictive, and can even be tied into a
system for armor or other equipment.

To install, import this module and have your default character
inherit from ClothedCharacter in your game's characters.py file:

    from evennia.contrib.game_systems.clothing import ClothedCharacter

    class Character(ClothedCharacter):

And then add ClothedCharacterCmdSet in your character set in your
game's commands/default_cmdsets.py:

    from evennia.contrib.game_systems.clothing import ClothedCharacterCmdSet

         ...
         at_cmdset_creation(self):

             super().at_cmdset_creation()
             ...
             self.add(ClothedCharacterCmdSet)    # <-- add this

From here, you can use the default builder commands to create clothes
with which to test the system:

    @create a pretty shirt : evennia.contrib.game_systems.clothing.ContribClothing
    @set shirt/clothing_type = 'top'
    wear shirt

"""

from evennia import DefaultCharacter, DefaultObject, default_cmds
from evennia.utils import at_search_result, evtable, inherits_from, iter_to_str

# Options start here.
# Maximum character length of 'wear style' strings, or None for unlimited.
WEARSTYLE_MAXLENGTH = getattr(settings, "CLOTHING_WEARSTYLE_MAXLENGTH", 50)

# The rest of these options have to do with clothing types. ContribClothing types are optional,
# but can be used to give better control over how different items of clothing behave. You
# can freely add, remove, or change clothing types to suit the needs of your game and use
# the options below to affect their behavior.

# The order in which clothing types appear on the description. Untyped clothing or clothing
# with a type not given in this list goes last.
CLOTHING_TYPE_ORDER = getattr(
    settings,
    "CLOTHING_TYPE_ORDERED",
    [
        "hat",
        "jewelry",
        "top",
        "undershirt",
        "gloves",
        "fullbody",
        "bottom",
        "underpants",
        "socks",
        "shoes",
        "accessory",
    ],
)
# The maximum number of each type of clothes that can be worn. Unlimited if untyped or not specified.
CLOTHING_TYPE_LIMIT = getattr(
    settings, "CLOTHING_TYPE_LIMIT", {"hat": 1, "gloves": 1, "socks": 1, "shoes": 1}
)
# The maximum number of clothing items that can be worn, or None for unlimited.
CLOTHING_OVERALL_LIMIT = getattr(settings, "CLOTHING_OVERALL_LIMIT", 20)
# What types of clothes will automatically cover what other types of clothes when worn.
# Note that clothing only gets auto-covered if it's already worn when you put something
# on that auto-covers it - for example, it's perfectly possible to have your underpants
# showing if you put them on after your pants!
CLOTHING_TYPE_AUTOCOVER = getattr(
    settings,
    "CLOTHING_TYPE_AUTOCOVER",
    {
        "top": ["undershirt"],
        "bottom": ["underpants"],
        "fullbody": ["undershirt", "underpants"],
        "shoes": ["socks"],
    },
)
# Types of clothes that can't be used to cover other clothes.
CLOTHING_TYPE_CANT_COVER_WITH = getattr(settings, "CLOTHING_TYPE_AUTOCOVER", ["jewelry"])

# HELPER FUNCTIONS START HERE
def order_clothes_list(clothes_list):
    """
    Orders a given clothes list by the order specified in CLOTHING_TYPE_ORDER.

    Args:
        clothes_list (list): List of clothing items to put in order

    Returns:
        ordered_clothes_list (list): The same list as passed, but re-ordered
                                     according to the hierarchy of clothing types
                                     specified in CLOTHING_TYPE_ORDER.
    """
    ordered_clothes_list = clothes_list
    # For each type of clothing that exists...
    for current_type in reversed(CLOTHING_TYPE_ORDER):
        # Check each item in the given clothes list.
        for clothes in clothes_list:
            # If the item has a clothing type...
            if clothes.db.clothing_type:
                item_type = clothes.db.clothing_type
                # And the clothing type matches the current type...
                if item_type == current_type:
                    # Move it to the front of the list!
                    ordered_clothes_list.remove(clothes)
                    ordered_clothes_list.insert(0, clothes)
    return ordered_clothes_list

def get_worn_clothes(character, exclude_covered=False):
    """
    Get a list of clothes worn by a given character.

    Args:
        character (obj): The character to get a list of worn clothes from.

        exclude_covered (bool): If True, excludes clothes covered by other
                                clothing from the returned list.

    Returns:
        ordered_clothes_list (list): A list of clothing items worn by the
                                     given character, ordered according to
                                     the CLOTHING_TYPE_ORDER option specified
                                     in this module.
    """
    clothes_list = []
    for thing in character.contents:
        # If uncovered or not excluding covered items
        if not thing.db.covered_by or exclude_covered is False:
            # If 'worn' is True, add to the list
            if thing.db.worn:
                clothes_list.append(thing)
    # Might as well put them in order here too.
    ordered_clothes_list = order_clothes_list(clothes_list)

def clothing_type_count(clothes_list):
    """
    Returns a dictionary of the number of each clothing type
    in a given list of clothing objects.

    Args:
        clothes_list (list): A list of clothing items from which
                             to count the number of clothing types
                             represented among them.

    Returns:
        types_count (dict): A dictionary of clothing types represented
                            in the given list and the number of each
                            clothing type represented.
    """
    types_count = {}
    for garment in clothes_list:
        if garment.db.clothing_type:
            type = garment.db.clothing_type
            if type not in list(types_count.keys()):
                types_count[type] = 1
            else:
                types_count[type] += 1
    return types_count

def single_type_count(clothes_list, type):
    """
    Returns an integer value of the number of a given type of clothing in a list.

    Args:
        clothes_list (list): List of clothing objects to count from
        type (str): Clothing type to count

    Returns:
        type_count (int): Number of garments of the specified type in the given
                          list of clothing objects
    """
    type_count = 0
            if garment.db.clothing_type == type:
                type_count += 1
    return type_count

class ContribClothing(DefaultObject):
    def wear(self, wearer, wearstyle, quiet=False):
        """
        Sets clothes to 'worn' and optionally echoes to the room.

        Args:
            wearer (obj): character object wearing this clothing object
            wearstyle (True or str): string describing the style of wear or True for none

            quiet (bool): If false, does not message the room

        Notes:
            Optionally sets db.worn with a 'wearstyle' that appends a short passage to
            the end of the name  of the clothing to describe how it's worn that shows
            up in the wearer's desc - I.E. 'around his neck' or 'tied loosely around
            her waist'. If db.worn is set to 'True' then just the name will be shown.
        """
        # Set clothing as worn
        self.db.worn = wearstyle
        # Auto-cover appropriate clothing types
        to_cover = []
        if clothing_type := self.db.clothing_type:
            if autocover_types := CLOTHING_TYPE_AUTOCOVER.get(clothing_type):
                to_cover.extend(
                    [
                        garment
                        for garment in get_worn_clothes(wearer)
                        if garment.db.clothing_type in autocover_types
                    ]
                )
        for garment in to_cover:
            garment.db.covered_by = self

        # Echo a message to the room
            if type(wearstyle) is str:
                message = f"$You() $conj(wear) {self.name} {wearstyle}"
            else:
                message = f"$You() $conj(put) on {self.name}"
            if to_cover:
                message += f", covering {iter_to_str(to_cover)}"
            wearer.location.msg_contents(message + ".", from_obj=wearer)

    def remove(self, wearer, quiet=False):
        """
        Removes worn clothes and optionally echoes to the room.

        Args:

        """
        self.db.worn = False
        uncovered_list = []

        # Check to see if any other clothes are covered by this object.
        for thing in wearer.contents:
            if thing.db.covered_by == self:
                thing.db.covered_by = False
                uncovered_list.append(thing.name)
            remove_message = f"$You() $conj(remove) {self.name}"
            if len(uncovered_list) > 0:
                remove_message += f", revealing {iter_to_str(uncovered_list)}"
            wearer.location.msg_contents(remove_message + ".", from_obj=wearer)

    def at_get(self, getter):
        """
        Makes absolutely sure clothes aren't already set as 'worn'
        when they're picked up, in case they've somehow had their
        location changed without getting removed.
        """

    def at_pre_move(self, destination, **kwargs):
        """
        Called just before starting to move this object to
        destination. Return False to abort move.

        Notes:
            If this method returns False/None, the move is cancelled
            before it is even started.
        """
        # Covered clothing cannot be removed, dropped, or otherwise relocated
        if self.db.covered_by:

class ClothedCharacter(DefaultCharacter):
    """
    Character that displays worn clothing when looked at. You can also
    just copy the return_appearance hook defined below to your own game's
    character typeclass.
    """

    def get_display_desc(self, looker, **kwargs):
        """
        Get the 'desc' component of the object description. Called by `return_appearance`.

        Args:
            looker (Object): Object doing the looking.
            **kwargs: Arbitrary data for use when overriding.
        Returns:
            str: The desc display string.
        """
        desc = self.db.desc

        outfit_list = []
        # Append worn, uncovered clothing to the description
        for garment in get_worn_clothes(self, exclude_covered=True):
            wearstyle = garment.db.worn
                outfit_list.append(f"{garment.name} {wearstyle}")
            else:
                outfit_list.append(garment.name)

        # Create outfit string
        if outfit_list:
            outfit = (
                f"{self.get_display_name(looker, **kwargs)} is wearing {iter_to_str(outfit_list)}."
            )
        else:
            outfit = f"{self.get_display_name(looker, **kwargs)} is wearing nothing."

        # Add on to base description
        if desc:
            desc += f"\n\n{outfit}"
        else:
            desc = outfit

        return desc

    def get_display_things(self, looker, **kwargs):
        """
        Get the 'things' component of the object's contents. Called by `return_appearance`.

        Args:
        Returns:
            str: A string describing the things in object.
        """

        def _filter_visible(obj_list):
            return (
                obj
                for obj in obj_list
                if obj != looker and obj.access(looker, "view") and not obj.db.worn
            )

        # sort and handle same-named things
        things = _filter_visible(self.contents_get(content_type="object"))

        grouped_things = defaultdict(list)
        for thing in things:
            grouped_things[thing.get_display_name(looker, **kwargs)].append(thing)

        thing_names = []
        for thingname, thinglist in sorted(grouped_things.items()):
            nthings = len(thinglist)
            thing = thinglist[0]
            singular, plural = thing.get_numbered_name(nthings, looker, key=thingname)
            thing_names.append(singular if nthings == 1 else plural)
        thing_names = iter_to_str(thing_names)
        return (
            f"\n{self.get_display_name(looker, **kwargs)} is carrying {thing_names}"
            if thing_names
            else ""
        )

# COMMANDS START HERE

class CmdWear(MuxCommand):
    """
    Puts on an item of clothing you are holding.

    Usage:
      wear <obj> [=] [wear style]

    Examples:
      wear red shirt
      wear scarf wrapped loosely about the shoulders
      wear blue hat = at a jaunty angle

    All the clothes you are wearing are appended to your description.
    If you provide a 'wear style' after the command, the message you
    provide will be displayed after the clothing's name.
    """

    key = "wear"
    help_category = "clothing"

            self.caller.msg("Usage: wear <obj> [=] [wear style]")
            return
            # check if the whole string is an object
            clothing = self.caller.search(self.lhs, candidates=self.caller.contents, quiet=True)
            if not clothing:
                # split out the first word as the object and the rest as the wearstyle
                argslist = self.lhs.split()
                self.lhs = argslist[0]
                self.rhs = " ".join(argslist[1:])
                clothing = self.caller.search(self.lhs, candidates=self.caller.contents)
            else:
                # pass the result through the search-result hook
                clothing = at_search_result(clothing, self.caller, self.lhs)

        else:
            # it had an explicit separator - just do a normal search for the lhs
            clothing = self.caller.search(self.lhs, candidates=self.caller.contents)

        if not clothing:
            return
        if not inherits_from(clothing, ContribClothing):
            self.caller.msg(f"{clothing.name} isn't something you can wear.")
            return

        if clothing.db.worn:
                # If no wearstyle was provided and the clothing is already being worn, do nothing
                self.caller.msg(f"You're already wearing your {clothing.name}.")
                return
            elif len(self.rhs) > WEARSTYLE_MAXLENGTH:
                self.caller.msg(
                    f"Please keep your wear style message to less than {WEARSTYLE_MAXLENGTH} characters."
                )
                return
            else:
                # Adjust the wearstyle
                clothing.db.worn = self.rhs
                self.caller.location.msg_contents(
                    f"$You() $conj(wear) {clothing.name} {self.rhs}.", from_obj=self.caller
                )
                return

        already_worn = get_worn_clothes(self.caller)

        # Enforce overall clothing limit.
        if CLOTHING_OVERALL_LIMIT and len(already_worn) >= CLOTHING_OVERALL_LIMIT:
            self.caller.msg("You can't wear any more clothes.")
            return

        # Apply individual clothing type limits.
        if clothing_type := clothing.db.clothing_type:
            if clothing_type in CLOTHING_TYPE_LIMIT:
                type_count = single_type_count(already_worn, clothing_type)
                if type_count >= CLOTHING_TYPE_LIMIT[clothing_type]:
                    self.caller.msg(
                        f"You can't wear any more clothes of the type '{clothing_type}'."
                    )
                    return

        wearstyle = self.rhs or True
        clothing.wear(self.caller, wearstyle)

class CmdRemove(MuxCommand):
    """
    Takes off an item of clothing.

    Usage:
       remove <obj>

    Removes an item of clothing you are wearing. You can't remove
    clothes that are covered up by something else - you must take
    off the covering item first.
    """

    key = "remove"

        clothing = self.caller.search(self.args, candidates=self.caller.contents)
            self.caller.msg("You don't have anything like that.")
            return
        if not clothing.db.worn:
            self.caller.msg("You're not wearing that!")
            return
        if covered := clothing.db.covered_by:
            self.caller.msg(f"You have to take off {covered} first.")
            return
        clothing.remove(self.caller)

class CmdCover(MuxCommand):
    """
    Covers a worn item of clothing with another you're holding or wearing.

    Usage:
        cover <worn obj> with <obj>

    When you cover a clothing item, it is hidden and no longer appears in
    your description until it's uncovered or the item covering it is removed.
    You can't remove an item of clothing if it's covered.
    """

    key = "cover"
    rhs_split = (" with ", "=")

        if not len(self.args) or not self.rhs:
            self.caller.msg("Usage: cover <worn clothing> with <clothing object>")
            return

        to_cover = self.caller.search(self.lhs, candidates=get_worn_clothes(self.caller))
        cover_with = self.caller.search(self.rhs, candidates=self.caller.contents)
        if not to_cover or not cover_with:
            return
        if to_cover == cover_with:
            self.caller.msg("You can't cover an item with itself!")
            return

        if not inherits_from(cover_with, ContribClothing):
            self.caller.msg(f"{cover_with.name} isn't something you can wear.")
            return

        if cover_with.db.clothing_type in CLOTHING_TYPE_CANT_COVER_WITH:
            self.caller.msg(f"You can't cover anything with {cover_with.name}.")
            return

        if covered_by := cover_with.db.covered_by:
            self.caller.msg(f"{cover_with.name} is already covered by {covered_by.name}.")
            return
        if covered_by := to_cover.db.covered_by:
            self.caller.msg(f"{to_cover.name} is already covered by {covered_by.name}.")
            return

        # Put on the item to cover with if it's not on already
        if not cover_with.db.worn:
            cover_with.wear(self.caller, True)
        to_cover.db.covered_by = cover_with

        self.caller.location.msg_contents(
            f"$You() $conj(cover) {to_cover.name} with {cover_with.name}.", from_obj=self.caller
        )

class CmdUncover(MuxCommand):
    """
    Reveals a worn item of clothing that's currently covered up.

    Usage:
        uncover <obj>

    When you uncover an item of clothing, you allow it to appear in your
    description without having to take off the garment that's currently
    covering it. You can't uncover an item of clothing if the item covering
    it is also covered by something else.
    """

    key = "uncover"

        """
        This performs the actual command.
        """

            self.caller.msg("Usage: uncover <worn clothing object>")
            return

        clothing = self.caller.search(self.args, candidates=get_worn_clothes(self.caller))
            return
        if covered_by := clothing.db.covered_by:
            if covered_by.db.covered_by:
                self.caller.msg(f"{clothing.name} is under too many layers to uncover.")
                return
            clothing.db.covered_by = None
            self.caller.location.msg_contents(
                f"$You() $conj(uncover) {clothing.name}.", from_obj=self.caller
            )

        else:
            self.caller.msg(f"{clothing.name} isn't covered by anything.")
            return

class CmdInventory(MuxCommand):
    """

    Usage:
      inventory
      inv

    """

    # Alternate version of the inventory command which separates
    # worn and carried items.

        if not self.caller.contents:
            self.caller.msg("You are not carrying or wearing anything.")
            return

        message_list = []

        carry_table = evtable.EvTable(border="header")
        wear_table = evtable.EvTable(border="header")

        carried = [obj for obj in items if not obj.db.worn]
        worn = [obj for obj in items if obj.db.worn]

        message_list.append("|wYou are carrying:|n")
        for item in carried:
            carry_table.add_row(
                item.get_display_name(self.caller), item.get_display_desc(self.caller)
            )
        if carry_table.nrows == 0:
            carry_table.add_row("Nothing.", "")
        message_list.append(str(carry_table))

        message_list.append("|wYou are wearing:|n")
        for item in worn:
            item_name = item.get_display_name(self.caller)
            if item.db.covered_by:
                item_name += " (hidden)"
            wear_table.add_row(item_name, item.get_display_desc(self.caller))
        if wear_table.nrows == 0:
            wear_table.add_row("Nothing.", "")
        message_list.append(str(wear_table))

        self.caller.msg("\n".join(message_list))

class ClothedCharacterCmdSet(default_cmds.CharacterCmdSet):
    """
    Command set for clothing, including new versions of 'give' and 'drop'
    that take worn and covered clothing into account, as well as a new
    version of 'inventory' that differentiates between carried and worn
    items.
    """

        """
        Populates the cmdset
        """
        super().at_cmdset_creation()
        #
        # any commands you add below will overload the default ones.
        #
        self.add(CmdWear())
        self.add(CmdRemove())
        self.add(CmdCover())
        self.add(CmdUncover())
        self.add(CmdInventory())

"""
Testing clothing contrib

"""

from evennia.objects.objects import DefaultRoom

from . import clothing

class TestClothingCmd(BaseEvenniaCommandTest):
        self.room = create_object(DefaultRoom, key="Room")
        self.wearer = create_object(clothing.ClothedCharacter, key="Wearer")
        self.wearer.location = self.room
        # Make a test hat
        self.test_hat = create_object(clothing.ContribClothing, key="test hat")
        self.test_hat.db.clothing_type = "hat"
        # Make a test scarf
        self.test_scarf = create_object(clothing.ContribClothing, key="test scarf")
        self.test_scarf.db.clothing_type = "accessory"

    def test_clothingcommands(self):
        # Test inventory command.
            clothing.CmdInventory(),
            "",
            "You are not carrying or wearing anything.",
            caller=self.wearer,
        )

        # Test wear command
        self.test_scarf.location = self.wearer
        self.test_hat.location = self.wearer
        self.call(clothing.CmdWear(), "", "Usage: wear <obj> [=] [wear style]", caller=self.wearer)
        self.call(clothing.CmdWear(), "hat", "You put on test hat.", caller=self.wearer)
            clothing.CmdWear(),
            "scarf stylishly",
            "You wear test scarf stylishly.",
        )
        # Test cover command.
            clothing.CmdCover(),
            "",
            "Usage: cover <worn clothing> with <clothing object>",
        )
            "hat with scarf",
            "You cover test hat with test scarf.",
        )
        # Test remove command.
        self.call(clothing.CmdRemove(), "", "Could not find ''.", caller=self.wearer)
            clothing.CmdRemove(),
            "hat",
            "You have to take off test scarf first.",
        )
            "scarf",
            "You remove test scarf, revealing test hat.",
        )
        # Test uncover command.
        self.test_scarf.wear(self.wearer, True)
        self.test_hat.db.covered_by = self.test_scarf
            clothing.CmdUncover(), "", "Usage: uncover <worn clothing object>", caller=self.wearer
        )
        self.call(clothing.CmdUncover(), "hat", "You uncover test hat.", caller=self.wearer)

    def test_clothing_limits(self):
        """
        make sure clothing type limits are being enforced
        """
        # change the scarf to a hat for convenience
        # since the "hat" type is limited to 1 by default
        self.test_scarf.db.clothing_type = "hat"
        # move to wearer to be wearable
        # try wearing the hat and scarf-hat
            "scarf",
            "You can't wear any more clothes of the type 'hat'.",
        )

class TestClothingFunc(BaseEvenniaTest):
        # Make a test shirt
        self.test_shirt = create_object(clothing.ContribClothing, key="test shirt")
        self.test_shirt.db.clothing_type = "top"
        self.test_shirt.location = self.wearer
        # Make test pants
        self.test_pants = create_object(clothing.ContribClothing, key="test pants")
        self.test_pants.db.clothing_type = "bottom"
        self.test_pants.location = self.wearer

    def test_clothingfunctions(self):
        self.test_hat.wear(self.wearer, "on the head")
        self.assertEqual(self.test_hat.db.worn, "on the head")

        self.test_hat.remove(self.wearer)
        self.assertFalse(self.test_hat.db.worn)

        self.test_hat.db.worn = True
        self.test_hat.at_get(self.wearer)

        self.test_hat.db.covered_by = self.test_shirt
        can_move = self.test_hat.at_pre_move(self.room)
        self.assertFalse(can_move)

        clothes_list = [self.test_shirt, self.test_hat, self.test_pants]
            clothing.order_clothes_list(clothes_list),
            [self.test_hat, self.test_shirt, self.test_pants],
        )

        self.test_hat.wear(self.wearer, True)
        self.test_pants.wear(self.wearer, True)
        self.assertEqual(clothing.get_worn_clothes(self.wearer), [self.test_hat, self.test_pants])

            clothing.clothing_type_count(clothes_list), {"hat": 1, "top": 1, "bottom": 1}
        )

        self.assertEqual(clothing.single_type_count(clothes_list, "hat"), 1)

"""
Cooldown contrib module.

Evennia contrib - owllex, 2021

This contrib provides a simple cooldown handler that can be attached to any
typeclassed Object or Account. A cooldown is a lightweight persistent
asynchronous timer that you can query to see if it is ready.

Cooldowns are good for modelling rate-limited actions, like how often a
character can perform a given command.

Cooldowns are completely asynchronous and must be queried to know their
state. They do not fire callbacks, so are not a good fit for use cases
where something needs to happen on a specific schedule (use delay or
a TickerHandler for that instead).

See also the evennia documentation for command cooldowns
(https://github.com/evennia/evennia/wiki/Command-Cooldown) for more information
about the concept.

To use, simply add the following property to the typeclass definition of any
object type that you want to support cooldowns. It will expose a new `cooldowns`
property that persists data to the object's attribute storage. You can set this
on your base `Object` typeclass to enable cooldown tracking on every kind of
object, or just put it on your `Character` typeclass.

By default the CooldownHandler will use the `cooldowns` property, but you can
customize this if desired by passing a different value for the db_attribute
parameter.

    from evennia.contrib.game_systems.cooldowns import Cooldownhandler
    from evennia.utils.utils import lazy_property

    def cooldowns(self):
        return CooldownHandler(self, db_attribute="cooldowns")

Example:

Assuming you've installed cooldowns on your Character typeclasses, you can use a
cooldown to limit how often you can perform a command. The following code
snippet will limit the use of a Power Attack command to once every 10 seconds
per character.

class PowerAttack(Command):
        if self.caller.cooldowns.ready("power attack"):
            self.do_power_attack()
            self.caller.cooldowns.add("power attack", 10)
        else:
            self.caller.msg("That's not ready yet!")

"""

class CooldownHandler:
    """
    Handler for cooldowns. This can be attached to any object that supports DB
    attributes (like a Character or Account).

    A cooldown is a timer that is usually used to limit how often some action
    can be performed or some effect can trigger. When a cooldown is first added,
    it counts down from the amount of time provided back to zero, at which point
    it is considered ready again.

    Cooldowns are named with an arbitrary string, and that string is used to
    check on the progression of the cooldown. Each cooldown is tracked
    separately and independently from other cooldowns on that same object. A
    cooldown is unique per-object.

    Cooldowns are saved persistently, so they survive reboots. This module does
    not register or provide callback functionality for when a cooldown becomes
    ready again. Users of cooldowns are expected to query the state of any
    cooldowns they are interested in.

    Methods:
    - ready(name): Checks whether a given cooldown name is ready.
    - time_left(name): Returns how much time is left on a cooldown.
    - add(name, seconds): Sets a given cooldown to last for a certain
        amount of time. Until then, ready() will return False for that
        cooldown name. set() is an alias.
    - extend(name, seconds): Like add(), but adds more time to the given
        cooldown if it already exists. If it doesn't exist yet, calling
        this is equivalent to calling add().
    - reset(cooldown): Resets a given cooldown, causing ready() to return
        True for that cooldown immediately.
    - clear(): Resets all cooldowns.
    """

    __slots__ = ("data", "db_attribute", "obj")

    def __init__(self, obj, db_attribute="cooldowns"):
        if not obj.attributes.has(db_attribute):
            obj.attributes.add(db_attribute, {})

        self.data = obj.attributes.get(db_attribute)
        self.db_attribute = db_attribute
        self.cleanup()

    @property
        """
        Returns a list of all keys in this object.
        """
        return list(self.data.keys())

    def ready(self, *args):
        """
        Checks whether all of the provided cooldowns are ready (expired). If a
        requested cooldown does not exist, it is considered ready.

        Args:
            *args (str): One or more cooldown names to check.
        Returns:
            bool: True if each cooldown has expired or does not exist.
        """
        return self.time_left(*args, use_int=True) <= 0

    def time_left(self, *args, use_int=False):
        """
        Returns the maximum amount of time left on one or more given cooldowns.
        If a requested cooldown does not exist, it is considered to have 0 time
        left.

        Args:
            use_int (bool): True to round the return value up to an int,
                False (default) to return a more precise float.
        Returns:
            float or int: Number of seconds until all provided cooldowns are
                ready. Returns 0 if all cooldowns are ready (or don't exist.)
        """
        now = time.time()
        cooldowns = [self.data[x] - now for x in args if x in self.data]
        if not cooldowns:
            return 0 if use_int else 0.0
        left = max(max(cooldowns), 0)
        return math.ceil(left) if use_int else left

    def add(self, cooldown, seconds):
        """
        Adds/sets a given cooldown to last for a specific amount of time.

        If this cooldown already exits, this call replaces it.

        Args:
            cooldown (str): The name of the cooldown.
            seconds (float or int): The number of seconds before this cooldown
                is ready again.
        """
        self.data[cooldown] = now + (max(seconds, 0) if seconds else 0)

    set = add

    def extend(self, cooldown, seconds):
        """
        Adds a specific amount of time to an existing cooldown.

        If this cooldown is already ready, this is equivalent to calling set. If
        the cooldown is not ready, it will be extended by the provided duration.

        Args:
            seconds (float or int): The number of seconds to extend this cooldown.
        Returns:
            float: The number of seconds until the cooldown will be ready again.
        """
        time_left = self.time_left(cooldown) + (seconds if seconds else 0)
        self.set(cooldown, time_left)
        return max(time_left, 0)

    def reset(self, cooldown):
        """
        Resets a given cooldown.

        Args:
        """
        if cooldown in self.data:
            del self.data[cooldown]

        """
        Resets all cooldowns.
        """
        self.data.clear()

        """
        Deletes all expired cooldowns. This helps keep attribute storage
        requirements small.
        """
        cooldowns = dict(self.data)
        keys = [x for x in cooldowns.keys() if cooldowns[x] - now < 0]
        if keys:
            for key in keys:
                del cooldowns[key]
            self.obj.attributes.add(self.db_attribute, cooldowns)
            self.data = self.obj.attributes.get(self.db_attribute)

"""
Cooldowns tests.

"""

from mock import patch

from . import cooldowns

@patch("evennia.contrib.game_systems.cooldowns.cooldowns.time.time", return_value=0.0)
class TestCooldowns(BaseEvenniaTest):
        self.handler = cooldowns.CooldownHandler(self.char1)

    def test_empty(self, mock_time):
        self.assertEqual(self.handler.all, [])
        self.assertTrue(self.handler.ready("a", "b", "c"))
        self.assertEqual(self.handler.time_left("a", "b", "c"), 0)

    def test_add(self, mock_time):
        self.assertEqual(self.handler.add, self.handler.set)
        self.handler.add("a", 10)
        self.assertFalse(self.handler.ready("a"))
        self.assertEqual(self.handler.time_left("a"), 10)
        mock_time.return_value = 9.0
        self.assertEqual(self.handler.time_left("a"), 1)
        mock_time.return_value = 10.0
        self.assertTrue(self.handler.ready("a"))
        self.assertEqual(self.handler.time_left("a"), 0)

    def test_add_float(self, mock_time):
        self.assertEqual(self.handler.time_left("a", use_int=False), 0)
        self.assertEqual(self.handler.time_left("a", use_int=True), 0)
        self.handler.add("a", 5.5)
        self.assertEqual(self.handler.time_left("a"), 5.5)
        self.assertEqual(self.handler.time_left("a", use_int=False), 5.5)
        self.assertEqual(self.handler.time_left("a", use_int=True), 6)

    def test_add_multi(self, mock_time):
        self.handler.add("b", 5)
        self.handler.add("c", 3)
        self.assertFalse(self.handler.ready("a", "b", "c"))
        self.assertEqual(self.handler.time_left("a", "b", "c"), 10)
        self.assertEqual(self.handler.time_left("a", "b"), 10)
        self.assertEqual(self.handler.time_left("a", "c"), 10)
        self.assertEqual(self.handler.time_left("b", "c"), 5)
        self.assertEqual(self.handler.time_left("c", "c"), 3)

    def test_add_none(self, mock_time):
        self.handler.add("a", None)

    def test_add_negative(self, mock_time):
        self.handler.add("a", -5)

    def test_add_overwrite(self, mock_time):
        self.handler.add("a", 5)
        self.handler.add("a", 3)
        self.assertEqual(self.handler.time_left("a"), 3)

    def test_extend(self, mock_time):
        self.assertEqual(self.handler.extend("a", 10), 10)
        self.assertEqual(self.handler.extend("a", 10), 20)
        self.assertEqual(self.handler.time_left("a"), 20)

    def test_extend_none(self, mock_time):
        self.assertEqual(self.handler.extend("a", None), 0)
        self.assertEqual(self.handler.extend("a", None), 10)

    def test_extend_negative(self, mock_time):
        self.assertEqual(self.handler.extend("a", -5), 0)
        self.assertEqual(self.handler.extend("a", -5), 5)
        self.assertEqual(self.handler.time_left("a"), 5)

    def test_extend_float(self, mock_time):
        self.assertEqual(self.handler.extend("a", -5.5), 0)
        self.assertEqual(self.handler.time_left("a"), 0.0)
        self.assertEqual(self.handler.time_left("a", use_int=False), 0.0)
        self.handler.add("a", 10.5)
        self.assertEqual(self.handler.extend("a", -5.25), 5.25)
        self.assertEqual(self.handler.time_left("a"), 5.25)
        self.assertEqual(self.handler.time_left("a", use_int=False), 5.25)

    def test_reset_non_existent(self, mock_time):
        self.handler.reset("a")

    def test_reset(self, mock_time):
        self.handler.set("a", 10)

    def test_clear(self, mock_time):
        self.handler.add("b", 10)
        self.handler.add("c", 10)
        self.handler.clear()

    def test_cleanup(self, mock_time):
        self.handler.add("c", 5)
        self.handler.add("d", 3.5)
        mock_time.return_value = 6.0
        self.handler.cleanup()
        self.assertEqual(self.handler.time_left("b", "c", "d"), 0)
        self.assertEqual(self.handler.time_left("a"), 4)
        self.assertEqual(list(self.handler.data.keys()), ["a"])

    def test_cleanup_doesnt_delete_anything(self, mock_time):
        mock_time.return_value = 1.0
        self.assertEqual(self.handler.time_left("d"), 2.5)
        self.assertEqual(self.handler.time_left("b", "c"), 4)
        self.assertEqual(self.handler.time_left("a"), 9)
        self.assertEqual(list(self.handler.data.keys()), ["a", "b", "c", "d"])

"""
Crafting - Griatch 2020

This is a general crafting engine. The basic functionality of crafting is to
combine any number of of items or tools in a 'recipe' to produce a new result.

    item + item + item + tool + tool  -> recipe -> new result

This is useful not only for traditional crafting but the engine is flexible
enough to also be useful for puzzles or similar.

## Installation

- Add the `CmdCraft` Command from this module to your default cmdset. This
  allows for crafting from in-game using a simple syntax.
- Create a new module and add it to a new list in your settings file
  (`server/conf/settings.py`) named `CRAFT_RECIPES_MODULES`, such as
  `CRAFT_RECIPE_MODULES = ["world.recipes_weapons"]`.
- In the new module(s), create one or more classes, each a child of
  `CraftingRecipe` from this module. Each such class must have a unique `.name`
  property. It also defines what inputs are required and what is created using
  this recipe.
- Objects to use for crafting should (by default) be tagged with tags using the
  tag-category `crafting_material` or `crafting_tool`. The name of the object
  doesn't matter, only its tag.

## Crafting in game

The default `craft` command handles all crafting needs.
::

    > craft spiked club from club, nails

Here, `spiked club` specifies the recipe while `club` and `nails` are objects
the crafter must have in their inventory. These will be consumed during
crafting (by default only if crafting was successful).

A recipe can also require *tools* (like the `hammer` above). These must be
either in inventory *or* be in the current location. Tools are *not* consumed
during the crafting process.
::

    > craft wooden doll from wood with knife

## Crafting in code

In code, you should use the helper function `craft` from this module. This
specifies the name of the recipe to use and expects all suitable
ingredients/tools as arguments (consumables and tools should be added together,
tools will be identified before consumables).

```python

    from evennia.contrib.game_systems.crafting import crafting

    spiked_club = crafting.craft(crafter, "spiked club", club, nails)

```

The result is always a list with zero or more objects. A fail leads to an empty
list. The crafter should already have been notified of any error in this case
(this should be handle by the recipe itself).

## Recipes

A *recipe* is a class that works like an input/output blackbox: you initialize
it with consumables (and/or tools) if they match the recipe, a new
result is spit out.  Consumables are consumed in the process while tools are not.

This module contains a base class for making new ingredient types
(`CraftingRecipeBase`) and an implementation of the most common form of
crafting (`CraftingRecipe`) using objects and prototypes.

Recipes are put in one or more modules added as a list to the
`CRAFT_RECIPE_MODULES` setting, for example:

```python

    CRAFT_RECIPE_MODULES = ['world.recipes_weapons', 'world.recipes_potions']

```

Below is an example of a crafting recipe and how `craft` calls it under the
hood. See the `CraftingRecipe` class for details of which properties and
methods are available to override - the craft behavior can be modified
substantially this way.

```python

    from evennia.contrib.game_systems.crafting.crafting import CraftingRecipe

    class PigIronRecipe(CraftingRecipe):
        # Pig iron is a high-carbon result of melting iron in a blast furnace.

        name = "pig iron"  # this is what crafting.craft and CmdCraft uses
        tool_tags = ["blast furnace"]
        consumable_tags = ["iron ore", "coal", "coal"]
        output_prototypes = [
            {"key": "Pig Iron ingot",
             "desc": "An ingot of crude pig iron.",
             "tags": [("pig iron", "crafting_material")]}
        ]

    # for testing, conveniently spawn all we need based on the tags on the class
    tools, consumables = PigIronRecipe.seed()

    recipe = PigIronRecipe(caller, *(tools + consumables))
    result = recipe.craft()

```

If the above class was added to a module in `CRAFT_RECIPE_MODULES`, it could be
called using its `.name` property, as "pig iron".

The [example_recipies](api:evennia.contrib.game_systems.crafting.example_recipes) module has
a full example of the components for creating a sword from base components.

----

"""

from evennia.prototypes.spawner import spawn
    callables_from_module,
    iter_to_str,
)

_RECIPE_CLASSES = {}

def _load_recipes():
    """
    Delayed loading of recipe classes. This parses
    `settings.CRAFT_RECIPE_MODULES`.

    """

    global _RECIPE_CLASSES
    if not _RECIPE_CLASSES:
        paths = ["evennia.contrib.game_systems.crafting.example_recipes"]
        if hasattr(settings, "CRAFT_RECIPE_MODULES"):
            paths += make_iter(settings.CRAFT_RECIPE_MODULES)
        for path in paths:
            for cls in callables_from_module(path).values():
                if inherits_from(cls, CraftingRecipeBase):
                    _RECIPE_CLASSES[cls.name] = cls

class CraftingError(RuntimeError):
    """
    Crafting error.

    """

class CraftingValidationError(CraftingError):
    """
    Error if crafting validation failed.

    """

class CraftingRecipeBase:
    """
    The recipe handles all aspects of performing a 'craft' operation. This is
    the base of the crafting system, intended to be replace if you want to
    adapt it for very different functionality - see the `CraftingRecipe` child
    class for an implementation of the most common type of crafting using
    objects.

    Example of usage:
    ::

        recipe = CraftRecipe(crafter, obj1, obj2, obj3)
        result = recipe.craft()

    Note that the most common crafting operation is that the inputs are
    consumed - so in that case the recipe cannot be used a second time (doing so
    will raise a `CraftingError`)

    Process:

    1. `.craft(**kwargs)` - this starts the process on the initialized recipe. The kwargs
       are optional but will be passed into all of the following hooks.
    2. `.pre_craft(**kwargs)` - this normally validates inputs and stores them in
       `.validated_inputs.`. Raises `CraftingValidationError` otherwise.
    4. `.do_craft(**kwargs)` - should return the crafted item(s) or the empty list. Any
       crafting errors should be immediately reported to user.
    5. `.post_craft(crafted_result, **kwargs)`- always called, even if `pre_craft`
       raised a `CraftingError` or `CraftingValidationError`.
       Should return `crafted_result` (modified or not).

    """

    name = "recipe base"

    # if set, allow running `.craft` more than once on the same instance.
    # don't set this unless crafting inputs are *not* consumed by the crafting
    # process (otherwise subsequent calls will fail).
    allow_reuse = False

    def __init__(self, crafter, *inputs, **kwargs):
        """
        Initialize the recipe.

        Args:
            crafter (Object): The one doing the crafting.
            *inputs (any): The ingredients of the recipe to use.
            **kwargs (any): Any other parameters that are relevant for
                this recipe.

        """
        self.crafter = crafter
        self.inputs = inputs
        self.craft_kwargs = kwargs
        self.allow_craft = True
        self.validated_inputs = []

    def msg(self, message, **kwargs):
        """
        Send message to crafter. This is a central point to override if wanting
        to change crafting return style in some way.

        Args:
            message(str): The message to send.
            **kwargs: Any optional properties relevant to this send.

        """
        self.crafter.msg(message, {"type": "crafting"})

    def pre_craft(self, **kwargs):
        """
        Hook to override.

        This is called just before crafting operation and is normally
        responsible for validating the inputs, storing data on
        `self.validated_inputs`.

        Args:
            **kwargs: Optional extra flags passed during initialization or
            `.craft(**kwargs)`.

        Raises:
            CraftingValidationError: If validation fails.

        """
        if self.allow_craft:
            self.validated_inputs = self.inputs[:]
        else:
            raise CraftingValidationError

    def do_craft(self, **kwargs):
        """

        This performs the actual crafting. At this point the inputs are
        expected to have been verified already. If needed, the validated
        inputs are available on this recipe instance.

        Args:
            **kwargs: Any extra flags passed at initialization.

        Returns:
            any: The result of crafting.

        """

    def post_craft(self, crafting_result, **kwargs):
        """

        This is called just after crafting has finished. A common use of this
        method is to delete the inputs.

        Args:
            crafting_result (any): The outcome of crafting, as returned by `do_craft`.

        Returns:
            any: The final crafting result.

        """
        return crafting_result

    def craft(self, raise_exception=False, **kwargs):
        """
        Main crafting call method. Call this to produce a result and make
        sure all hooks run correctly.

        Args:
            raise_exception (bool): If crafting would return `None`, raise
                exception instead.
            **kwargs (any): Any other parameters that is relevant
                for this particular craft operation. This will temporarily
                override same-named kwargs given at the creation of this recipe
                and be passed into all of the crafting hooks.

        Returns:
            any: The result of the craft, or `None` if crafting failed.

        Raises:
            CraftingValidationError: If recipe validation failed and
                `raise_exception` is True.
            CraftingError: On If trying to rerun a no-rerun recipe, or if crafting
                would return `None` and raise_exception` is set.

        """
        craft_result = None
            # override/extend craft_kwargs from initialization.
            craft_kwargs = copy(self.craft_kwargs)
            craft_kwargs.update(kwargs)

            try:
                try:
                    # this assigns to self.validated_inputs
                    self.pre_craft(**craft_kwargs)
                except (CraftingError, CraftingValidationError):
                    if raise_exception:
                        raise
                else:
                    craft_result = self.do_craft(**craft_kwargs)
                finally:
                    craft_result = self.post_craft(craft_result, **craft_kwargs)
            except (CraftingError, CraftingValidationError):
                if raise_exception:
                    raise

            # possibly turn off re-use depending on class setting
            self.allow_craft = self.allow_reuse
        elif not self.allow_reuse:
            raise CraftingError("Cannot re-run crafting without re-initializing recipe first.")
        if craft_result is None and raise_exception:
            raise CraftingError(f"Crafting of {self.name} failed.")
        return craft_result

class NonExistentRecipe(CraftingRecipeBase):
    """A recipe that does not exist and never produces anything."""

    allow_craft = True
    allow_reuse = True

    def __init__(self, crafter, *inputs, name="", **kwargs):
        super().__init__(crafter, *inputs, **kwargs)
        self.name = name

        msg = f"Unknown recipe '{self.name}'"
        self.msg(msg)
        raise CraftingError(msg)

class CraftingRecipe(CraftingRecipeBase):
    """
    The CraftRecipe implements the most common form of crafting: Combining (and
    consuming) inputs to produce a new result. This type of recipe only works
    with typeclassed entities as inputs and outputs, since it's based on Tags
    and Prototypes.

    There are two types of crafting ingredients: 'tools' and 'consumables'. The
    difference between them is that the former is not consumed in the crafting
    process. So if you need a hammer and anvil to craft a sword, they are
    'tools' whereas the materials of the sword are 'consumables'.

    Examples:
    ::

        class FlourRecipe(CraftRecipe):
            name = "flour"
            tool_tags = ['windmill']
            consumable_tags = ["wheat"]
            output_prototypes = [
                {"key": "Bag of flour",
                 "typeclass": "typeclasses.food.Flour",
                 "desc": "A small bag of flour."
                 "tags": [("flour", "crafting_material")],
                }
            ]

        class BreadRecipe(CraftRecipe):
            name = "bread"
            tool_tags = ["roller", "owen"]
            consumable_tags = ["flour", "egg", "egg", "salt", "water", "yeast"]
                {"key": "bread",
                 "desc": "A tasty bread."
                }
            ]

    ## Properties on the class level:

    - `name` (str): The name of this recipe. This should be globally unique.

    ### tools

    - `tool_tag_category` (str): What tag-category tools must use. Default is
      'crafting_tool'.
    - `tool_tags` (list): Object-tags to use for tooling. If more than one instace
      of a tool is needed, add multiple entries here.
    - `tool_names` (list): Human-readable names for tools. These are used for informative
      messages/errors. If not given, the tags will be used. If given, this list should
      match the length of `tool_tags`.:
    - `exact_tools` (bool, default True): Must have exactly the right tools, any extra
      leads to failure.
    - `exact_tool_order` (bool, default False): Tools must be added in exactly the
      right order for crafting to pass.

    ### consumables

    - `consumable_tag_category` (str): What tag-category consumables must use.
      Default is 'crafting_material'.
    - `consumable_tags` (list): Tags for objects that will be consumed as part of
      running the recipe.
    - `consumable_names` (list): Human-readable names for consumables. Same as for tools.
    - `exact_consumables` (bool, default True): Normally, adding more consumables
      than needed leads to a a crafting error. If this is False, the craft will
      still succeed (only the needed ingredients will be consumed).
    - `exact_consumable_order` (bool, default False): Normally, the order in which
      ingredients are added does not matter. With this set, trying to add consumables in
      another order than given will lead to failing crafting.
    - `consume_on_fail` (bool, default False): Normally, consumables remain if
      crafting fails. With this flag, a failed crafting will still consume
      consumables. Note that this will also consume any 'extra' consumables
      added not part of the recipe!

    ### outputs (result of crafting)

    - `output_prototypes` (list): One or more prototypes (`prototype_keys` or
      full dicts) describing how to create the result(s) of this recipe.
    - `output_names` (list): Human-readable names for (prospective) prototypes.
      This is used in error messages. If not given, this is extracted from the
      prototypes' `key` if possible.

    ### custom error messages

    custom messages all have custom formatting markers. Many are empty strings
    when not applicable.
    ::

        {missing}: Comma-separated list of tool/consumable missing for missing/out of order errors.
        {excess}: Comma-separated list of tool/consumable added in excess of recipe
        {inputs}: Comma-separated list of any inputs (tools + consumables) involved in error.
        {tools}: Comma-sepatated list of tools involved in error.
        {consumables}: Comma-separated list of consumables involved in error.
        {outputs}: Comma-separated list of (expected) outputs
        {t0}..{tN-1}: Individual tools, same order as `.tool_names`.
        {c0}..{cN-1}: Individual consumables, same order as `.consumable_names`.
        {o0}..{oN-1}: Individual outputs, same order as `.output_names`.

    - `error_tool_missing_message`: "Could not craft {outputs} without {missing}."
    - `error_tool_order_message`:
      "Could not craft {outputs} since {missing} was added in the wrong order."
    - `error_tool_excess_message`: "Could not craft {outputs} (extra {excess})."
    - `error_consumable_missing_message`: "Could not craft {outputs} without {missing}."
    - `error_consumable_order_message`:
    - `error_consumable_excess_message`: "Could not craft {outputs} (excess {excess})."
    - `success_message`: "You successfuly craft {outputs}!"
    - `failure_message`: ""  (this is handled by the other error messages by default)

    ## Hooks

    1. Crafting starts by calling `.craft(**kwargs)` on the parent class. The
       `**kwargs` are optional, extends any `**kwargs` passed to the class
       constructor and will be passed into all the following hooks.
    3. `.pre_craft(**kwargs)` should handle validation of inputs. Results should
       be stored in `validated_consumables/tools` respectively. Raises `CraftingValidationError`
       otherwise.
    4. `.do_craft(**kwargs)` will not be called if validation failed. Should return
       a list of the things crafted.
    5. `.post_craft(crafting_result, **kwargs)` is always called, also if validation
       failed (`crafting_result` will then be falsy). It does any cleanup. By default
       this deletes consumables.

    Use `.msg` to conveniently send messages to the crafter. Raise
    `evennia.contrib.game_systems.crafting.crafting.CraftingError` exception to abort
    crafting at any time in the sequence. If raising with a text, this will be
    shown to the crafter automatically

    """

    name = "crafting recipe"

    # this define the overall category all material tags must have
    consumable_tag_category = "crafting_material"
    # tag category for tool objects
    tool_tag_category = "crafting_tool"

    # the tools needed to perform this crafting. Tools are never consumed (if they were,
    # they'd need to be a consumable). If more than one instance of a tool is needed,
    # there should be multiple entries in this list.
    tool_tags = []
    # human-readable names for the tools. This will be used for informative messages
    # or when usage fails. If empty, use tag-names.
    tool_names = []
    # if we must have exactly the right tools, no more
    exact_tools = True
    # if the order of the tools matters
    exact_tool_order = False
    # error to show if missing tools
    error_tool_missing_message = "Could not craft {outputs} without {missing}."
    # error to show if tool-order matters and it was wrong. Missing is the first
    # tool out of order
    error_tool_order_message = (
        "Could not craft {outputs} since {missing} was added in the wrong order."
    )
    # if .exact_tools is set and there are more than needed
    error_tool_excess_message = (
        "Could not craft {outputs} without the exact tools (extra {excess})."
    )

    # a list of tag-keys (of the `tag_category`). If more than one of each type
    # is needed, there should be multiple same-named entries in this list.
    consumable_tags = []
    # these are human-readable names for the items to use. This is used for informative
    # messages or when usage fails. If empty, the tag-names will be used. If given, this
    # must have the same length as `consumable_tags`.
    consumable_names = []
    # if True, consume valid inputs also if crafting failed (returned None)
    consume_on_fail = False
    # if True, having any wrong input result in failing the crafting. If False,
    # extra components beyond the recipe are ignored.
    exact_consumables = True
    # if True, the exact order in which inputs are provided matters and must match
    # the order of `consumable_tags`. If False, order doesn't matter.
    exact_consumable_order = False
    # error to show if missing consumables
    error_consumable_missing_message = "Could not craft {outputs} without {missing}."
    # error to show if consumable order matters and it was wrong. Missing is the first
    # consumable out of order
    error_consumable_order_message = (
    )
    # if .exact_consumables is set and there are more than needed
    error_consumable_excess_message = (
        "Could not craft {outputs} without the exact ingredients (extra {excess})."
    )

    # this is a list of one or more prototypes (prototype_keys to existing
    # prototypes or full prototype-dicts) to use to build the result. All of
    # these will be returned (as a list) if crafting succeeded.
    output_prototypes = []
    # human-readable name(s) for the (expected) result of this crafting. This will usually only
    # be used for error messages (to report what would have been). If not given, the
    # prototype's key or typeclass will be used. If given, this must have the same length
    # as `output_prototypes`.
    output_names = []
    # general craft-failure msg to show after other error-messages.
    failure_message = ""
    # show after a successful craft
    success_message = "You successfully craft {outputs}!"

        """
        Args:
            *inputs (Object): The ingredients (+tools) of the recipe to use. The
                The recipe will itself figure out (from tags) which is a tool and
                which is a consumable.
                this recipe. These will be passed into the crafting hooks.

        Notes:
            Internally, this class stores validated data in
            `.validated_consumables`  and `.validated_tools` respectively. The
            `.validated_inputs` property (from parent) holds a list of everything
            types in the order inserted to the class constructor.

        """

        self.validated_consumables = []
        self.validated_tools = []

        # validate class properties
        if self.consumable_names:
            assert len(self.consumable_names) == len(self.consumable_tags), (
                f"Crafting {self.__class__}.consumable_names list must "
                "have the same length as .consumable_tags."
            )
        else:
            self.consumable_names = self.consumable_tags

        if self.tool_names:
            assert len(self.tool_names) == len(self.tool_tags), (
                f"Crafting {self.__class__}.tool_names list must "
                "have the same length as .tool_tags."
            )
        else:
            self.tool_names = self.tool_tags

        assert isinstance(
            self.output_prototypes, (list, tuple)
        ), "Crafting {self.__class__}.output_prototypes must be a list or tuple."

        if self.output_names:
            assert len(self.output_names) == len(self.output_prototypes), (
                f"Crafting {self.__class__}.output_names list must "
                "have the same length as .output_prototypes."
            )
        else:
            self.output_names = [
                prot.get("key", prot.get("typeclass", "unnamed"))
                if isinstance(prot, dict)
                else str(prot)
                for prot in self.output_prototypes
            ]

        # don't allow reuse if we have consumables. If only tools we can reuse
        # over and over since nothing changes.
        self.allow_reuse = not bool(self.consumable_tags)

    def _format_message(self, message, **kwargs):
        missing = iter_to_str(kwargs.get("missing", ""))
        excess = iter_to_str(kwargs.get("excess", ""))
        involved_tools = iter_to_str(kwargs.get("tools", ""))
        involved_cons = iter_to_str(kwargs.get("consumables", ""))

        # build template context
        mapping = {"missing": missing, "excess": excess}
        mapping.update(
            {
                f"i{ind}": self.consumable_names[ind]
                for ind, name in enumerate(self.consumable_names or self.consumable_tags)
            }
        )
            {f"o{ind}": self.output_names[ind] for ind, name in enumerate(self.output_names)}
        )
        mapping["tools"] = involved_tools
        mapping["consumables"] = involved_cons

        mapping["inputs"] = iter_to_str(self.consumable_names)
        mapping["outputs"] = iter_to_str(self.output_names)

        # populate template and return
        return message.format_map(mapping)

    def seed(cls, tool_kwargs=None, consumable_kwargs=None, location=None):
        """
        This is a helper class-method for easy testing and application of this
        recipe. When called, it will create simple dummy ingredients with names
        and tags needed by this recipe.

        Args:
            tool_kwargs (dict, optional): Will be passed as `**tool_kwargs` into the `create_object`
                call for each tool.  If not given, the matching
                `tool_name` or `tool_tag` will  be used for key.
            consumable_kwargs (dict, optional): This will be passed as
                `**consumable_kwargs` into the `create_object` call for each consumable.
                If not given, matching `consumable_name` or `consumable_tag`
                will  be used for key.
            location (Object, optional): If given, the created items will be created in this
                location. This is a shortcut for adding {"location": <obj>} to both the
                consumable/tool kwargs (and will *override* any such setting in those kwargs).

        Returns:
            tuple: A tuple `(tools, consumables)` with newly created dummy
            objects matching the recipe ingredient list.

        Example:
        ::
            tools, consumables = SwordRecipe.seed(location=caller)
            recipe = SwordRecipe(caller, *(tools + consumables))
            result = recipe.craft()

        Notes:
            If `key` is given in `consumable/tool_kwargs` then _every_ created item
            of each type will have the same key.

        """
        if not tool_kwargs:
            tool_kwargs = {}
        if not consumable_kwargs:
            consumable_kwargs = {}

            tool_kwargs["location"] = location
            consumable_kwargs["location"] = location

        tool_key = tool_kwargs.pop("key", None)
        cons_key = consumable_kwargs.pop("key", None)
        tool_tags = tool_kwargs.pop("tags", [])
        cons_tags = consumable_kwargs.pop("tags", [])

        tools = []
        for itag, tag in enumerate(cls.tool_tags):
            tools.append(
                create_object(
                    key=tool_key or (cls.tool_names[itag] if cls.tool_names else tag.capitalize()),
                    tags=[(tag, cls.tool_tag_category), *tool_tags],
                    **tool_kwargs,
                )
            )
        consumables = []
        for itag, tag in enumerate(cls.consumable_tags):
            consumables.append(
                    key=cons_key
                    or (cls.consumable_names[itag] if cls.consumable_names else tag.capitalize()),
                    tags=[(tag, cls.consumable_tag_category), *cons_tags],
                    **consumable_kwargs,
                )
            )
        return tools, consumables

        """
        Do pre-craft checks, including input validation.

        Check so the given inputs are what is needed. This operates on
        `self.inputs` which is set to the inputs added to the class
        constructor. Validated data is stored as lists on `.validated_tools`
        and `.validated_consumables` respectively.

        Args:
            **kwargs: Any optional extra kwargs passed during initialization of
                the recipe class.

        Raises:
            CraftingValidationError: If validation fails. At this point the crafter
                is expected to have been informed of the problem already.

        """

        def _check_completeness(
            tagmap,
            taglist,
            namelist,
            exact_match,
            exact_order,
            error_missing_message,
            error_order_message,
            error_excess_message,
        ):
            """Compare tagmap (inputs) to taglist (required)"""
            valids = []
            for itag, tagkey in enumerate(taglist):
                found_obj = None
                for obj, objtags in tagmap.items():
                    if tagkey in objtags:
                        found_obj = obj
                        break
                    if exact_order:
                        # if we get here order is wrong
                        err = self._format_message(
                            error_order_message, missing=obj.get_display_name(looker=self.crafter)
                        )
                        self.msg(err)
                        raise CraftingValidationError(err)

                # since we pop from the mapping, it gets ever shorter
                match = tagmap.pop(found_obj, None)
                if match:
                    valids.append(found_obj)
                elif exact_match:
                    err = self._format_message(
                        error_missing_message,
                        missing=namelist[itag] if namelist else tagkey.capitalize(),
                    )
                    raise CraftingValidationError(err)

            if exact_match and tagmap:
                # something is left in tagmap, that means it was never popped and
                # thus this is not an exact match
                err = self._format_message(
                    error_excess_message,
                    excess=[obj.get_display_name(looker=self.crafter) for obj in tagmap],
                )
                raise CraftingValidationError(err)

            return valids

        # get tools and consumables from self.inputs
        tool_map = {
            obj: obj.tags.get(category=self.tool_tag_category, return_list=True)
            for obj in self.inputs
            if obj
            and hasattr(obj, "tags")
            and inherits_from(obj, "evennia.objects.models.ObjectDB")
        }
        tool_map = {obj: tags for obj, tags in tool_map.items() if tags}
        consumable_map = {
            obj: obj.tags.get(category=self.consumable_tag_category, return_list=True)
            if obj
            and obj not in tool_map
        }
        consumable_map = {obj: tags for obj, tags in consumable_map.items() if tags}

        # we set these so they are available for error management at all times,
        # they will be updated with the actual values at the end
        self.validated_tools = [obj for obj in tool_map]
        self.validated_consumables = [obj for obj in consumable_map]

        tools = _check_completeness(
            tool_map,
            self.tool_tags,
            self.tool_names,
            self.exact_tools,
            self.exact_tool_order,
            self.error_tool_missing_message,
            self.error_tool_order_message,
            self.error_tool_excess_message,
        )
        consumables = _check_completeness(
            consumable_map,
            self.consumable_tags,
            self.consumable_names,
            self.exact_consumables,
            self.exact_consumable_order,
            self.error_consumable_missing_message,
            self.error_consumable_order_message,
            self.error_consumable_excess_message,
        )

        # regardless of flags, the tools/consumable lists much contain exactly
        # all the recipe needs now.
        if len(tools) != len(self.tool_tags):
            raise CraftingValidationError(
                f"Tools {tools}'s tags do not match expected tags {self.tool_tags}"
            )
        if len(consumables) != len(self.consumable_tags):
                f"Consumables {consumables}'s tags do not match "
                f"expected tags {self.consumable_tags}"
            )

        self.validated_tools = tools
        self.validated_consumables = consumables

        """
        Hook to override. This will not be called if validation in `pre_craft`
        fails.

        expected to have been verified already.

        Returns:
            list: A list of spawned objects created from the inputs, or None
                on a failure.

        Notes:
            This method should use `self.msg` to inform the user about the
            specific reason of failure immediately.
            We may want to analyze the tools in some way here to affect the
            crafting process.

        """
        return spawn(*self.output_prototypes)

    def post_craft(self, craft_result, **kwargs):
        """
        This is called just after crafting has finished. A common use of
        this method is to delete the inputs.

        Args:
            craft_result (list): The crafted result, provided by `self.do_craft`.
            **kwargs (any): Passed from `self.craft`.

        Returns:
            list: The return(s) of the craft, possibly modified in this method.

        Notes:
            This is _always_ called, also if validation in `pre_craft` fails
            (`craft_result` will then be `None`).

        """
        if craft_result:
            self.msg(self._format_message(self.success_message))
        elif self.failure_message:
            self.msg(self._format_message(self.failure_message))

        if craft_result or self.consume_on_fail:
            # consume the inputs
            for obj in self.validated_consumables:
                obj.delete()

def craft(crafter, recipe_name, *inputs, raise_exception=False, **kwargs):
    """
    Access function. Craft a given recipe from a source recipe module. A
    recipe module is a Python module containing recipe classes. Note that this
    requires `settings.CRAFT_RECIPE_MODULES` to be added to a list of one or
    more python-paths to modules holding Recipe-classes.

    Args:
        crafter (Object): The one doing the crafting.
        recipe_name (str): The `CraftRecipe.name` to use. This uses fuzzy-matching
            if the result is unique.
        *inputs: Suitable ingredients and/or tools (Objects) to use in the crafting.
        raise_exception (bool, optional): If crafting failed for whatever
            reason, raise `CraftingError`. The user will still be informed by the
            recipe.
        **kwargs: Optional kwargs to pass into the recipe (will passed into
            recipe.craft).

    Returns:
        list: Crafted objects, if any.

    Raises:
        CraftingError: If `raise_exception` is True and crafting failed to
        produce an output.  KeyError: If `recipe_name` failed to find a
        matching recipe class (or the hit was not precise enough.)

    Notes:
        If no recipe_module is given, will look for a list `settings.CRAFT_RECIPE_MODULES` and
        lastly fall back to the example module
        `"evennia.contrib.game_systems.crafting.example_recipes"`

    """
    # delayed loading/caching of recipes
    _load_recipes()

    RecipeClass = _RECIPE_CLASSES.get(recipe_name, None)
    if not RecipeClass:
        # try a startswith fuzzy match
        matches = [key for key in _RECIPE_CLASSES if key.startswith(recipe_name)]
            # try in-match
            matches = [key for key in _RECIPE_CLASSES if recipe_name in key]
        if len(matches) == 1:
            RecipeClass = matches[0]

        if raise_exception:
            raise KeyError(
                f"No recipe in settings.CRAFT_RECIPE_MODULES has a name matching {recipe_name}"
            )
        else:
            RecipeClass = functools.partial(NonExistentRecipe, name=recipe_name)
    recipe = RecipeClass(crafter, *inputs, **kwargs)
    return recipe.craft(raise_exception=raise_exception)

# craft command/cmdset

class CraftingCmdSet(CmdSet):
    """
    Store crafting command.
    """

    key = "Crafting cmdset"

        self.add(CmdCraft())

class CmdCraft(Command):
    """
    Craft an item using ingredients and tools

    Usage:
      craft <recipe> [from <ingredient>,...] [using <tool>, ...]

    Examples:
      craft snowball from snow
      craft puppet from piece of wood using knife
      craft bread from flour, butter, water, yeast using owen, bowl, roller
      craft fireball using wand, spellbook

    Notes:
        Ingredients must be in the crafter's inventory. Tools can also be
        things in the current location, like a furnace, windmill or anvil.

    """

    key = "craft"

        """
        Handle parsing of:
        ::

            <recipe> [FROM <ingredients>] [USING <tools>]

        Examples:
        ::

            craft snowball from snow
            craft puppet from piece of wood using knife
            craft bread from flour, butter, water, yeast using owen, bowl, roller
            craft fireball using wand, spellbook

        """
        self.args = args = self.args.strip().lower()
        recipe, ingredients, tools = "", "", ""

        if "from" in args:
            recipe, *rest = args.split(" from ", 1)
            rest = rest[0] if rest else ""
            ingredients, *tools = rest.split(" using ", 1)
        elif "using" in args:
            recipe, *tools = args.split(" using ", 1)
        tools = tools[0] if tools else ""

        self.recipe = recipe.strip()
        self.ingredients = [ingr.strip() for ingr in ingredients.split(",")]
        self.tools = [tool.strip() for tool in tools.split(",")]

        """
        Perform crafting.

        Will check the `craft` locktype. If a consumable/ingredient does not pass
        this check, we will check for the 'crafting_consumable_err_msg'
        Attribute, otherwise will use a default. If failing on a tool, will use
        the `crafting_tool_err_msg` if available.

        """

        if not self.args or not self.recipe:
            self.caller.msg("Usage: craft <recipe> from <ingredient>, ... [using <tool>,...]")
            return

        ingredients = []
        for ingr_key in self.ingredients:
            if not ingr_key:
                continue
            obj = caller.search(ingr_key, location=self.caller)
            # since ingredients are consumed we need extra check so we don't
            # try to include characters or accounts etc.
            if not obj:
                return
            if (
                not inherits_from(obj, "evennia.objects.models.ObjectDB")
                or obj.sessions.all()
                or not obj.access(caller, "craft", default=True)
            ):
                # We don't allow to include puppeted objects nor those with the
                # 'negative' permission 'nocraft'.
                    obj.attributes.get(
                        "crafting_consumable_err_msg",
                        default=f"{obj.get_display_name(looker=caller)} can't be used for this.",
                    )
                )
                return
            ingredients.append(obj)

        tools = []
        for tool_key in self.tools:
            if not tool_key:
                continue
            # tools are not consumed, can also exist in the current room
            obj = caller.search(tool_key)
            if not obj:
            if not obj.access(caller, "craft", default=True):
                        "crafting_tool_err_msg",
                    )
                )
                return
            tools.append(obj)

        # perform craft and make sure result is in inventory
        # (the recipe handles all returns to caller)
        result = craft(caller, self.recipe, *(tools + ingredients))
        if result:
            for obj in result:
                obj.location = caller

"""
How to make a sword - example crafting tree for the crafting system.

See the `SwordSmithingBaseRecipe` in this module for an example of extendng the
recipe with a mocked 'skill' system (just random chance in our case). The skill
system used is game-specific but likely to be needed for most 'real' crafting
systems.

Note that 'tools' are references to the tools used - they don't need to be in
the inventory of the crafter. So when 'blast furnace' is given below, it is a
reference to a blast furnace used, not suggesting the crafter is carrying it
around with them.

## Sword crafting tree

::

    # base materials (consumables)

    iron ore, ash, sand, coal, oak wood, water, fur

    # base tools (marked with [T] for clarity and assumed to already exist)

    blast furnace[T], furnace[T], crucible[T], anvil[T],
    hammer[T], knife[T], cauldron[T]

    # recipes for making a sword

    pig iron = iron ore + 2xcoal + blast furnace[T]
    crucible_steel = pig iron + ash + sand + 2xcoal + crucible[T]
    sword blade = crucible steel + hammer[T] + anvil[T] + furnace[T]
    sword pommel = crucible steel + hammer[T] + anvil[T] + furnace[T]
    sword guard = crucible steel + hammer[T] + anvil[T] + furnace[T]

    rawhide = fur + knife[T]
    oak bark + cleaned oak wood = oak wood + knife[T]
    leather = rawhide + oak bark + water + cauldron[T]

    sword handle = cleaned oak wood + knife[T]

    sword = sword blade + sword guard + sword pommel
            + sword handle + leather + knife[T] + hammer[T] + furnace[T]

## Recipes used for spell casting

This is a simple example modifying the base Recipe to use as a way
to describe magical spells instead. It combines tools with
a skill (an attribute on the caster) in order to produce a magical effect.

The example `CmdCast` command can be added to the CharacterCmdset in
`mygame/commands/default_cmdsets` to test it out. The 'effects' are
just mocked for the example.

::
    # base tools (assumed to already exist)

    spellbook[T], wand[T]

    # skill (stored as Attribute on caster)

    firemagic skill level10+

    # recipe for fireball

    fireball = spellbook[T] + wand[T] + [firemagic skill lvl10+]

----

"""

from random import randint, random

from .crafting import CraftingRecipe, CraftingValidationError, craft

# Sword recipe

class PigIronRecipe(CraftingRecipe):
    """
    Pig iron is a high-carbon result of melting iron in a blast furnace.

    """

    name = "pig iron"
    tool_tags = ["blast furnace"]
    consumable_tags = ["iron ore", "coal", "coal"]
    output_prototypes = [
        {
            "key": "Pig Iron ingot",
            "desc": "An ingot of crude pig iron.",
            "tags": [("pig iron", "crafting_material")],
        }
    ]

class CrucibleSteelRecipe(CraftingRecipe):
    """
    Mixing pig iron with impurities like ash and sand and melting it in a
    crucible produces a medieval level of steel (like damascus steel).

    """

    name = "crucible steel"
    tool_tags = ["crucible"]
    consumable_tags = ["pig iron", "ash", "sand", "coal", "coal"]
        {
            "key": "Crucible steel ingot",
            "desc": "An ingot of multi-colored crucible steel.",
            "tags": [("crucible steel", "crafting_material")],
        }
    ]

class _SwordSmithingBaseRecipe(CraftingRecipe):
    """
    A parent for all metallurgy sword-creation recipes. Those have a chance to
    failure but since steel is not lost in the process you can always try
    again.

    """

    success_message = "Your smithing work bears fruit and you craft {outputs}!"
    failed_message = (
        "You work and work but you are not happy with the result. You need to start over."
    )

    def craft(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
        against a skill found on `self.crafter`). In this case you can always
        start over since steel is not lost but can be re-smelted again for
        another try.

        Args:
            validated_inputs (list): all consumables/tools being used.
            **kwargs: any extra kwargs passed during crafting.

        Returns:
            any: The result of the craft, or None if a failure.

        Notes:
            Depending on if we return a crafting result from this
            method or not, `success_message` or `failure_message`
            will be echoed to the crafter.

            (for more control we could also message directly and raise
            crafting.CraftingError to abort craft process on failure).

        """
        if random.random() < 0.8:
            # 80% chance of success. This will spawn the sword and show
            # success-message.
            return super().craft(**kwargs)
        else:
            # fail and show failed message

class SwordBladeRecipe(_SwordSmithingBaseRecipe):
    """
    A [sword]blade requires hammering the steel out into shape using heat and
    force. This also includes the tang, which is the base for the hilt (the
    part of the sword you hold on to).

    """

    name = "sword blade"
    tool_tags = ["hammer", "anvil", "furnace"]
    consumable_tags = ["crucible steel"]
        {
            "key": "Sword blade",
            "desc": "A long blade that may one day become a sword.",
            "tags": [("sword blade", "crafting_material")],
        }
    ]

class SwordPommelRecipe(_SwordSmithingBaseRecipe):
    """
    The pommel is the 'button' or 'ball' etc the end of the sword hilt, holding
    it together.

    """

    name = "sword pommel"
        {
            "key": "Sword pommel",
            "desc": "The pommel for a future sword.",
            "tags": [("sword pommel", "crafting_material")],
        }
    ]

class SwordGuardRecipe(_SwordSmithingBaseRecipe):
    """
    The guard stops the hand from accidentally sliding off the hilt onto the
    sword's blade and also protects the hand when parrying.

    """

    name = "sword guard"
        {
            "key": "Sword guard",
            "desc": "The cross-guard for a future sword.",
            "tags": [("sword guard", "crafting_material")],
        }
    ]

class RawhideRecipe(CraftingRecipe):
    """
    Rawhide is animal skin cleaned and stripped of hair.

    """

    name = "rawhide"
    tool_tags = ["knife"]
    consumable_tags = ["fur"]
        {
            "key": "Rawhide",
            "desc": "Animal skin, cleaned and with hair removed.",
            "tags": [("rawhide", "crafting_material")],
        }
    ]

class OakBarkRecipe(CraftingRecipe):
    """
    The actual thing needed for tanning leather is Tannin, but we skip
    the step of refining tannin from the bark and use the bark as-is.

    This produces two outputs - the bark and the cleaned wood.
    """

    name = "oak bark"
    consumable_tags = ["oak wood"]
        {
            "key": "Oak bark",
            "desc": "Bark of oak, stripped from the core wood.",
            "tags": [("oak bark", "crafting_material")],
        },
        {
            "key": "Oak Wood (cleaned)",
            "desc": "Oakwood core, stripped of bark.",
            "tags": [("cleaned oak wood", "crafting_material")],
        },
    ]

class LeatherRecipe(CraftingRecipe):
    """
    Leather is produced by tanning rawhide in a process traditionally involving
    the chemical Tannin. Here we abbreviate this process a bit. Maybe a
    'tanning rack' tool should be required too ...

    """

    name = "leather"
    tool_tags = ["cauldron"]
    consumable_tags = ["rawhide", "oak bark", "water"]
        {
            "key": "Piece of Leather",
            "desc": "A piece of leather.",
            "tags": [("leather", "crafting_material")],
        }
    ]

class SwordHandleRecipe(CraftingRecipe):
    """
    The handle is the part of the hilt between the guard and the pommel where
    you hold the sword. It consists of wooden pieces around the steel tang. It
    is wrapped in leather, but that will be added at the end.

    """

    name = "sword handle"
    consumable_tags = ["cleaned oak wood"]
        {
            "key": "Sword handle",
            "desc": "Two pieces of wood to be be fitted onto a sword's tang as its handle.",
            "tags": [("sword handle", "crafting_material")],
        }
    ]

class SwordRecipe(_SwordSmithingBaseRecipe):
    """
    A finished sword consists of a Blade ending in a non-sharp part called the
    Tang. The cross Guard is put over the tang against the edge of the blade.
    The Handle is put over the tang to give something easier to hold. The
    Pommel locks everything in place. The handle is wrapped in leather
    strips for better grip.

    This covers only a single 'sword' type.

    """

    name = "sword"
    tool_tags = ["hammer", "furnace", "knife"]
    consumable_tags = ["sword blade", "sword guard", "sword pommel", "sword handle", "leather"]
        {
            "key": "Sword",
            "desc": "A bladed weapon.",
            # setting the tag as well - who knows if one can make something from this too!
            "tags": [("sword", "crafting_material")],
        }
        # obviously there would be other properties of a 'sword' added here
        # too, depending on how combat works in the your game!
    ]
    # this requires more precision
    exact_consumable_order = True

# Recipes for spell casting

class _MagicRecipe(CraftingRecipe):
    """
    A base 'recipe' to represent magical spells.

    We *could* treat this just like the sword above - by combining the wand and spellbook to make a
    fireball object that the user can then throw with another command. For this example we instead
    generate 'magical effects' as strings+values that we would then supposedly inject into a
    combat system or other resolution system.

    We also assume that the crafter has skills set on itself as plain Attributes.

    """

    name = ""
    # all spells require a spellbook and a wand (so there!)
    tool_tags = ["spellbook", "wand"]

    error_tool_missing_message = "Cannot cast spells without {missing}."
    success_message = "You successfully cast the spell!"
    # custom properties
    skill_requirement = []  # this should be on the form [(skillname, min_level)]
    skill_roll = ""  # skill to roll for success
    desired_effects = []  # on the form [(effect, value), ...]
    failure_effects = []  # ''
    error_too_low_skill_level = "Your skill {skill_name} is too low to cast {spell}."
    error_no_skill_roll = "You must have the skill {skill_name} to cast the spell {spell}."

        """
        This is where we do input validation. We want to do the
        normal validation of the tools, but also check for a skill
        on the crafter. This must set the result on `self.validated_inputs`.
        We also set the crafter's relevant skill value on `self.skill_roll_value`.

        Args:

        Raises:

        """
        # this will check so the spellbook and wand are at hand.
        super().pre_craft(**kwargs)

        # at this point we have the items available, let's also check for the skill. We
        # assume the  crafter has the skill available as an Attribute
        # on itself.

        crafter = self.crafter
        for skill_name, min_value in self.skill_requirements:
            skill_value = crafter.attributes.get(skill_name)

            if skill_value is None or skill_value < min_value:
                self.msg(
                    self.error_too_low_skill_level.format(skill_name=skill_name, spell=self.name)
                )
                raise CraftingValidationError

        # get the value of the skill to roll
        self.skill_roll_value = self.crafter.attributes.get(self.skill_roll)
        if self.skill_roll_value is None:
            self.msg(self.error_no_skill_roll.format(skill_name=self.skill_roll, spell=self.name))

        """
        'Craft' the magical effect. When we get to this point we already know we have all the
        prequisite for creating the effect. In this example we will store the effect on the crafter;
        maybe this enhances the crafter or makes a new attack available to them in combat.

        An alternative to this would of course be to spawn an actual object for the effect, like
        creating a potion or an actual fireball-object to throw (this depends on how your combat
        works).

        """
        # we do a simple skill check here.
        if randint(1, 18) <= self.skill_roll_value:
            # a success!
            return True, self.desired_effects
        else:
            # a failure!
            return False, self.failure_effects

        """
        Always called at the end of crafting, regardless of successful or not.

        Since we get a custom craft result (True/False, effects) we need to
        wrap the original post_craft to output the error messages for us
        correctly.

        """
        success = False
            success, _ = craft_result
        # default post_craft just checks if craft_result is truthy or not.
        # we don't care about its return value since we already have craft_result.
        super().post_craft(success, **kwargs)

class FireballRecipe(_MagicRecipe):
    """
    A Fireball is a magical effect that can be thrown at a target to cause damage.

    Note that the magic-effects are just examples, an actual rule system would
    need to be created to understand what they mean when used.

    """

    name = "fireball"
    skill_requirements = [("firemagic", 10)]  # skill 'firemagic' lvl 10 or higher
    skill_roll = "firemagic"
    success_message = "A ball of flame appears!"
    desired_effects = [("target_fire_damage", 25), ("ranged_attack", -2), ("mana_cost", 12)]
    failure_effects = [("self_fire_damage", 5), ("mana_cost", 5)]

class HealingRecipe(_MagicRecipe):
    """
    Healing magic will restore a certain amount of health to the target over time.

    need to be created to understand what they mean.

    """

    name = "heal"
    skill_requirements = [("bodymagic", 5), ("empathy", 10)]
    skill_roll = "bodymagic"
    success_message = "You successfully extend your healing aura."
    desired_effects = [("healing", 15), ("mana_cost", 5)]
    failure_effects = []

class CmdCast(Command):
    """
    Cast a magical spell.

    Usage:
        cast <spell> <target>

    """

    key = "cast"

        """
        Simple parser, assuming spellname doesn't have spaces.
        Stores result in self.target and self.spellname.

        """
        args = self.args.strip().lower()
        target = None
        if " " in args:
            self.spellname, *target = args.split(" ", 1)
        else:
            self.spellname = args

        if not self.spellname:
            self.caller.msg("You must specify a spell name.")

        if target:
            self.target = self.caller.search(target[0].strip())
            if not self.target:
        else:
            self.target = self.caller

        # all items carried by the caller could work
        possible_tools = self.caller.contents

        try:
            # if this completes without an exception, the caster will have
            # a new magic_effect set on themselves, ready to use or apply in some way.
            success, effects = craft(
                self.caller, self.spellname, *possible_tools, raise_exception=True
            )
        except CraftingValidationError:
            return
            self.caller.msg(f"You don't know of a spell called '{self.spellname}'")
            return

        # Applying the magical effect to target would happen below.
        # self.caller.db.active_spells[self.spellname] holds all the effects
        # of this particular prepared spell. For a fireball you could perform
        # an attack roll here and apply damage if you hit. For healing you would heal the target
        # (which could be yourself) by a number of health points given by the recipe.
        effect_txt = ", ".join(f"{eff[0]}({eff[1]})" for eff in effects)
        success_txt = "|gsucceeded|n" if success else "|rfailed|n"
        self.caller.msg(
            f"Casting the spell {self.spellname} on {self.target} {success_txt}, "
            f"causing the following effects: {effect_txt}."
        )

"""
Unit tests for the crafting system contrib.

"""

from unittest import mock

from django.core.exceptions import ObjectDoesNotExist

from evennia.utils.test_resources import BaseEvenniaTestCase

from . import crafting, example_recipes

class TestCraftUtils(BaseEvenniaTestCase):
    """
    Test helper utils for crafting.

    """

    @override_settings(CRAFT_RECIPE_MODULES=[])
    def test_load_recipes(self):
        """This should only load the example module now"""

        crafting._load_recipes()
            crafting._RECIPE_CLASSES,
            {
                "crucible steel": example_recipes.CrucibleSteelRecipe,
                "leather": example_recipes.LeatherRecipe,
                "fireball": example_recipes.FireballRecipe,
                "heal": example_recipes.HealingRecipe,
                "oak bark": example_recipes.OakBarkRecipe,
                "pig iron": example_recipes.PigIronRecipe,
                "rawhide": example_recipes.RawhideRecipe,
                "sword": example_recipes.SwordRecipe,
                "sword blade": example_recipes.SwordBladeRecipe,
                "sword guard": example_recipes.SwordGuardRecipe,
                "sword handle": example_recipes.SwordHandleRecipe,
                "sword pommel": example_recipes.SwordPommelRecipe,
            },
        )

class _TestMaterial:
    def __init__(self, name):

        return self.name

class TestCraftingRecipeBase(BaseEvenniaTestCase):
    """
    Test the parent recipe class.
    """

        self.crafter = mock.MagicMock()
        self.crafter.msg = mock.MagicMock()

        self.inp1 = _TestMaterial("test1")
        self.inp2 = _TestMaterial("test2")
        self.inp3 = _TestMaterial("test3")

        self.kwargs = {"kw1": 1, "kw2": 2}

        self.recipe = crafting.CraftingRecipeBase(
            self.crafter, self.inp1, self.inp2, self.inp3, **self.kwargs
        )

    def test_msg(self):
        """Test messaging to crafter"""

        self.recipe.msg("message")
        self.crafter.msg.assert_called_with("message", {"type": "crafting"})

    def test_pre_craft(self):
        """Test validating hook"""
        self.recipe.pre_craft()
        self.assertEqual(self.recipe.validated_inputs, (self.inp1, self.inp2, self.inp3))

    def test_pre_craft_fail(self):
        """Should rase error if validation fails"""
        self.recipe.allow_craft = False
        with self.assertRaises(crafting.CraftingValidationError):
            self.recipe.pre_craft()

    def test_craft_hook__succeed(self):
        """Test craft hook, the main access method."""

        expected_result = _TestMaterial("test_result")
        self.recipe.do_craft = mock.MagicMock(return_value=expected_result)

        self.assertTrue(self.recipe.allow_craft)

        result = self.recipe.craft()

        # check result
        self.assertEqual(result, expected_result)
        self.recipe.do_craft.assert_called_with(kw1=1, kw2=2)

        # since allow_reuse is False, this usage should now be turned off
        self.assertFalse(self.recipe.allow_craft)
        # trying to re-run again should fail since rerun is False
        with self.assertRaises(crafting.CraftingError):
            self.recipe.craft()

    def test_craft_hook__fail(self):
        """Test failing the call"""

        self.recipe.do_craft = mock.MagicMock(return_value=None)

        # trigger exception
            self.recipe.craft(raise_exception=True)

        # reset and try again without exception
        self.recipe.allow_craft = True
        self.assertEqual(result, None)

class _MockRecipe(crafting.CraftingRecipe):
    name = "testrecipe"
    tool_tags = ["tool1", "tool2"]
    consumable_tags = ["cons1", "cons2", "cons3"]
        {
            "key": "Result1",
            "prototype_key": "resultprot",
            "tags": [("result1", "crafting_material")],
        }
    ]

@override_settings(CRAFT_RECIPE_MODULES=[])
class TestCraftingRecipe(BaseEvenniaTestCase):
    """
    Test the CraftingRecipe class with one recipe
    """

        self.tool1 = create_object(key="tool1", tags=[("tool1", "crafting_tool")], nohome=True)
        self.tool2 = create_object(key="tool2", tags=[("tool2", "crafting_tool")], nohome=True)
        self.cons1 = create_object(key="cons1", tags=[("cons1", "crafting_material")], nohome=True)
        self.cons2 = create_object(key="cons2", tags=[("cons2", "crafting_material")], nohome=True)
        self.cons3 = create_object(key="cons3", tags=[("cons3", "crafting_material")], nohome=True)

        try:
            self.tool1.delete()
            self.tool2.delete()
            self.cons1.delete()
            self.cons2.delete()
            self.cons3.delete()
        except ObjectDoesNotExist:
            pass

    def test_error_format(self):
        """Test the automatic error formatter"""
        recipe = _MockRecipe(
            self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3
        )

        msg = "{missing},{tools},{consumables},{inputs},{outputs}" "{i0},{i1},{o0}"
        kwargs = {
            "missing": "foo",
            "tools": ["bar", "bar2", "bar3"],
            "consumables": ["cons1", "cons2"],
        }

        expected = {
            "i0": "cons1",
            "i1": "cons2",
            "i2": "cons3",
            "o0": "Result1",
            "tools": "bar, bar2, and bar3",
            "consumables": "cons1 and cons2",
            "inputs": "cons1, cons2, and cons3",
            "outputs": "Result1",
        }

        result = recipe._format_message(msg, **kwargs)
        self.assertEqual(result, msg.format_map(expected))

    def test_craft__success(self):
        """Test to create a result from the recipe"""
        )

        self.assertEqual(result[0].key, "Result1")
        self.assertEqual(result[0].tags.all(), ["result1", "resultprot"])
        self.crafter.msg.assert_called_with(
            recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
        )

        # make sure consumables are gone
        self.assertIsNone(self.cons1.pk)
        self.assertIsNone(self.cons2.pk)
        self.assertIsNone(self.cons3.pk)
        # make sure tools remain
        self.assertIsNotNone(self.tool1.pk)
        self.assertIsNotNone(self.tool2.pk)

    def test_seed__success(self):
        """Test seed helper classmethod"""

        # needed for other dbs to pass seed
        homeroom = create_object(key="HomeRoom", nohome=True)

        # call classmethod directly
        with override_settings(DEFAULT_HOME=f"#{homeroom.id}"):
            tools, consumables = _MockRecipe.seed()

        # this should be a normal successful crafting
        recipe = _MockRecipe(self.crafter, *(tools + consumables))

        )

        for cons in consumables:
            self.assertIsNone(cons.pk)
        for tool in tools:
            self.assertIsNotNone(tool.pk)

    def test_craft_missing_tool__fail(self):
        """Fail craft by missing tool2"""
        recipe = _MockRecipe(self.crafter, self.tool1, self.cons1, self.cons2, self.cons3)
        self.assertFalse(result)
            recipe.error_tool_missing_message.format(outputs="Result1", missing="tool2"),
            {"type": "crafting"},
        )

        # make sure consumables are still there
        self.assertIsNotNone(self.cons1.pk)
        self.assertIsNotNone(self.cons2.pk)
        self.assertIsNotNone(self.cons3.pk)

    def test_craft_missing_cons__fail(self):
        """Fail craft by missing cons3"""
        recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2)
            recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
        )

    def test_craft_missing_cons__always_consume__fail(self):
        """Fail craft by missing cons3, with always-consume flag"""

        cons4 = create_object(key="cons4", tags=[("cons4", "crafting_material")], nohome=True)

        recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, cons4)
        recipe.consume_on_fail = True

        )

        # make sure consumables are deleted even though we failed
        # the extra should also be gone
        self.assertIsNone(cons4.pk)
        # but cons3 should be fine since it was not included
        # make sure tools remain as normal

    def test_craft_wrong_tool__fail(self):
        """Fail craft by including a wrong tool"""

        wrong = create_object(key="wrong", tags=[("wrongtool", "crafting_tool")], nohome=True)

        recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, wrong)
            recipe.error_tool_excess_message.format(
                outputs="Result1", excess=wrong.get_display_name(looker=self.crafter)
            ),
        )

    def test_craft_tool_excess__fail(self):
        """Fail by too many consumables"""

        # note that this is a valid tag!
        tool3 = create_object(key="tool3", tags=[("tool2", "crafting_tool")], nohome=True)

            self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3
        )
                outputs="Result1", excess=tool3.get_display_name(looker=self.crafter)
            ),
        )

        self.assertIsNotNone(tool3.pk)

    def test_craft_cons_excess__fail(self):

        cons4 = create_object(key="cons4", tags=[("cons3", "crafting_material")], nohome=True)

            self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4
        )
            recipe.error_consumable_excess_message.format(
                outputs="Result1", excess=cons4.get_display_name(looker=self.crafter)
            ),
        )

        self.assertIsNotNone(cons4.pk)

    def test_craft_tool_excess__sucess(self):
        """Allow too many consumables"""

        )
        recipe.exact_tools = False
        self.assertTrue(result)
        )

    def test_craft_cons_excess__sucess(self):

        )
        recipe.exact_consumables = False
        )

    def test_craft_tool_order__fail(self):
        """Strict tool-order recipe fail"""
            self.crafter, self.tool2, self.tool1, self.cons1, self.cons2, self.cons3
        )
        recipe.exact_tool_order = True
            recipe.error_tool_order_message.format(
                outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter)
            ),
        )

    def test_craft_cons_order__fail(self):
            self.crafter, self.tool1, self.tool2, self.cons3, self.cons2, self.cons1
        )
        recipe.exact_consumable_order = True
            recipe.error_consumable_order_message.format(
                outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter)
            ),
        )

class TestCraftSword(BaseEvenniaTestCase):
    """
    Test the `craft` function by crafting the example sword.

    """

    @override_settings(CRAFT_RECIPE_MODULES=[], DEFAULT_HOME="#999999")
    @mock.patch("evennia.contrib.game_systems.crafting.example_recipes.random")
    def test_craft_sword(self, mockrandom):
        """
        Craft example sword. For the test, every crafting works.

        """
        # make sure every craft succeeds
        mockrandom.random = mock.MagicMock(return_value=0.2)

        def _co(key, tagkey, is_tool=False):
            tagcat = "crafting_tool" if is_tool else "crafting_material"
            return create_object(key=key, tags=[(tagkey, tagcat)], nohome=True)

        def _craft(recipe_name, *inputs):
            """shortcut to shorten and return only one element"""
            result = crafting.craft(self.crafter, recipe_name, *inputs, raise_exception=True)
            return result[0] if len(result) == 1 else result

        # generate base materials
        iron_ore1 = _co("Iron ore ingot", "iron ore")
        iron_ore2 = _co("Iron ore ingot", "iron ore")
        iron_ore3 = _co("Iron ore ingot", "iron ore")

        ash1 = _co("Pile of Ash", "ash")
        ash2 = _co("Pile of Ash", "ash")
        ash3 = _co("Pile of Ash", "ash")

        sand1 = _co("Pile of sand", "sand")
        sand2 = _co("Pile of sand", "sand")
        sand3 = _co("Pile of sand", "sand")

        coal01 = _co("Pile of coal", "coal")
        coal02 = _co("Pile of coal", "coal")
        coal03 = _co("Pile of coal", "coal")
        coal04 = _co("Pile of coal", "coal")
        coal05 = _co("Pile of coal", "coal")
        coal06 = _co("Pile of coal", "coal")
        coal07 = _co("Pile of coal", "coal")
        coal08 = _co("Pile of coal", "coal")
        coal09 = _co("Pile of coal", "coal")
        coal10 = _co("Pile of coal", "coal")
        coal11 = _co("Pile of coal", "coal")
        coal12 = _co("Pile of coal", "coal")

        oak_wood = _co("Pile of oak wood", "oak wood")
        water = _co("Bucket of water", "water")
        fur = _co("Bundle of Animal fur", "fur")

        # tools
        blast_furnace = _co("Blast furnace", "blast furnace", is_tool=True)
        furnace = _co("Smithing furnace", "furnace", is_tool=True)
        crucible = _co("Smelting crucible", "crucible", is_tool=True)
        anvil = _co("Smithing anvil", "anvil", is_tool=True)
        hammer = _co("Smithing hammer", "hammer", is_tool=True)
        knife = _co("Working knife", "knife", is_tool=True)
        cauldron = _co("Cauldron", "cauldron", is_tool=True)

        # making pig iron
        inputs = [iron_ore1, coal01, coal02, blast_furnace]
        pig_iron1 = _craft("pig iron", *inputs)

        inputs = [iron_ore2, coal03, coal04, blast_furnace]
        pig_iron2 = _craft("pig iron", *inputs)

        inputs = [iron_ore3, coal05, coal06, blast_furnace]
        pig_iron3 = _craft("pig iron", *inputs)

        # making crucible steel
        inputs = [pig_iron1, ash1, sand1, coal07, coal08, crucible]
        crucible_steel1 = _craft("crucible steel", *inputs)

        inputs = [pig_iron2, ash2, sand2, coal09, coal10, crucible]
        crucible_steel2 = _craft("crucible steel", *inputs)

        inputs = [pig_iron3, ash3, sand3, coal11, coal12, crucible]
        crucible_steel3 = _craft("crucible steel", *inputs)

        # smithing
        inputs = [crucible_steel1, hammer, anvil, furnace]
        sword_blade = _craft("sword blade", *inputs)

        inputs = [crucible_steel2, hammer, anvil, furnace]
        sword_pommel = _craft("sword pommel", *inputs)

        inputs = [crucible_steel3, hammer, anvil, furnace]
        sword_guard = _craft("sword guard", *inputs)

        # stripping fur
        inputs = [fur, knife]
        rawhide = _craft("rawhide", *inputs)

        # making bark (tannin) and cleaned wood
        inputs = [oak_wood, knife]
        oak_bark, cleaned_oak_wood = _craft("oak bark", *inputs)

        # leathermaking
        inputs = [rawhide, oak_bark, water, cauldron]
        leather = _craft("leather", *inputs)

        # sword handle
        inputs = [cleaned_oak_wood, knife]
        sword_handle = _craft("sword handle", *inputs)

        # sword (order matters)
        inputs = [
            sword_blade,
            sword_guard,
            sword_pommel,
            sword_handle,
            leather,
            knife,
            hammer,
            furnace,
        ]
        sword = _craft("sword", *inputs)

        self.assertEqual(sword.key, "Sword")

        # make sure all materials and intermediaries are deleted
        self.assertIsNone(iron_ore1.pk)
        self.assertIsNone(iron_ore2.pk)
        self.assertIsNone(iron_ore3.pk)
        self.assertIsNone(ash1.pk)
        self.assertIsNone(ash2.pk)
        self.assertIsNone(ash3.pk)
        self.assertIsNone(sand1.pk)
        self.assertIsNone(sand2.pk)
        self.assertIsNone(sand3.pk)
        self.assertIsNone(coal01.pk)
        self.assertIsNone(coal02.pk)
        self.assertIsNone(coal03.pk)
        self.assertIsNone(coal04.pk)
        self.assertIsNone(coal05.pk)
        self.assertIsNone(coal06.pk)
        self.assertIsNone(coal07.pk)
        self.assertIsNone(coal08.pk)
        self.assertIsNone(coal09.pk)
        self.assertIsNone(coal10.pk)
        self.assertIsNone(coal11.pk)
        self.assertIsNone(coal12.pk)
        self.assertIsNone(oak_wood.pk)
        self.assertIsNone(water.pk)
        self.assertIsNone(fur.pk)
        self.assertIsNone(pig_iron1.pk)
        self.assertIsNone(pig_iron2.pk)
        self.assertIsNone(pig_iron3.pk)
        self.assertIsNone(crucible_steel1.pk)
        self.assertIsNone(crucible_steel2.pk)
        self.assertIsNone(crucible_steel3.pk)
        self.assertIsNone(sword_blade.pk)
        self.assertIsNone(sword_pommel.pk)
        self.assertIsNone(sword_guard.pk)
        self.assertIsNone(rawhide.pk)
        self.assertIsNone(oak_bark.pk)
        self.assertIsNone(leather.pk)
        self.assertIsNone(sword_handle.pk)

        # make sure all tools remain
        self.assertIsNotNone(blast_furnace)
        self.assertIsNotNone(furnace)
        self.assertIsNotNone(crucible)
        self.assertIsNotNone(anvil)
        self.assertIsNotNone(hammer)
        self.assertIsNotNone(knife)
        self.assertIsNotNone(cauldron)

@mock.patch("evennia.contrib.game_systems.crafting.crafting._load_recipes", new=mock.MagicMock())
@mock.patch(
    "evennia.contrib.game_systems.crafting.crafting._RECIPE_CLASSES",
    new={"testrecipe": _MockRecipe},
)
class TestCraftCommand(BaseEvenniaCommandTest):
    """Test the crafting command"""

        tools, consumables = _MockRecipe.seed(
            tool_kwargs={"location": self.char1}, consumable_kwargs={"location": self.char1}
        )

        "Successfully craft using command"
            crafting.CmdCraft(),
            "testrecipe from cons1, cons2, cons3 using tool1, tool2",
            _MockRecipe.success_message.format(outputs="Result1"),
        )

    def test_craft__notools__failure(self):
        "Craft fail no tools"
            "testrecipe from cons1, cons2, cons3",
            _MockRecipe.error_tool_missing_message.format(outputs="Result1", missing="tool1"),
        )

    def test_craft__nocons__failure(self):
            "testrecipe using tool1, tool2",
            _MockRecipe.error_consumable_missing_message.format(outputs="Result1", missing="cons1"),
        )

    def test_craft__unknown_recipe__failure(self):
            "nonexistent from cons1, cons2, cons3 using tool1, tool2",
            "Unknown recipe 'nonexistent'",
        )

"""
Gendersub

Griatch 2015

This is a simple gender-aware Character class for allowing users to
insert custom markers in their text to indicate gender-aware
messaging. It relies on a modified msg() and is meant as an
inspiration and starting point to how to do stuff like this.

An object can have the following genders:
 - male (he/his)
 - female (her/hers)
 - neutral (it/its)
 - ambiguous (they/them/their/theirs)

Usage

When in use, messages can contain special tags to indicate pronouns gendered
based on the one being addressed. Capitalization will be retained.

- `|s`, `|S`: Subjective form: he, she, it, He, She, It, They
- `|o`, `|O`: Objective form: him, her, it, Him, Her, It, Them
- `|p`, `|P`: Possessive form: his, her, its, His, Her, Its, Their
- `|a`, `|A`: Absolute Possessive form: his, hers, its, His, Hers, Its, Theirs

For example,

```
char.msg("%s falls on |p face with a thud." % char.key)
"Tom falls on his face with a thud"
```

The default gender is "ambiguous" (they/them/their/theirs).

To use, have DefaultCharacter inherit from this, or change
setting.DEFAULT_CHARACTER to point to this class.

The `gender` command is used to set the gender. It needs to be added to the
default cmdset before it becomes available.

"""

import re

from evennia import Command, DefaultCharacter

# gender maps

_GENDER_PRONOUN_MAP = {
    "male": {"s": "he", "o": "him", "p": "his", "a": "his"},
    "female": {"s": "she", "o": "her", "p": "her", "a": "hers"},
    "neutral": {"s": "it", "o": "it", "p": "its", "a": "its"},
    "ambiguous": {"s": "they", "o": "them", "p": "their", "a": "theirs"},
}
_RE_GENDER_PRONOUN = re.compile(r"(?<!\|)\|(?!\|)[sSoOpPaA]")

# in-game command for setting the gender

class SetGender(Command):
    """
    Sets gender on yourself

    Usage:
      @gender male || female || neutral || ambiguous

    """

    key = "gender"
    aliases = "sex"
    locks = "call:all()"

        """
        Implements the command.
        """
        arg = self.args.strip().lower()
        if arg not in ("male", "female", "neutral", "ambiguous"):
            caller.msg("Usage: @gender male||female||neutral||ambiguous")
            return
        caller.db.gender = arg
        caller.msg("Your gender was set to %s." % arg)

# Gender-aware character class

class GenderCharacter(DefaultCharacter):
    """
    This is a Character class aware of gender.

    """

        """
        Called once when the object is created.
        """
        self.db.gender = "ambiguous"

    def _get_pronoun(self, regex_match):
        """
        Get pronoun from the pronoun marker in the text. This is used as
        the callable for the re.sub function.

        Args:
            regex_match (MatchObject): the regular expression match.

        Notes:
            - `|s`, `|S`: Subjective form: he, she, it, He, She, It, They
            - `|o`, `|O`: Objective form: him, her, it, Him, Her, It, Them
            - `|p`, `|P`: Possessive form: his, her, its, His, Her, Its, Their
            - `|a`, `|A`: Absolute Possessive form: his, hers, its, His, Hers, Its, Theirs

        """
        typ = regex_match.group()[1]  # "s", "O" etc
        gender = self.attributes.get("gender", default="ambiguous")
        gender = gender if gender in ("male", "female", "neutral") else "ambiguous"
        pronoun = _GENDER_PRONOUN_MAP[gender][typ.lower()]
        return pronoun.capitalize() if typ.isupper() else pronoun

    def msg(self, text=None, from_obj=None, session=None, **kwargs):
        """
        Emits something to a session attached to the object.
        Overloads the default msg() implementation to include
        gender-aware markers in output.

        Args:
            from_obj (obj, optional): object that is sending. If
                given, at_msg_send will be called
            session (Session or list, optional): session or list of
                sessions to relay to, if any. If set, will
                force send regardless of MULTISESSION_MODE.
        Notes:
            `at_msg_receive` will be called on this Object.
            All extra kwargs will be passed on to the protocol.

        """
        if text is None:
            super().msg(from_obj=from_obj, session=session, **kwargs)
            return

        try:
            if text and isinstance(text, tuple):
                text = (_RE_GENDER_PRONOUN.sub(self._get_pronoun, text[0]), *text[1:])
            else:
                text = _RE_GENDER_PRONOUN.sub(self._get_pronoun, text)
            pass
        super().msg(text, from_obj=from_obj, session=session, **kwargs)

"""
Test gendersub contrib.

"""

from . import gendersub

class TestGenderSub(BaseEvenniaCommandTest):
    def test_setgender(self):
        self.call(gendersub.SetGender(), "male", "Your gender was set to male.")
        self.call(gendersub.SetGender(), "ambiguous", "Your gender was set to ambiguous.")
        self.call(gendersub.SetGender(), "Foo", "Usage: @gender")

    def test_gendercharacter(self):
        char = create_object(gendersub.GenderCharacter, key="Gendered", location=self.room1)
        txt = "Test |p gender"
            gendersub._RE_GENDER_PRONOUN.sub(char._get_pronoun, txt), "Test their gender"
        )
            "evennia.contrib.game_systems.gendersub.gendersub.DefaultCharacter.msg"
        ) as mock_msg:
            char.db.gender = "female"
            char.msg("Test |p gender")
            mock_msg.assert_called_with("Test her gender", from_obj=None, session=None)

"""
In-Game Mail system

Evennia Contribution - grungies1138 2016

A simple Brandymail style @mail system that uses the Msg class from Evennia
Core. It has two Commands, both of which can be used on their own:

   - CmdMail - this should sit on the Account cmdset and makes the `mail` command
    available both IC and OOC. Mails will always go to Accounts (other players).
   - CmdMailCharacter - this should sit on the Character cmdset and makes the `mail`
    command ONLY available when puppeting a character. Mails will be sent to other
    Characters only and will not be available when OOC.
   - If adding *both* commands to their respective cmdsets, you'll get two separate
    IC and OOC mailing systems, with different lists of mail for IC and OOC modes.

Install one or both of the following (see above):

- CmdMail (IC + OOC mail, sent between players)

    # mygame/commands/default_cmds.py

    from evennia.contrib.game_systems import mail

    # in AccountCmdSet.at_cmdset_creation:
        self.add(mail.CmdMail())

- CmdMailCharacter (optional, IC only mail, sent between characters)

    # in CharacterCmdSet.at_cmdset_creation:
        self.add(mail.CmdMailCharacter())

Once installed, use `help mail` in game for help with the mail command. Use
ic/ooc to switch in and out of IC/OOC modes.

"""

import re

from evennia import AccountDB, ObjectDB, default_cmds
from evennia.utils import create, datetime_format, evtable, inherits_from, make_iter

_HEAD_CHAR = "|015-|n"
_SUB_HEAD_CHAR = "-"
_WIDTH = 78

class CmdMail(default_cmds.MuxAccountCommand):
    """
    Communicate with others by sending mail.

    Usage:
      @mail       - Displays all the mail an account has in their mailbox
      @mail <#>   - Displays a specific message
      @mail <accounts>=<subject>/<message>
              - Sends a message to the comma separated list of accounts.
      @mail/delete <#> - Deletes a specific message
      @mail/forward <account list>=<#>[/<Message>]
              - Forwards an existing message to the specified list of accounts,
                original message is delivered with optional Message prepended.
      @mail/reply <#>=<message>
              - Replies to a message #. Prepends message to the original
                message text.
    Switches:
      delete  - deletes a message
      forward - forward a received message to another object with an optional message attached.
      reply   - Replies to a received message, appending the original message to the bottom.
    Examples:
      @mail 2
      @mail Griatch=New mail/Hey man, I am sending you a message!
      @mail/delete 6
      @mail/forward feend78 Griatch=4/You guys should read this.
      @mail/reply 9=Thanks for the info!

    """

    key = "@mail"
    aliases = ["mail"]
    lock = "cmd:all()"

        """
        Add convenience check to know if caller is an Account or not since this cmd
        will be able to add to either Object- or Account level.

        """
        self.caller_is_account = bool(
            inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount")
        )

    def search_targets(self, namelist):
        """
        Search a list of targets of the same type as caller.

        Args:
            caller (Object or Account): The type of object to search.
            namelist (list): List of strings for objects to search for.

        Returns:
            targetlist (Queryset): Any target matches.

        """
        nameregex = r"|".join(r"^%s$" % re.escape(name) for name in make_iter(namelist))
        if self.caller_is_account:
            matches = AccountDB.objects.filter(username__iregex=nameregex)
        else:
            matches = ObjectDB.objects.filter(db_key__iregex=nameregex)

    def get_all_mail(self):
        """
        Returns a list of all the messages where the caller is a recipient. These
            are all messages tagged with tags of the `mail` category.

        Returns:
            messages (QuerySet): Matching Msg objects.

        """
            return Msg.objects.get_by_tag(category="mail").filter(db_receivers_accounts=self.caller)
        else:
            return Msg.objects.get_by_tag(category="mail").filter(db_receivers_objects=self.caller)

    def send_mail(self, recipients, subject, message, caller):
        """
        Function for sending new mail.  Also useful for sending notifications
        from objects or systems.

        Args:
            recipients (list): list of Account or Character objects to receive
                the newly created mails.
            subject (str): The header or subject of the message to be delivered.
            message (str): The body of the message being sent.
            caller (obj): The object (or Account or Character) that is sending the message.

        """
        for recipient in recipients:
            recipient.msg("You have received a new @mail from %s" % caller)
            new_message = create.create_message(
                self.caller, message, receivers=recipient, header=subject
            )
            new_message.tags.add("new", category="mail")

        if recipients:
            caller.msg("You sent your message.")
            return
        else:
            caller.msg("No valid target(s) found. Cannot send message.")
            return

        """
        Do the main command functionality
        """

        subject = ""
        body = ""

        if self.switches or self.args:
            if "delete" in self.switches or "del" in self.switches:
                try:
                    if not self.lhs:
                        self.caller.msg("No Message ID given. Unable to delete.")
                        return
                    else:
                        all_mail = self.get_all_mail()
                        mind_max = max(0, all_mail.count() - 1)
                        mind = max(0, min(mind_max, int(self.lhs) - 1))
                        if all_mail[mind]:
                            mail = all_mail[mind]
                            question = "Delete message {} ({}) [Y]/N?".format(mind + 1, mail.header)
                            ret = yield (question)
                            # handle not ret, it will be None during unit testing
                            if not ret or ret.strip().upper() not in ("N", "No"):
                                all_mail[mind].delete()
                                self.caller.msg("Message %s deleted" % (mind + 1,))
                            else:
                                self.caller.msg("Message not deleted.")
                        else:
                            raise IndexError
                    self.caller.msg("That message does not exist.")
                    self.caller.msg("Usage: @mail/delete <message ID>")
            elif "forward" in self.switches or "fwd" in self.switches:
                try:
                    if not self.rhs:
                        self.caller.msg(
                            "Cannot forward a message without a target list. " "Please try again."
                        )
                        return
                    elif not self.lhs:
                        self.caller.msg("You must define a message to forward.")
                        return
                    else:
                        if "/" in self.rhs:
                            message_number, message = self.rhs.split("/", 1)
                            mind = max(0, min(mind_max, int(message_number) - 1))

                            if all_mail[mind]:
                                old_message = all_mail[mind]

                                self.send_mail(
                                    self.search_targets(self.lhslist),
                                    "FWD: " + old_message.header,
                                    message
                                    + "\n---- Original Message ----\n"
                                    + old_message.message,
                                    self.caller,
                                )
                                self.caller.msg("Message forwarded.")
                            else:
                                raise IndexError
                        else:
                            mind = max(0, min(mind_max, int(self.rhs) - 1))
                                    "\n---- Original Message ----\n" + old_message.message,
                                )
                                old_message.tags.remove("new", category="mail")
                                old_message.tags.add("fwd", category="mail")
                            else:
                    self.caller.msg("Message does not exist.")
                    self.caller.msg("Usage: @mail/forward <account list>=<#>[/<Message>]")
            elif "reply" in self.switches or "rep" in self.switches:
                try:
                        self.caller.msg("You must define a message to reply to.")
                        return
                        self.caller.msg("You must supply a reply message")
                        return
                    else:
                            old_message = all_mail[mind]
                            self.send_mail(
                                old_message.senders,
                                "RE: " + old_message.header,
                                self.rhs + "\n---- Original Message ----\n" + old_message.message,
                                self.caller,
                            )
                            old_message.tags.remove("new", category="mail")
                            old_message.tags.add("-", category="mail")
                            return
                        else:
                    self.caller.msg("Usage: @mail/reply <#>=<message>")
            else:
                # normal send
                    if "/" in self.rhs:
                        subject, body = self.rhs.split("/", 1)
                    else:
                        body = self.rhs
                    self.send_mail(self.search_targets(self.lhslist), subject, body, self.caller)
                else:
                    all_mail = self.get_all_mail()
                    mind_max = max(0, all_mail.count() - 1)
                    try:
                        message = all_mail[mind]
                    except (ValueError, IndexError):
                        self.caller.msg("'%s' is not a valid mail id." % self.lhs)
                        return

                    messageForm = []
                    if message:
                        messageForm.append(_HEAD_CHAR * _WIDTH)
                        messageForm.append(
                            "|wFrom:|n %s" % (message.senders[0].get_display_name(self.caller))
                        )
                        # note that we cannot use %-d format here since Windows does not support it
                        day = message.db_date_created.day
                            "|wSent:|n %s"
                            % message.db_date_created.strftime(f"%b {day}, %Y - %H:%M:%S")
                        )
                        messageForm.append("|wSubject:|n %s" % message.header)
                        messageForm.append(_SUB_HEAD_CHAR * _WIDTH)
                        messageForm.append(message.message)
                    self.caller.msg("\n".join(messageForm))
                    message.tags.remove("new", category="mail")
                    message.tags.add("-", category="mail")

        else:
            # list messages
            messages = self.get_all_mail()

            if messages:
                table = evtable.EvTable(
                    "|wID|n",
                    "|wFrom|n",
                    "|wSubject|n",
                    "|wArrived|n",
                    "",
                    table=None,
                    border="header",
                    header_line_char=_SUB_HEAD_CHAR,
                    width=_WIDTH,
                )
                index = 1
                for message in messages:
                    status = str(message.db_tags.last().db_key.upper())
                    if status == "NEW":
                        status = "|gNEW|n"

                        index,
                        message.senders[0].get_display_name(self.caller),
                        message.header,
                        datetime_format(message.db_date_created),
                        status,
                    )
                    index += 1

                table.reformat_column(0, width=6)
                table.reformat_column(1, width=18)
                table.reformat_column(2, width=34)
                table.reformat_column(3, width=13)
                table.reformat_column(4, width=7)

                self.caller.msg(_HEAD_CHAR * _WIDTH)
                self.caller.msg(str(table))
            else:
                self.caller.msg("There are no messages in your inbox.")

# character - level version of the command

class CmdMailCharacter(CmdMail):

"""
Test mail contrib

"""

from . import mail

class TestMail(BaseEvenniaCommandTest):
    def test_mail(self):
        self.call(mail.CmdMail(), "2", "'2' is not a valid mail id.", caller=self.account)
        self.call(mail.CmdMail(), "test", "'test' is not a valid mail id.", caller=self.account)
        self.call(mail.CmdMail(), "", "There are no messages in your inbox.", caller=self.account)
            mail.CmdMailCharacter(),
            "Char=Message 1",
            "You have received a new @mail from Char|You sent your message.",
        )
            mail.CmdMailCharacter(), "Char=Message 2", "You sent your message.", caller=self.char2
        )
            mail.CmdMail(),
            "TestAccount2=Message 2",
            "You have received a new @mail from TestAccount2",
            caller=self.account2,
        )
            mail.CmdMail(), "TestAccount=Message 1", "You sent your message.", caller=self.account2
        )
            mail.CmdMail(), "TestAccount=Message 2", "You sent your message.", caller=self.account2
        )
        self.call(mail.CmdMail(), "", "| ID    From              Subject", caller=self.account)
        self.call(mail.CmdMail(), "2", "From: TestAccount2", caller=self.account)
            "/forward TestAccount2 = 1/Forward message",
            "You sent your message.|Message forwarded.",
        )
            mail.CmdMail(), "/reply 2=Reply Message2", "You sent your message.", caller=self.account
        )
        self.call(mail.CmdMail(), "/delete 2", "Message 2 deleted", caller=self.account)

"""
Evennia Multidescer

Contrib - Griatch 2016

A "multidescer" is a concept from the MUSH world. It allows for
creating, managing and switching between multiple character
descriptions. This multidescer will not require any changes to the
Character class, rather it will use the `multidescs` Attribute (a
list) and create it if it does not exist.

This contrib also works well together with the rpsystem contrib (which
also adds the short descriptions and the `sdesc` command).

Edit `mygame/commands/default_cmdsets.py` and add
`from evennia.contrib.game_systems.multidescer import CmdMultiDesc` to the top.

Next, look up the `at_cmdset_create` method of the `CharacterCmdSet`
class and add a line `self.add(CmdMultiDesc())` to the end
of it.

Reload the server and you should have the +desc command available (it
will replace the default `desc` command).

"""
import re

from evennia import default_cmds
from evennia.utils.utils import crop

# regex for the set functionality
_RE_KEYS = re.compile(r"([\w\s]+)(?:\+*?)", re.U + re.I)

# Helper functions for the Command

class DescValidateError(ValueError):
    "Used for tracebacks from desc systems"
    pass

def _update_store(caller, key=None, desc=None, delete=False, swapkey=None):
    """
    Helper function for updating the database store.

    Args:
        caller (Object): The caller of the command.
        key (str): Description identifier
        desc (str): Description text.
        delete (bool): Delete given key.
        swapkey (str): Swap list positions of `key` and this key.

    """
    if not caller.db.multidesc:
        # initialize the multidesc attribute
        caller.db.multidesc = [("caller", caller.db.desc or "")]
    if not key:
        return
    lokey = key.lower()
    match = [ind for ind, tup in enumerate(caller.db.multidesc) if tup[0] == lokey]
    if match:
        idesc = match[0]
        if delete:
            # delete entry
            del caller.db.multidesc[idesc]
        elif swapkey:
            # swap positions
            loswapkey = swapkey.lower()
            swapmatch = [ind for ind, tup in enumerate(caller.db.multidesc) if tup[0] == loswapkey]
            if swapmatch:
                iswap = swapmatch[0]
                if idesc == iswap:
                    raise DescValidateError("Swapping a key with itself does nothing.")
                temp = caller.db.multidesc[idesc]
                caller.db.multidesc[idesc] = caller.db.multidesc[iswap]
                caller.db.multidesc[iswap] = temp
            else:
                raise DescValidateError("Description key '|w%s|n' not found." % swapkey)
        elif desc:
            # update in-place
            caller.db.multidesc[idesc] = (lokey, desc)
        else:
            raise DescValidateError("No description was set.")
    else:
        # no matching key
        if delete or swapkey:
            raise DescValidateError("Description key '|w%s|n' not found." % key)
        elif desc:
            # insert new at the top of the stack
            caller.db.multidesc.insert(0, (lokey, desc))
        else:

# eveditor save/load/quit functions

def _save_editor(caller, buffer):
    "Called when the editor saves its contents"
    key = caller.db._multidesc_editkey
    _update_store(caller, key, buffer)
    caller.msg("Saved description to key '%s'." % key)

def _load_editor(caller):
    "Called when the editor loads contents"
    match = [ind for ind, tup in enumerate(caller.db.multidesc) if tup[0] == key]
    if match:
        return caller.db.multidesc[match[0]][1]
    return ""

def _quit_editor(caller):
    "Called when the editor quits"
    del caller.db._multidesc_editkey

# The actual command class

class CmdMultiDesc(default_cmds.MuxCommand):
    """
    Manage multiple descriptions

    Usage:
        +desc [key]                - show current desc desc with <key>
        +desc <key> = <text>       - add/replace desc with <key>
        +desc/list                 - list descriptions (abbreviated)
        +desc/list/full            - list descriptions (full texts)
        +desc/edit <key>           - add/edit desc <key> in line editor
        +desc/del <key>            - delete desc <key>
        +desc/swap <key1>-<key2>   - swap positions of <key1> and <key2> in list
        +desc/set <key> [+key+...] - set desc as default or combine multiple descs

    Notes:
        When combining multiple descs with +desc/set <key> + <key2> + ...,
        any keys not matching an actual description will be inserted
        as plain text. Use e.g. ansi line break ||/ to add a new
        paragraph and + + or ansi space ||_ to add extra whitespace.

    """

    key = "+desc"
    aliases = ["desc"]

        """
        Implements the multidescer.  We will use `db.desc` for the
        description in use and `db.multidesc` to store all descriptions.
        """

        try:
            if "list" in switches or "all" in switches:
                # list all stored descriptions, either in full or cropped.
                # Note that we list starting from 1, not from 0.
                _update_store(caller)
                do_crop = "full" not in switches
                if do_crop:
                    outtext = [
                        "|w%s:|n %s" % (key, crop(desc)) for key, desc in caller.db.multidesc
                    ]
                else:
                    outtext = [
                        "\n|w%s:|n|n\n%s\n%s" % (key, "-" * (len(key) + 1), desc)
                        for key, desc in caller.db.multidesc
                    ]

                caller.msg("|wStored descs:|n\n" + "\n".join(outtext))
                return

            elif "edit" in switches:
                # Use the eveditor to edit/create the named description
                if not args:
                    caller.msg("Usage: %s/edit key" % self.key)
                    return

                # this is used by the editor to know what to edit; it's deleted automatically
                caller.db._multidesc_editkey = args
                # start the editor
                EvEditor(
                    caller,
                    loadfunc=_load_editor,
                    savefunc=_save_editor,
                    quitfunc=_quit_editor,
                    key="multidesc editor",
                    persistent=True,
                )

            elif "delete" in switches or "del" in switches:
                # delete a multidesc entry.
                    caller.msg("Usage: %s/delete key" % self.key)
                    return
                _update_store(caller, args, delete=True)
                caller.msg("Deleted description with key '%s'." % args)

            elif "swap" in switches or "switch" in switches or "reorder" in switches:
                # Reorder list by swapping two entries. We expect numbers starting from 1
                keys = [arg for arg in args.split("-", 1)]
                if not len(keys) == 2:
                    caller.msg("Usage: %s/swap key1-key2" % self.key)
                    return
                key1, key2 = keys
                # perform the swap
                _update_store(caller, key1, swapkey=key2)
                caller.msg("Swapped descs '%s' and '%s'." % (key1, key2))

            elif "set" in switches:
                # switches one (or more) of the multidescs to be the "active" description
                    caller.msg("Usage: %s/set key [+ key2 + key3 + ...]" % self.key)
                    return
                new_desc = []
                multidesc = caller.db.multidesc
                for key in args.split("+"):
                    notfound = True
                    lokey = key.strip().lower()
                    for mkey, desc in multidesc:
                        if lokey == mkey:
                            new_desc.append(desc)
                            notfound = False
                            continue
                    if notfound:
                        # if we get here, there is no desc match, we add it as a normal string
                        new_desc.append(key)
                new_desc = "".join(new_desc)
                caller.db.desc = new_desc
                caller.msg("%s\n\n|wThe above was set as the current description.|n" % new_desc)

            elif self.rhs or "add" in switches:
                # add text directly to a new entry or an existing one.
                if not (self.lhs and self.rhs):
                    caller.msg("Usage: %s/add key = description" % self.key)
                    return
                key, desc = self.lhs, self.rhs
                _update_store(caller, key, desc)
                caller.msg("Stored description '%s': \"%s\"" % (key, crop(desc)))

            else:
                # display the current description or a numbered description
                if args:
                    key = args.lower()
                    multidesc = caller.db.multidesc
                        if key == mkey:
                            caller.msg("|wDecsription %s:|n\n%s" % (key, desc))
                            return
                    caller.msg("Description key '%s' not found." % key)
                else:
                    caller.msg("|wCurrent desc:|n\n%s" % caller.db.desc)

        except DescValidateError as err:
            # This is triggered by _key_to_index

"""
Test multidescer contrib.

"""

from . import multidescer

class TestMultidescer(BaseEvenniaCommandTest):
    def test_cmdmultidesc(self):
        self.call(multidescer.CmdMultiDesc(), "/list", "Stored descs:\ncaller:")
            multidescer.CmdMultiDesc(), "test = Desc 1", "Stored description 'test': \"Desc 1\""
        )
            multidescer.CmdMultiDesc(), "test2 = Desc 2", "Stored description 'test2': \"Desc 2\""
        )
            multidescer.CmdMultiDesc(), "/swap test-test2", "Swapped descs 'test' and 'test2'."
        )
            multidescer.CmdMultiDesc(),
            "test3 = Desc 3init",
            "Stored description 'test3': \"Desc 3init\"",
        )
            "/list",
            "Stored descs:\ntest3: Desc 3init\ntest: Desc 1\ntest2: Desc 2\ncaller:",
        )
            multidescer.CmdMultiDesc(), "test3 = Desc 3", "Stored description 'test3': \"Desc 3\""
        )
            "/set test1 + test2 + + test3",
            "test1 Desc 2 Desc 3\n\n" "The above was set as the current description.",
        )
        self.assertEqual(self.char1.db.desc, "test1 Desc 2 Desc 3")

"""
Puzzles System - Provides a typeclass and commands for
objects that can be combined (i.e. 'use'd) to produce
new objects.

Evennia contribution - Henddher 2018

A Puzzle is a recipe of what objects (aka parts) must
be combined by a player so a new set of objects
(aka results) are automatically created.

Add the PuzzleSystemCmdSet to all players (e.g. in their Character typeclass).

Alternatively:

    py self.cmdset.add('evennia.contrib.game_systems.puzzles.PuzzleSystemCmdSet')

Usage:

Consider this simple Puzzle:

    orange, mango, yogurt, blender = fruit smoothie

As a Builder:

    create/drop orange
    create/drop mango
    create/drop yogurt
    create/drop blender
    create/drop fruit smoothie

    puzzle smoothie, orange, mango, yogurt, blender = fruit smoothie
    ...
    Puzzle smoothie(#1234) created successfuly.

    destroy/force orange, mango, yogurt, blender, fruit smoothie

    armpuzzle #1234
    Part orange is spawned at ...
    Part mango is spawned at ...
    ....
    Puzzle smoothie(#1234) has been armed successfully

As Player:

    use orange, mango, yogurt, blender
    ...
    Genius, you blended all fruits to create a fruit smoothie!

Details:

Puzzles are created from existing objects. The given
objects are introspected to create prototypes for the
puzzle parts and results. These prototypes become the
puzzle recipe. (See PuzzleRecipe and @puzzle
command). Once the recipe is created, all parts and result
can be disposed (i.e. destroyed).

At a later time, a Builder or a Script can arm the puzzle
and spawn all puzzle parts in their respective
locations (See armpuzzle).

A regular player can collect the puzzle parts and combine
them (See use command). If player has specified
all pieces, the puzzle is considered solved and all
its puzzle parts are destroyed while the puzzle results
are spawened on their corresponding location.

"""

    CmdSet,
    DefaultScript,
    create_script,
)
from evennia.utils.utils import inherits_from

# Tag used by puzzles
_PUZZLES_TAG_CATEGORY = "puzzles"
_PUZZLES_TAG_RECIPE = "puzzle_recipe"
# puzzle part and puzzle result
_PUZZLES_TAG_MEMBER = "puzzle_member"

_PUZZLE_DEFAULT_FAIL_USE_MESSAGE = "You try to utilize %s but nothing happens ... something amiss?"
_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = "You are a Genius!!!"
_PUZZLE_DEFAULT_SUCCESS_USE_LOCATION_MESSAGE = "|c{caller}|n performs some kind of tribal dance and |y{result_names}|n seems to appear from thin air"

# ----------- UTILITY FUNCTIONS ------------

def proto_def(obj, with_tags=True):
    """
    Basic properties needed to spawn
    and compare recipe with candidate part
    """
    protodef = {
        # TODO: Don't we need to honor ALL properties? attributes, contents, etc.
        "prototype_key": "%s(%s)" % (obj.key, obj.dbref),
        "key": obj.key,
        "typeclass": obj.typeclass_path,
        "desc": obj.db.desc,
        "location": obj.location,
        "home": obj.home,
        "locks": ";".join(obj.locks.all()),
        "permissions": obj.permissions.all()[:],
    }
    if with_tags:
        tags = obj.tags.all(return_key_and_category=True)
        tags = [(t[0], t[1], None) for t in tags]
        tags.append((_PUZZLES_TAG_MEMBER, _PUZZLES_TAG_CATEGORY, None))
        protodef["tags"] = tags
    return protodef

def maskout_protodef(protodef, mask):
    """
    Returns a new protodef after removing protodef values based on mask
    """
    protodef = dict(protodef)
    for m in mask:
        if m in protodef:
            protodef.pop(m)

# Colorize the default success message
def _colorize_message(msg):
    _i = 0
    _colors = ["|r", "|g", "|y"]
    _msg = []
    for l in msg:
        _msg += _colors[_i] + l
        _i = (_i + 1) % len(_colors)
    msg = "".join(_msg) + "|n"
    return msg

_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE = _colorize_message(_PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE)

# ------------------------------------------

class PuzzleRecipe(DefaultScript):
    """
    Definition of a Puzzle Recipe
    """

    def save_recipe(self, puzzle_name, parts, results):
        self.db.puzzle_name = str(puzzle_name)
        self.db.parts = tuple(parts)
        self.db.results = tuple(results)
        self.db.mask = tuple()
        self.tags.add(_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY)
        self.db.use_success_message = _PUZZLE_DEFAULT_SUCCESS_USE_MESSAGE
        self.db.use_success_location_message = _PUZZLE_DEFAULT_SUCCESS_USE_LOCATION_MESSAGE

class CmdCreatePuzzleRecipe(MuxCommand):
    """
    Creates a puzzle recipe. A puzzle consists of puzzle-parts that
    the player can 'use' together to create a specified result.

    Usage:
        @puzzle name,<part1[,part2,...>] = <result1[,result2,...]>

    Example:
        create/drop balloon
        create/drop glass of water
        create/drop water balloon
        @puzzle waterballon,balloon,glass of water = water balloon
        @del ballon, glass of water, water balloon
        @armpuzzle #1

    Notes:
    Each part and result are objects that must (temporarily) exist and be placed in their
    corresponding location in order to create the puzzle. After the creation of the puzzle,
    these objects are not needed anymore and can be deleted. Components of the puzzle
    will be re-created by use of the `@armpuzzle` command later.

    """

    key = "@puzzle"
    aliases = "@puzzlerecipe"
    locks = "cmd:perm(puzzle) or perm(Builder)"
    help_category = "Puzzles"

    confirm = True
    default_confirm = "no"

        if len(self.lhslist) < 2 or not self.rhs:
            string = "Usage: @puzzle name,<part1[,...]> = <result1[,...]>"
            return

        puzzle_name = self.lhslist[0]
        if len(puzzle_name) == 0:
            caller.msg("Invalid puzzle name %r." % puzzle_name)
            return

        # if there is another puzzle with same name
        # warn user that parts and results will be
        # interchangable
        _puzzles = search.search_script_attribute(key="puzzle_name", value=puzzle_name)
        _puzzles = list(filter(lambda p: isinstance(p, PuzzleRecipe), _puzzles))
        if _puzzles:
            confirm = (
                "There are %d puzzles with the same name.\n" % len(_puzzles)
                + "Its parts and results will be interchangeable.\n"
                + "Continue yes/[no]? "
            )
            answer = ""
            while answer.strip().lower() not in ("y", "yes", "n", "no"):
                answer = yield (confirm)
                answer = self.default_confirm if answer == "" else answer
            if answer.strip().lower() in ("n", "no"):
                caller.msg("Cancelled: no puzzle created.")
                return

        def is_valid_obj_location(obj):
            # Rooms are the only valid locations.
            # TODO: other valid locations could be added here.
            # Certain locations can be handled accordingly: e.g,
            # a part is located in a character's inventory,
            # perhaps will translate into the player character
            # having the part in his/her inventory while being
            # located in the same room where the builder was
            # located.
            # Parts and results may have different valid locations
            if not inherits_from(obj.location, DefaultRoom):
                caller.msg("Invalid location for %s" % (obj.key))
                valid = False
            return valid

        def is_valid_part_location(part):
            return is_valid_obj_location(part)

        def is_valid_result_location(part):

        def is_valid_inheritance(obj):
            valid = (
                not inherits_from(obj, DefaultCharacter)
                and not inherits_from(obj, DefaultRoom)
                and not inherits_from(obj, DefaultExit)
            )
                caller.msg("Invalid typeclass for %s" % (obj))

        def is_valid_part(part):
            return is_valid_inheritance(part) and is_valid_part_location(part)

        def is_valid_result(result):
            return is_valid_inheritance(result) and is_valid_result_location(result)

        parts = []
        for objname in self.lhslist[1:]:
            if not obj:
                return
            if not is_valid_part(obj):
                return
            parts.append(obj)

        results = []
        for objname in self.rhslist:
            if not obj:
                return
            if not is_valid_result(obj):
                return
            results.append(obj)

        for part in parts:
            caller.msg("Part %s(%s)" % (part.name, part.dbref))

        for result in results:
            caller.msg("Result %s(%s)" % (result.name, result.dbref))

        proto_parts = [proto_def(obj) for obj in parts]
        proto_results = [proto_def(obj) for obj in results]

        puzzle = create_script(PuzzleRecipe, key=puzzle_name, persistent=True)
        puzzle.save_recipe(puzzle_name, proto_parts, proto_results)
        puzzle.locks.add("control:id(%s) or perm(Builder)" % caller.dbref[1:])

            "Puzzle |y'%s' |w%s(%s)|n has been created |gsuccessfully|n."
            % (puzzle.db.puzzle_name, puzzle.name, puzzle.dbref)
        )

            "You may now dispose of all parts and results. \n"
            "Use @puzzleedit #{dbref} to customize this puzzle further. \n"
            "Use @armpuzzle #{dbref} to arm a new puzzle instance.".format(dbref=puzzle.dbref)
        )

class CmdEditPuzzle(MuxCommand):
    """
    Edits puzzle properties

    Usage:
        @puzzleedit[/delete] <#dbref>
        @puzzleedit <#dbref>/use_success_message = <Custom message>
        @puzzleedit <#dbref>/use_success_location_message = <Custom message from {caller} producing {result_names}>
        @puzzleedit <#dbref>/mask = attr1[,attr2,...]>
        @puzzleedit[/addpart] <#dbref> = <obj[,obj2,...]>
        @puzzleedit[/delpart] <#dbref> = <obj[,obj2,...]>
        @puzzleedit[/addresult] <#dbref> = <obj[,obj2,...]>
        @puzzleedit[/delresult] <#dbref> = <obj[,obj2,...]>

    Switches:
      addpart - adds parts to the puzzle
      delpart - removes parts from the puzzle
      addresult - adds results to the puzzle
      delresult - removes results from the puzzle
      delete - deletes the recipe. Existing parts and results aren't modified

      mask - attributes to exclude during matching (e.g. location, desc, etc.)
      use_success_location_message containing {result_names} and {caller} will
        automatically be replaced with correct values. Both are optional.

      When removing parts/results, it's possible to remove all.

    """

    key = "@puzzleedit"
    locks = "cmd:perm(puzzleedit) or perm(Builder)"

        self._USAGE = "Usage: @puzzleedit[/switches] <dbref>[/attribute = <value>]"

        if not self.lhslist:
            caller.msg(self._USAGE)
            return

        if "/" in self.lhslist[0]:
            recipe_dbref, attr = self.lhslist[0].split("/")
        else:
            recipe_dbref = self.lhslist[0]

        if not utils.dbref(recipe_dbref):
            caller.msg("A puzzle recipe's #dbref must be specified.\n" + self._USAGE)
            return

        puzzle = search.search_script(recipe_dbref)
        if not puzzle or not inherits_from(puzzle[0], PuzzleRecipe):
            caller.msg("%s(%s) is not a puzzle" % (puzzle[0].name, recipe_dbref))
            return

        puzzle = puzzle[0]
        puzzle_name_id = "%s(%s)" % (puzzle.name, puzzle.dbref)

            if not (puzzle.access(caller, "control") or puzzle.access(caller, "delete")):
                caller.msg("You don't have permission to delete %s." % puzzle_name_id)
                return

            puzzle.delete()
            caller.msg("%s was deleted" % puzzle_name_id)
            return

        elif "addpart" in self.switches:
            objs = self._get_objs()
            if objs:
                added = self._add_parts(objs, puzzle)
                caller.msg("%s were added to parts" % (", ".join(added)))
            return

        elif "delpart" in self.switches:
            if objs:
                removed = self._remove_parts(objs, puzzle)
                caller.msg("%s were removed from parts" % (", ".join(removed)))
            return

        elif "addresult" in self.switches:
            if objs:
                added = self._add_results(objs, puzzle)
                caller.msg("%s were added to results" % (", ".join(added)))
            return

        elif "delresult" in self.switches:
            if objs:
                removed = self._remove_results(objs, puzzle)
                caller.msg("%s were removed from results" % (", ".join(removed)))
            return

        else:
            # edit attributes

            if not (puzzle.access(caller, "control") or puzzle.access(caller, "edit")):
                caller.msg("You don't have permission to edit %s." % puzzle_name_id)
                return

            if attr == "use_success_message":
                puzzle.db.use_success_message = self.rhs
                    "%s use_success_message = %s\n"
                    % (puzzle_name_id, puzzle.db.use_success_message)
                )
                return
            elif attr == "use_success_location_message":
                puzzle.db.use_success_location_message = self.rhs
                    "%s use_success_location_message = %s\n"
                    % (puzzle_name_id, puzzle.db.use_success_location_message)
                )
                return
            elif attr == "mask":
                puzzle.db.mask = tuple(self.rhslist)
                caller.msg("%s mask = %r\n" % (puzzle_name_id, puzzle.db.mask))
                return

    def _get_objs(self):
        if not self.rhslist:
            self.caller.msg(self._USAGE)
            return
        objs = []
        for o in self.rhslist:
            obj = self.caller.search(o)
            if obj:

    def _add_objs_to(self, objs, to):
        """Adds propto objs to the given set (parts or results)"""
        added = []
        toobjs = list(to[:])
            protoobj = proto_def(obj)
            toobjs.append(protoobj)
            added.append(obj.key)
        return added, toobjs

    def _remove_objs_from(self, objs, frm):
        """Removes propto objs from the given set (parts or results)"""
        removed = []
        fromobjs = list(frm[:])
            if protoobj in fromobjs:
                fromobjs.remove(protoobj)
                removed.append(obj.key)
        return removed, fromobjs

    def _add_parts(self, objs, puzzle):
        added, toobjs = self._add_objs_to(objs, puzzle.db.parts)
        puzzle.db.parts = tuple(toobjs)
        return added

    def _remove_parts(self, objs, puzzle):
        removed, fromobjs = self._remove_objs_from(objs, puzzle.db.parts)
        puzzle.db.parts = tuple(fromobjs)
        return removed

    def _add_results(self, objs, puzzle):
        added, toobjs = self._add_objs_to(objs, puzzle.db.results)
        puzzle.db.results = tuple(toobjs)

    def _remove_results(self, objs, puzzle):
        removed, fromobjs = self._remove_objs_from(objs, puzzle.db.results)
        puzzle.db.results = tuple(fromobjs)

class CmdArmPuzzle(MuxCommand):
    """
    Arms a puzzle by spawning all its parts.

    Usage:
      @armpuzzle <puzzle #dbref>

    Notes:
        Create puzzles with `@puzzle`; get list of
        defined puzzles using `@lspuzzlerecipes`.

    """

    key = "@armpuzzle"
    locks = "cmd:perm(armpuzzle) or perm(Builder)"

        if self.args is None or not utils.dbref(self.args):
            caller.msg("A puzzle recipe's #dbref must be specified")
            return

        puzzle = search.search_script(self.args)
            caller.msg("Invalid puzzle %r" % (self.args))
            return

            "Puzzle Recipe %s(%s) '%s' found.\nSpawning %d parts ..."
            % (puzzle.name, puzzle.dbref, puzzle.db.puzzle_name, len(puzzle.db.parts))
        )

        for proto_part in puzzle.db.parts:
            part = spawn(proto_part)[0]
                "Part %s(%s) spawned and placed at %s(%s)"
                % (part.name, part.dbref, part.location, part.location.dbref)
            )
            part.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY)
            part.db.puzzle_name = puzzle.db.puzzle_name

        caller.msg("Puzzle armed |gsuccessfully|n.")

def _lookups_parts_puzzlenames_protodefs(parts):
    # Create lookup dicts by part's dbref and by puzzle_name(tags)
    parts_dict = dict()
    puzzlename_tags_dict = dict()
    puzzle_ingredients = dict()
    for part in parts:
        parts_dict[part.dbref] = part
        protodef = proto_def(part, with_tags=False)
        # remove 'prototype_key' as it will prevent equality
        del protodef["prototype_key"]
        puzzle_ingredients[part.dbref] = protodef
        tags_categories = part.tags.all(return_key_and_category=True)
        for tag, category in tags_categories:
            if category != _PUZZLES_TAG_CATEGORY:
                continue
            if tag not in puzzlename_tags_dict:
                puzzlename_tags_dict[tag] = []
            puzzlename_tags_dict[tag].append(part.dbref)
    return parts_dict, puzzlename_tags_dict, puzzle_ingredients

def _puzzles_by_names(names):
    # Find all puzzles by puzzle name (i.e. tag name)
    puzzles = []
    for puzzle_name in names:
        if not _puzzles:
            continue
        else:
            puzzles.extend(_puzzles)
    return puzzles

def _matching_puzzles(puzzles, puzzlename_tags_dict, puzzle_ingredients):
    # Check if parts can be combined to solve a puzzle
    matched_puzzles = dict()
    for puzzle in puzzles:
        puzzle_protoparts = list(puzzle.db.parts[:])
        puzzle_mask = puzzle.db.mask[:]
        # remove tags and prototype_key as they prevent equality
        for i, puzzle_protopart in enumerate(puzzle_protoparts[:]):
            del puzzle_protopart["tags"]
            del puzzle_protopart["prototype_key"]
            puzzle_protopart = maskout_protodef(puzzle_protopart, puzzle_mask)
            puzzle_protoparts[i] = puzzle_protopart

        matched_dbrefparts = []
        parts_dbrefs = puzzlename_tags_dict[puzzle.db.puzzle_name]
        for part_dbref in parts_dbrefs:
            protopart = puzzle_ingredients[part_dbref]
            protopart = maskout_protodef(protopart, puzzle_mask)
            if protopart in puzzle_protoparts:
                puzzle_protoparts.remove(protopart)
                matched_dbrefparts.append(part_dbref)
        else:
            if len(puzzle_protoparts) == 0:
                matched_puzzles[puzzle.dbref] = matched_dbrefparts
    return matched_puzzles

class CmdUsePuzzleParts(MuxCommand):
    """
    Use an object, or a group of objects at once.

    Example:
      You look around you and see a pole, a long string, and a needle.

      use pole, long string, needle

      Genius! You built a fishing pole.

    Usage:
        use <obj1> [,obj2,...]
    """

    # Technical explanation
    """
    Searches for all puzzles whose parts match the given set of objects. If there are matching
    puzzles, the result objects are spawned in their corresponding location if all parts have been
    passed in.
    """

    key = "use"
    aliases = "combine"
    locks = "cmd:pperm(use) or pperm(Player)"

            caller.msg("Use what?")
            return

        many = "these" if len(self.lhslist) > 1 else "this"

        # either all are parts, or abort finding matching puzzles
        parts = []
        partnames = self.lhslist[:]
        for partname in partnames:
            part = caller.search(
                partname,
                multimatch_string="Which %s. There are many.\n" % (partname),
                nofound_string="There is no %s around." % (partname),
            )

            if not part:
                return

            if not part.tags.get(_PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY):
                # not a puzzle part ... abort
                caller.msg("You have no idea how %s can be used" % (many))
                return

            # a valid part
            parts.append(part)

        # Create lookup dicts by part's dbref and by puzzle_name(tags)
        parts_dict, puzzlename_tags_dict, puzzle_ingredients = _lookups_parts_puzzlenames_protodefs(
            parts
        )

        # Find all puzzles by puzzle name (i.e. tag name)
        puzzles = _puzzles_by_names(puzzlename_tags_dict.keys())

        logger.log_info("PUZZLES %r" % ([(p.dbref, p.db.puzzle_name) for p in puzzles]))

        # Create lookup dict of puzzles by dbref
        puzzles_dict = dict((puzzle.dbref, puzzle) for puzzle in puzzles)
        # Check if parts can be combined to solve a puzzle
        matched_puzzles = _matching_puzzles(puzzles, puzzlename_tags_dict, puzzle_ingredients)

        if len(matched_puzzles) == 0:
            # TODO: we could use part.fail_message instead, if there was one
            #    random part falls and lands on your feet
            #    random part hits you square on the face
            caller.msg(_PUZZLE_DEFAULT_FAIL_USE_MESSAGE % (many))
            return

        puzzletuples = sorted(matched_puzzles.items(), key=lambda t: len(t[1]), reverse=True)

        logger.log_info("MATCHED PUZZLES %r" % (puzzletuples))

        # sort all matched puzzles and pick largest one(s)
        puzzledbref, matched_dbrefparts = puzzletuples[0]
        nparts = len(matched_dbrefparts)
        puzzle = puzzles_dict[puzzledbref]
        largest_puzzles = list(itertools.takewhile(lambda t: len(t[1]) == nparts, puzzletuples))

        # if there are more than one, choose one at random.
        # we could show the names of all those that can be resolved
        # but that would give away that there are other puzzles that
        # can be resolved with the same parts.
        # just hint how many.
        if len(largest_puzzles) > 1:
                "Your gears start turning and %d different ideas come to your mind ...\n"
                % (len(largest_puzzles))
            )
            puzzletuple = choice(largest_puzzles)
            puzzle = puzzles_dict[puzzletuple[0]]
            caller.msg("You try %s ..." % (puzzle.db.puzzle_name))

        # got one, spawn its results
        result_names = []
        for proto_result in puzzle.db.results:
            result = spawn(proto_result)[0]
            result.tags.add(puzzle.db.puzzle_name, category=_PUZZLES_TAG_CATEGORY)
            result.db.puzzle_name = puzzle.db.puzzle_name
            result_names.append(result.name)

        # Destroy all parts used
        for dbref in matched_dbrefparts:
            parts_dict[dbref].delete()

        result_names = ", ".join(result_names)
        caller.msg(puzzle.db.use_success_message)
        caller.location.msg_contents(
            puzzle.db.use_success_location_message.format(caller=caller, result_names=result_names),
            exclude=(caller,),
        )

class CmdListPuzzleRecipes(MuxCommand):
    """
    Searches for all puzzle recipes

    Usage:
        @lspuzzlerecipes
    """

    key = "@lspuzzlerecipes"
    locks = "cmd:perm(lspuzzlerecipes) or perm(Builder)"

        recipes = search.search_script_tag(_PUZZLES_TAG_RECIPE, category=_PUZZLES_TAG_CATEGORY)

        div = "-" * 60
        text = [div]
        msgf_recipe = "Puzzle |y'%s' %s(%s)|n"
        msgf_item = "%2s|c%15s|n: |w%s|n"
        for recipe in recipes:
            text.append(msgf_recipe % (recipe.db.puzzle_name, recipe.name, recipe.dbref))
            text.append("Success Caller message:\n" + recipe.db.use_success_message + "\n")
            text.append(
                "Success Location message:\n" + recipe.db.use_success_location_message + "\n"
            )
            text.append("Mask:\n" + str(recipe.db.mask) + "\n")
            text.append("Parts")
            for protopart in recipe.db.parts[:]:
                mark = "-"
                for k, v in protopart.items():
                    text.append(msgf_item % (mark, k, v))
                    mark = ""
            text.append("Results")
            for protoresult in recipe.db.results[:]:
                mark = "-"
                for k, v in protoresult.items():
                    mark = ""
        else:
            text.append(div)
            text.append("Found |r%d|n puzzle(s)." % (len(recipes)))
        caller.msg("\n".join(text))

class CmdListArmedPuzzles(MuxCommand):
    """
    Searches for all armed puzzles

    Usage:
        @lsarmedpuzzles
    """

    key = "@lsarmedpuzzles"
    locks = "cmd:perm(lsarmedpuzzles) or perm(Builder)"

        armed_puzzles = search.search_tag(_PUZZLES_TAG_MEMBER, category=_PUZZLES_TAG_CATEGORY)

        armed_puzzles = dict(
            (k, list(g)) for k, g in itertools.groupby(armed_puzzles, lambda ap: ap.db.puzzle_name)
        )

        msgf_pznm = "Puzzle name: |y%s|n"
        msgf_item = "|m%25s|w(%s)|n at |c%25s|w(%s)|n"
        for pzname, items in armed_puzzles.items():
            text.append(msgf_pznm % (pzname))
                text.append(
                    msgf_item % (item.name, item.dbref, item.location.name, item.location.dbref)
                )
        else:
            text.append("Found |r%d|n armed puzzle(s)." % (len(armed_puzzles)))

class PuzzleSystemCmdSet(CmdSet):
    """
    CmdSet to create, arm and resolve Puzzles
    """

        self.add(CmdCreatePuzzleRecipe())
        self.add(CmdEditPuzzle())
        self.add(CmdArmPuzzle())
        self.add(CmdListPuzzleRecipes())
        self.add(CmdListArmedPuzzles())
        self.add(CmdUsePuzzleParts())

"""
Testing puzzles.

"""

# Test of the Puzzles module

import re

from evennia.utils import search

from . import puzzles

class TestPuzzles(BaseEvenniaCommandTest):
        self.steel = create_object(self.object_typeclass, key="steel", location=self.char1.location)
        self.flint = create_object(self.object_typeclass, key="flint", location=self.char1.location)
        self.fire = create_object(self.object_typeclass, key="fire", location=self.char1.location)
        self.steel.tags.add("tag-steel")
        self.steel.tags.add("tag-steel", category="tagcat")
        self.flint.tags.add("tag-flint")
        self.flint.tags.add("tag-flint", category="tagcat")
        self.fire.tags.add("tag-fire")
        self.fire.tags.add("tag-fire", category="tagcat")

    def _assert_msg_matched(self, msg, regexs, re_flags=0):
        matches = []
        for regex in regexs:
            m = re.search(regex, msg, re_flags)
            self.assertIsNotNone(m, "%r didn't match %r" % (regex, msg))
            matches.append(m)

    def _assert_recipe(self, name, parts, results, and_destroy_it=True, expected_count=1):
        def _keys(items):
            return [item["key"] for item in items]

        recipes = search.search_script_tag("", category=puzzles._PUZZLES_TAG_CATEGORY)
        self.assertEqual(expected_count, len(recipes))
        self.assertEqual(name, recipes[expected_count - 1].db.puzzle_name)
        self.assertEqual(parts, _keys(recipes[expected_count - 1].db.parts))
        self.assertEqual(results, _keys(recipes[expected_count - 1].db.results))
            puzzles._PUZZLES_TAG_RECIPE,
            recipes[expected_count - 1].tags.get(category=puzzles._PUZZLES_TAG_CATEGORY),
        )
        recipe_dbref = recipes[expected_count - 1].dbref
        if and_destroy_it:
            recipes[expected_count - 1].delete()
        return recipe_dbref if not and_destroy_it else None

    def _assert_no_recipes(self):
            0, len(search.search_script_tag("", category=puzzles._PUZZLES_TAG_CATEGORY))
        )

    # good recipes
    def _good_recipe(self, name, parts, results, and_destroy_it=True, expected_count=1):
        regexs = []
        for p in parts:
            regexs.append(r"^Part %s\(#\d+\)$" % (p))
        for r in results:
            regexs.append(r"^Result %s\(#\d+\)$" % (r))
        regexs.append(r"^Puzzle '%s' %s\(#\d+\) has been created successfully.$" % (name, name))
        lhs = [name] + parts
        cmdstr = ",".join(lhs) + "=" + ",".join(results)
        msg = self.call(puzzles.CmdCreatePuzzleRecipe(), cmdstr, caller=self.char1)
        recipe_dbref = self._assert_recipe(name, parts, results, and_destroy_it, expected_count)
        self._assert_msg_matched(msg, regexs, re_flags=re.MULTILINE | re.DOTALL)
        return recipe_dbref

    def _check_room_contents(self, expected, check_test_tags=False):
        by_obj_key = lambda o: o.key
        room1_contents = sorted(self.room1.contents, key=by_obj_key)
        for key, grp in itertools.groupby(room1_contents, by_obj_key):
            if key in expected:
                grp = list(grp)
                self.assertEqual(
                    expected[key],
                    len(grp),
                    "Expected %d but got %d for %s" % (expected[key], len(grp), key),
                )
                if check_test_tags:
                    for gi in grp:
                        tags = gi.tags.all(return_key_and_category=True)
                        self.assertIn(("tag-" + gi.key, "tagcat"), tags)

    def _arm(self, recipe_dbref, name, parts):
        regexs = [
            r"^Puzzle Recipe %s\(#\d+\) '%s' found.$" % (name, name),
            r"^Spawning %d parts ...$" % (len(parts)),
        ]
            regexs.append(r"^Part %s\(#\d+\) spawned .*$" % (p))
        regexs.append(r"^Puzzle armed successfully.$")
        msg = self.call(puzzles.CmdArmPuzzle(), recipe_dbref, caller=self.char1)

    def test_cmdset_puzzle(self):
        self.char1.cmdset.add("evennia.contrib.game_systems.puzzles.PuzzleSystemCmdSet")
        # FIXME: testing nothing, this is just to bump up coverage

    def test_cmd_puzzle(self):
        self._assert_no_recipes()

        # bad syntax
        def _bad_syntax(cmdstr):
                puzzles.CmdCreatePuzzleRecipe(),
                cmdstr,
                "Usage: @puzzle name,<part1[,...]> = <result1[,...]>",
                caller=self.char1,
            )

        _bad_syntax("")
        _bad_syntax("=")
        _bad_syntax("nothing =")
        _bad_syntax("= nothing")
        _bad_syntax("nothing")
        _bad_syntax(",nothing")
        _bad_syntax("name, nothing")
        _bad_syntax("name, nothing =")

        self._good_recipe("makefire", ["steel", "flint"], ["fire", "steel", "flint"])
        self._good_recipe("hot steels", ["steel", "fire"], ["steel", "fire"])
        self._good_recipe(
            "furnace",
            ["steel", "steel", "fire"],
            ["steel", "steel", "fire", "fire", "fire", "fire"],
        )

        # bad recipes
        def _bad_recipe(name, parts, results, fail_regex):
            cmdstr = ",".join([name] + parts) + "=" + ",".join(results)
            msg = self.call(puzzles.CmdCreatePuzzleRecipe(), cmdstr, caller=self.char1)
            self._assert_no_recipes()
            self.assertIsNotNone(re.match(fail_regex, msg), msg)

        _bad_recipe("name", ["nothing"], ["neither"], r"Could not find 'nothing'.")
        _bad_recipe("name", ["steel"], ["nothing"], r"Could not find 'nothing'.")
        _bad_recipe("", ["steel", "fire"], ["steel", "fire"], r"^Invalid puzzle name ''.")
        self.steel.location = self.char1
        _bad_recipe("name", ["steel"], ["fire"], r"^Invalid location for steel$")
        _bad_recipe("name", ["flint"], ["steel"], r"^Invalid location for steel$")
        _bad_recipe("name", ["self"], ["fire"], r"^Invalid typeclass for Char$")
        _bad_recipe("name", ["here"], ["fire"], r"^Invalid typeclass for Room$")

    def test_cmd_armpuzzle(self):
        # bad arms
            puzzles.CmdArmPuzzle(),
            "1",
            "A puzzle recipe's #dbref must be specified",
        )
        self.call(puzzles.CmdArmPuzzle(), "#1", "Invalid puzzle '#1'", caller=self.char1)

        recipe_dbref = self._good_recipe(
            "makefire", ["steel", "flint"], ["fire", "steel", "flint"], and_destroy_it=False
        )

        # delete proto parts and proto result
        self.steel.delete()
        self.flint.delete()
        self.fire.delete()

        # good arm
        self._arm(recipe_dbref, "makefire", ["steel", "flint"])
        self._check_room_contents({"steel": 1, "flint": 1}, check_test_tags=True)

    def _use(self, cmdstr, expmsg):
        msg = self.call(puzzles.CmdUsePuzzleParts(), cmdstr, expmsg, caller=self.char1)
        return msg

    def test_cmd_use(self):
        self._use("", "Use what?")
        self._use("something", "There is no something around.")
        self._use("steel", "You have no idea how this can be used")
        self._use("steel flint", "There is no steel flint around.")
        self._use("steel, flint", "You have no idea how these can be used")

            "makefire", ["steel", "flint"], ["fire"], and_destroy_it=False
        )
        recipe2_dbref = self._good_recipe(
            "makefire2", ["steel", "flint"], ["fire"], and_destroy_it=False, expected_count=2
        )

        # although there is steel and flint
        # those aren't valid puzzle parts because
        # the puzzle hasn't been armed
        self._check_room_contents({"steel": 2, "flint": 2}, check_test_tags=True)

        # there are duplicated objects now
        self._use("steel", "Which steel. There are many")
        self._use("flint", "Which flint. There are many")

        # delete proto parts and proto results

        # solve puzzle
        self._use("steel, flint", "You are a Genius")
            1,
            len(
                list(
                    filter(
                        lambda o: o.key == "fire"
                        and ("makefire", puzzles._PUZZLES_TAG_CATEGORY)
                        in o.tags.all(return_key_and_category=True)
                        and (puzzles._PUZZLES_TAG_MEMBER, puzzles._PUZZLES_TAG_CATEGORY)
                        in o.tags.all(return_key_and_category=True),
                        self.room1.contents,
                    )
                )
            ),
        )
        self._check_room_contents({"steel": 0, "flint": 0, "fire": 1}, check_test_tags=True)

        # trying again will fail as it was resolved already
        # and the parts were destroyed
        self._use("steel, flint", "There is no steel around")
        self._use("flint, steel", "There is no flint around")

        # arm same puzzle twice so there are duplicated parts
        self._check_room_contents({"steel": 2, "flint": 2, "fire": 1}, check_test_tags=True)

        # try solving with multiple parts but incomplete set
        self._use(
            "steel-1, steel-2", "You try to utilize these but nothing happens ... something amiss?"
        )

        # arm the other puzzle. Their parts are identical
        self._arm(recipe2_dbref, "makefire2", ["steel", "flint"])
        self._check_room_contents({"steel": 3, "flint": 3, "fire": 1}, check_test_tags=True)

        # solve with multiple parts for
        # multiple puzzles. Both can be solved but
        # only one is.
            "steel-1, flint-2, steel-3, flint-3",
            "Your gears start turning and 2 different ideas come to your mind ... ",
        )
        self._check_room_contents({"steel": 2, "flint": 2, "fire": 2}, check_test_tags=True)

        self.room1.msg_contents = Mock()

        # solve all
        self._use("steel-1, flint-1", "You are a Genius")
        self.room1.msg_contents.assert_called_once_with(
            "|cChar|n performs some kind of tribal dance and |yfire|n seems to appear from thin air",
            exclude=(self.char1,),
        )
        self._check_room_contents({"steel": 0, "flint": 0, "fire": 4}, check_test_tags=True)

    def test_puzzleedit(self):
        )

        def _puzzleedit(swt, dbref, args, expmsg):
            if (swt is None) and (dbref is None) and (args is None):
                cmdstr = ""
            else:
                cmdstr = "%s %s%s" % (swt, dbref, args)
            self.call(puzzles.CmdEditPuzzle(), cmdstr, expmsg, caller=self.char1)

        sid = self.script.id
        _puzzleedit(
            None, None, None, "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit"
        )
        _puzzleedit("", "1", "", "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit")
        _puzzleedit("", "", "", "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit")
            "",
            recipe_dbref,
            "dummy",
            "A puzzle recipe's #dbref must be specified.\nUsage: @puzzleedit",
        )
        _puzzleedit("", self.script.dbref, "", "Script(#{}) is not a puzzle".format(sid))

        # edit use_success_message and use_success_location_message
            "",
            "/use_success_message = Yes!",
            "makefire(%s) use_success_message = Yes!" % recipe_dbref,
        )
            "",
            "/use_success_location_message = {result_names} Yeah baby! {caller}",
            "makefire(%s) use_success_location_message = {result_names} Yeah baby! {caller}"
            % recipe_dbref,
        )

        self._use("steel, flint", "Yes!")
            "fire Yeah baby! Char", exclude=(self.char1,)
        )
        self.room1.msg_contents.reset_mock()

        # edit mask: exclude location and desc during matching
            "",
            "/mask = location,desc",
            "makefire(%s) mask = ('location', 'desc')" % recipe_dbref,
        )

        # change location and desc
        self.char1.search("steel").db.desc = "A solid bar of steel"
        self.char1.search("steel").location = self.char1
        self.char1.search("flint").db.desc = "A flint steel"
        self.char1.search("flint").location = self.char1
        )

        # delete
        _puzzleedit("/delete", recipe_dbref, "", "makefire(%s) was deleted" % recipe_dbref)

    def test_puzzleedit_add_remove_parts_results(self):
        )

        def _puzzleedit(swt, dbref, rhslist, expmsg):
            cmdstr = "%s %s = %s" % (swt, dbref, ", ".join(rhslist))

        red_steel = create_object(
            self.object_typeclass, key="red steel", location=self.char1.location
        )
        smoke = create_object(self.object_typeclass, key="smoke", location=self.char1.location)

        _puzzleedit("/addresult", recipe_dbref, ["smoke"], "smoke were added to results")
            "/addpart", recipe_dbref, ["red steel", "steel"], "red steel, steel were added to parts"
        )

        # create a box so we can put all objects in
        # so that they can't be found during puzzle resolution
        self.box = create_object(self.object_typeclass, key="box", location=self.char1.location)

        def _box_all():
            for o in self.room1.contents:
                if o not in [self.char1, self.char2, self.exit, self.obj1, self.obj2, self.box]:
                    o.location = self.box

        _box_all()

        self._arm(recipe_dbref, "makefire", ["steel", "flint", "red steel", "steel"])
        self._check_room_contents({"steel": 2, "red steel": 1, "flint": 1})
            "steel-1, flint", "You try to utilize these but nothing happens ... something amiss?"
        )
        self._use("steel-1, flint, red steel, steel-2", "You are a Genius")
        self._check_room_contents({"smoke": 1, "fire": 1})

        self.fire.location = self.room1
        self.steel.location = self.room1

        _puzzleedit("/delresult", recipe_dbref, ["fire"], "fire were removed from results")
            "/delpart", recipe_dbref, ["steel", "steel"], "steel, steel were removed from parts"
        )

        self._arm(recipe_dbref, "makefire", ["flint", "red steel"])
        self._check_room_contents({"red steel": 1, "flint": 1})
        self._use("red steel, flint", "You are a Genius")
        self._check_room_contents({"smoke": 1, "fire": 0})

    def test_lspuzzlerecipes_lsarmedpuzzles(self):
        msg = self.call(puzzles.CmdListPuzzleRecipes(), "", caller=self.char1)
        self._assert_msg_matched(
            msg, [r"^-+$", r"^Found 0 puzzle\(s\)\.$", r"-+$"], re.MULTILINE | re.DOTALL
        )

        )

            msg,
            [
                r"^-+$",
                r"^Puzzle 'makefire'.*$",
                r"^Success Caller message:$",
                r"^Success Location message:$",
                r"^Mask:$",
                r"^Parts$",
                r"^.*key: steel$",
                r"^.*key: flint$",
                r"^Results$",
                r"^.*key: fire$",
                r"^-+$",
                r"^Found 1 puzzle\(s\)\.$",
                r"^-+$",
            ],
            re.MULTILINE | re.DOTALL,
        )

        msg = self.call(puzzles.CmdListArmedPuzzles(), "", caller=self.char1)
            msg,
            [r"^-+$", r"^-+$", r"^Found 0 armed puzzle\(s\)\.$", r"^-+$"],
        )

            msg,
            [
                r"^-+$",
                r"^Puzzle name: makefire$",
                r"^.*steel.* at \s+ Room.*$",
                r"^.*flint.* at \s+ Room.*$",
                r"^Found 1 armed puzzle\(s\)\.$",
                r"^-+$",
            ],
        )

    def test_e2e(self):
        def _destroy_objs_in_room(keys):
            for obj in self.room1.contents:
                if obj.key in keys:
                    obj.delete()

        # parts don't survive resolution
        # but produce a large result set
        tree = create_object(self.object_typeclass, key="tree", location=self.char1.location)
        axe = create_object(self.object_typeclass, key="axe", location=self.char1.location)
        sweat = create_object(self.object_typeclass, key="sweat", location=self.char1.location)
        dull_axe = create_object(
            self.object_typeclass, key="dull axe", location=self.char1.location
        )
        timber = create_object(self.object_typeclass, key="timber", location=self.char1.location)
        log = create_object(self.object_typeclass, key="log", location=self.char1.location)
        parts = ["tree", "axe"]
        results = (["sweat"] * 10) + ["dull axe"] + (["timber"] * 20) + (["log"] * 50)
        recipe_dbref = self._good_recipe("lumberjack", parts, results, and_destroy_it=False)

        _destroy_objs_in_room(set(parts + results))

        sps = sorted(parts)
        expected = {key: len(list(grp)) for key, grp in itertools.groupby(sps)}
        expected.update({r: 0 for r in set(results)})

        self._arm(recipe_dbref, "lumberjack", parts)
        self._check_room_contents(expected)

        self._use(",".join(parts), "You are a Genius")
        srs = sorted(set(results))
        expected = {(key, len(list(grp))) for key, grp in itertools.groupby(srs)}
        expected.update({p: 0 for p in set(parts)})

        # parts also appear in results
        # causing a new puzzle to be armed 'automatically'
        # i.e. the puzzle is self-sustaining
        hole = create_object(self.object_typeclass, key="hole", location=self.char1.location)
        shovel = create_object(self.object_typeclass, key="shovel", location=self.char1.location)
        dirt = create_object(self.object_typeclass, key="dirt", location=self.char1.location)

        parts = ["shovel", "hole"]
        results = ["dirt", "hole", "shovel"]
            "digger", parts, results, and_destroy_it=False, expected_count=2
        )

        nresolutions = 0

        sps = sorted(set(parts))
        expected.update({"dirt": nresolutions})

        self._arm(recipe_dbref, "digger", parts)

        for i in range(10):
            self._use(",".join(parts), "You are a Genius")
            nresolutions += 1
            expected.update({"dirt": nresolutions})
            self._check_room_contents(expected)

        # Uppercase puzzle name
        balloon = create_object(self.object_typeclass, key="Balloon", location=self.char1.location)
        parts = ["Balloon"]
        results = ["Balloon"]
            "boom!!!", parts, results, and_destroy_it=False, expected_count=3
        )

        self._arm(recipe_dbref, "boom!!!", parts)

    def test_e2e_accumulative(self):
        flashlight = create_object(
            self.object_typeclass, key="flashlight", location=self.char1.location
        )
        flashlight_w_1 = create_object(
            self.object_typeclass, key="flashlight-w-1", location=self.char1.location
        )
        flashlight_w_2 = create_object(
            self.object_typeclass, key="flashlight-w-2", location=self.char1.location
        )
        flashlight_w_3 = create_object(
            self.object_typeclass, key="flashlight-w-3", location=self.char1.location
        )
        battery = create_object(self.object_typeclass, key="battery", location=self.char1.location)

        battery.tags.add("flashlight-1", category=puzzles._PUZZLES_TAG_CATEGORY)
        battery.tags.add("flashlight-2", category=puzzles._PUZZLES_TAG_CATEGORY)
        battery.tags.add("flashlight-3", category=puzzles._PUZZLES_TAG_CATEGORY)

        # TODO: instead of tagging each flashlight,
        # arm and resolve each puzzle in order so they all
        # are tagged correctly
        # it will be necessary to add/remove parts/results because
        # each battery is supposed to be consumed during resolution
        # as the new flashlight has one more battery than before
        flashlight_w_1.tags.add("flashlight-2", category=puzzles._PUZZLES_TAG_CATEGORY)
        flashlight_w_2.tags.add("flashlight-3", category=puzzles._PUZZLES_TAG_CATEGORY)

        recipe_fl1_dbref = self._good_recipe(
            "flashlight-1",
            ["flashlight", "battery"],
            ["flashlight-w-1"],
            and_destroy_it=False,
            expected_count=1,
        )
        recipe_fl2_dbref = self._good_recipe(
            "flashlight-2",
            ["flashlight-w-1", "battery"],
            ["flashlight-w-2"],
            expected_count=2,
        )
        recipe_fl3_dbref = self._good_recipe(
            "flashlight-3",
            ["flashlight-w-2", "battery"],
            ["flashlight-w-3"],
            expected_count=3,
        )

        # delete protoparts
        for obj in [battery, flashlight, flashlight_w_1, flashlight_w_2, flashlight_w_3]:

        def _group_parts(parts, excluding=set()):
            group = dict()
            dbrefs = dict()
                if o.key in parts and o.dbref not in excluding:
                    if o.key not in group:
                        group[o.key] = []
                    group[o.key].append(o.dbref)
                    dbrefs[o.dbref] = o
            return group, dbrefs

        # arm each puzzle and group its parts
        self._arm(recipe_fl1_dbref, "flashlight-1", ["battery", "flashlight"])
        fl1_parts, fl1_dbrefs = _group_parts(["battery", "flashlight"])
        self._arm(recipe_fl2_dbref, "flashlight-2", ["battery", "flashlight-w-1"])
        fl2_parts, fl2_dbrefs = _group_parts(
            ["battery", "flashlight-w-1"], excluding=list(fl1_dbrefs.keys())
        )
        self._arm(recipe_fl3_dbref, "flashlight-3", ["battery", "flashlight-w-2"])
        fl3_parts, fl3_dbrefs = _group_parts(
            ["battery", "flashlight-w-2"],
            excluding=set(list(fl1_dbrefs.keys()) + list(fl2_dbrefs.keys())),
        )

        self._check_room_contents(
            {
                "battery": 3,
                "flashlight": 1,
                "flashlight-w-1": 1,
                "flashlight-w-2": 1,
                "flashlight-w-3": 0,
            }
        )

        # all batteries have identical protodefs
        battery_1 = fl1_dbrefs[fl1_parts["battery"][0]]
        battery_2 = fl2_dbrefs[fl2_parts["battery"][0]]
        battery_3 = fl3_dbrefs[fl3_parts["battery"][0]]
        protodef_battery_1 = puzzles.proto_def(battery_1, with_tags=False)
        del protodef_battery_1["prototype_key"]
        protodef_battery_2 = puzzles.proto_def(battery_2, with_tags=False)
        del protodef_battery_2["prototype_key"]
        protodef_battery_3 = puzzles.proto_def(battery_3, with_tags=False)
        del protodef_battery_3["prototype_key"]
        assert protodef_battery_1 == protodef_battery_2 == protodef_battery_3

        # each battery can be used in every other puzzle

        b1_parts_dict, b1_puzzlenames, b1_protodefs = puzzles._lookups_parts_puzzlenames_protodefs(
            [battery_1]
        )
        _puzzles = puzzles._puzzles_by_names(b1_puzzlenames.keys())
        assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set(
            [p.db.puzzle_name for p in _puzzles]
        )
        matched_puzzles = puzzles._matching_puzzles(_puzzles, b1_puzzlenames, b1_protodefs)
        assert 0 == len(matched_puzzles)

        b2_parts_dict, b2_puzzlenames, b2_protodefs = puzzles._lookups_parts_puzzlenames_protodefs(
            [battery_2]
        )
        _puzzles = puzzles._puzzles_by_names(b2_puzzlenames.keys())
        )
        matched_puzzles = puzzles._matching_puzzles(_puzzles, b2_puzzlenames, b2_protodefs)
        b3_parts_dict, b3_puzzlenames, b3_protodefs = puzzles._lookups_parts_puzzlenames_protodefs(
            [battery_3]
        )
        _puzzles = puzzles._puzzles_by_names(b3_puzzlenames.keys())
        )
        matched_puzzles = puzzles._matching_puzzles(_puzzles, b3_puzzlenames, b3_protodefs)

        assert battery_1 == list(b1_parts_dict.values())[0]
        assert battery_2 == list(b2_parts_dict.values())[0]
        assert battery_3 == list(b3_parts_dict.values())[0]
        assert b1_puzzlenames.keys() == b2_puzzlenames.keys() == b3_puzzlenames.keys()
        for puzzle_name in ["flashlight-1", "flashlight-2", "flashlight-3"]:
            assert puzzle_name in b1_puzzlenames
            assert puzzle_name in b2_puzzlenames
            assert puzzle_name in b3_puzzlenames
        assert (
            list(b1_protodefs.values())[0]
            == list(b2_protodefs.values())[0]
            == list(b3_protodefs.values())[0]
            == protodef_battery_1
            == protodef_battery_2
            == protodef_battery_3
        )

        # all flashlights have similar protodefs except their key
        flashlight_1 = fl1_dbrefs[fl1_parts["flashlight"][0]]
        flashlight_2 = fl2_dbrefs[fl2_parts["flashlight-w-1"][0]]
        flashlight_3 = fl3_dbrefs[fl3_parts["flashlight-w-2"][0]]
        protodef_flashlight_1 = puzzles.proto_def(flashlight_1, with_tags=False)
        del protodef_flashlight_1["prototype_key"]
        assert protodef_flashlight_1["key"] == "flashlight"
        del protodef_flashlight_1["key"]
        protodef_flashlight_2 = puzzles.proto_def(flashlight_2, with_tags=False)
        del protodef_flashlight_2["prototype_key"]
        assert protodef_flashlight_2["key"] == "flashlight-w-1"
        del protodef_flashlight_2["key"]
        protodef_flashlight_3 = puzzles.proto_def(flashlight_3, with_tags=False)
        del protodef_flashlight_3["prototype_key"]
        assert protodef_flashlight_3["key"] == "flashlight-w-2"
        del protodef_flashlight_3["key"]
        assert protodef_flashlight_1 == protodef_flashlight_2 == protodef_flashlight_3

        # each flashlight can only be used in its own puzzle

        f1_parts_dict, f1_puzzlenames, f1_protodefs = puzzles._lookups_parts_puzzlenames_protodefs(
            [flashlight_1]
        )
        _puzzles = puzzles._puzzles_by_names(f1_puzzlenames.keys())
        assert set(["flashlight-1"]) == set([p.db.puzzle_name for p in _puzzles])
        matched_puzzles = puzzles._matching_puzzles(_puzzles, f1_puzzlenames, f1_protodefs)
        f2_parts_dict, f2_puzzlenames, f2_protodefs = puzzles._lookups_parts_puzzlenames_protodefs(
            [flashlight_2]
        )
        _puzzles = puzzles._puzzles_by_names(f2_puzzlenames.keys())
        assert set(["flashlight-2"]) == set([p.db.puzzle_name for p in _puzzles])
        matched_puzzles = puzzles._matching_puzzles(_puzzles, f2_puzzlenames, f2_protodefs)
        f3_parts_dict, f3_puzzlenames, f3_protodefs = puzzles._lookups_parts_puzzlenames_protodefs(
            [flashlight_3]
        )
        _puzzles = puzzles._puzzles_by_names(f3_puzzlenames.keys())
        assert set(["flashlight-3"]) == set([p.db.puzzle_name for p in _puzzles])
        matched_puzzles = puzzles._matching_puzzles(_puzzles, f3_puzzlenames, f3_protodefs)

        assert flashlight_1 == list(f1_parts_dict.values())[0]
        assert flashlight_2 == list(f2_parts_dict.values())[0]
        assert flashlight_3 == list(f3_parts_dict.values())[0]
        for puzzle_name in set(
            list(f1_puzzlenames.keys()) + list(f2_puzzlenames.keys()) + list(f3_puzzlenames.keys())
        ):
            assert puzzle_name in ["flashlight-1", "flashlight-2", "flashlight-3", "puzzle_member"]
        protodef_flashlight_1["key"] = "flashlight"
        assert list(f1_protodefs.values())[0] == protodef_flashlight_1
        protodef_flashlight_2["key"] = "flashlight-w-1"
        assert list(f2_protodefs.values())[0] == protodef_flashlight_2
        protodef_flashlight_3["key"] = "flashlight-w-2"
        assert list(f3_protodefs.values())[0] == protodef_flashlight_3

        # each battery can be matched with every other flashlight
        # to potentially resolve each puzzle
        for batt in [battery_1, battery_2, battery_3]:
            parts_dict, puzzlenames, protodefs = puzzles._lookups_parts_puzzlenames_protodefs(
                [batt, flashlight_1]
            )
            assert set([batt.dbref, flashlight_1.dbref]) == set(puzzlenames["flashlight-1"])
            assert set([batt.dbref]) == set(puzzlenames["flashlight-2"])
            assert set([batt.dbref]) == set(puzzlenames["flashlight-3"])
            _puzzles = puzzles._puzzles_by_names(puzzlenames.keys())
            assert set(["flashlight-1", "flashlight-2", "flashlight-3"]) == set(
                [p.db.puzzle_name for p in _puzzles]
            )
            matched_puzzles = puzzles._matching_puzzles(_puzzles, puzzlenames, protodefs)
            assert 1 == len(matched_puzzles)
                [batt, flashlight_2]
            )
            assert set([batt.dbref]) == set(puzzlenames["flashlight-1"])
            assert set([batt.dbref, flashlight_2.dbref]) == set(puzzlenames["flashlight-2"])
            )
                [batt, flashlight_3]
            )
            assert set([batt.dbref, flashlight_3.dbref]) == set(puzzlenames["flashlight-3"])
            )

        # delete all parts
        for part in (
            list(fl1_dbrefs.values()) + list(fl2_dbrefs.values()) + list(fl3_dbrefs.values())
        ):
            part.delete()

            {
                "battery": 0,
                "flashlight": 0,
                "flashlight-w-1": 0,
                "flashlight-w-2": 0,
            }
        )

        # arm first puzzle 3 times and group its parts so we can solve
        # all puzzles with the parts from the 1st armed
        for i in range(3):
            self._arm(recipe_fl1_dbref, "flashlight-1", ["battery", "flashlight"])

        # delete the 2 extra flashlights so we can start solving
        for flashlight_dbref in fl1_parts["flashlight"][1:]:
            fl1_dbrefs[flashlight_dbref].delete()

            {
            }
        )

        self._use("battery-1, flashlight", "You are a Genius")
            {
                "battery": 2,
            }
        )

        self._use("battery-1, flashlight-w-1", "You are a Genius")
            {
                "battery": 1,
            }
        )

        self._use("battery, flashlight-w-2", "You are a Genius")
            {
                "flashlight-w-3": 1,
            }
        )

    def test_e2e_interchangeable_parts_and_results(self):
        # Parts and Results can be used in multiple puzzles
        egg = create_object(self.object_typeclass, key="egg", location=self.char1.location)
        flour = create_object(self.object_typeclass, key="flour", location=self.char1.location)
        boiling_water = create_object(
            self.object_typeclass, key="boiling water", location=self.char1.location
        )
        boiled_egg = create_object(
            self.object_typeclass, key="boiled egg", location=self.char1.location
        )
        dough = create_object(self.object_typeclass, key="dough", location=self.char1.location)
        pasta = create_object(self.object_typeclass, key="pasta", location=self.char1.location)

        # Three recipes:
        # 1. breakfast: egg + boiling water = boiled egg & boiling water
        # 2. dough: egg + flour = dough
        # 3. entree: dough + boiling water = pasta & boiling water
        # tag interchangeable parts according to their puzzles' name
        egg.tags.add("breakfast", category=puzzles._PUZZLES_TAG_CATEGORY)
        egg.tags.add("dough", category=puzzles._PUZZLES_TAG_CATEGORY)
        dough.tags.add("entree", category=puzzles._PUZZLES_TAG_CATEGORY)
        boiling_water.tags.add("breakfast", category=puzzles._PUZZLES_TAG_CATEGORY)
        boiling_water.tags.add("entree", category=puzzles._PUZZLES_TAG_CATEGORY)

        # create recipes
        recipe1_dbref = self._good_recipe(
            "breakfast",
            ["egg", "boiling water"],
            ["boiled egg", "boiling water"],
        )
            "dough", ["egg", "flour"], ["dough"], and_destroy_it=False, expected_count=2
        )
        recipe3_dbref = self._good_recipe(
            "entree",
            ["dough", "boiling water"],
            ["pasta", "boiling water"],
        )

        for obj in [egg, flour, boiling_water, boiled_egg, dough, pasta]:

        self._arm(recipe1_dbref, "breakfast", ["egg", "boiling water"])
        breakfast_parts, breakfast_dbrefs = _group_parts(["egg", "boiling water"])
        self._arm(recipe2_dbref, "dough", ["egg", "flour"])
        dough_parts, dough_dbrefs = _group_parts(
            ["egg", "flour"], excluding=list(breakfast_dbrefs.keys())
        )
        self._arm(recipe3_dbref, "entree", ["dough", "boiling water"])
        entree_parts, entree_dbrefs = _group_parts(
            excluding=set(list(breakfast_dbrefs.keys()) + list(dough_dbrefs.keys())),
        )

            # print "boxing all\n", "-"*20
                    # print o.key, o.dbref, "boxed"
                else:
                    # print "skipped", o.key, o.dbref
                    pass

        def _unbox(dbrefs):
            # print "unboxing", dbrefs, "\n", "-"*20
            for o in self.box.contents:
                if o.dbref in dbrefs:
                    o.location = self.room1
                    # print "unboxed", o.key, o.dbref

        # solve dough puzzle using breakfast's egg
        # and dough's flour. A new dough will be created
        _unbox(breakfast_parts.pop("egg") + dough_parts.pop("flour"))
        self._use("egg, flour", "You are a Genius")

        # solve entree puzzle with newly created dough
        # and breakfast's boiling water. A new
        # boiling water and pasta will be created
        _unbox(breakfast_parts.pop("boiling water"))
        self._use("boiling water, dough", "You are a Genius")

        # solve breakfast puzzle with dough's egg
        # and newly created boiling water. A new
        # boiling water and boiled egg will be created
        _unbox(dough_parts.pop("egg"))
        self._use("boiling water, egg", "You are a Genius")

        # solve entree puzzle using entree's dough
        _unbox(entree_parts.pop("dough"))

        self._check_room_contents({"boiling water": 1, "pasta": 2, "boiled egg": 1})

"""
Simple turn-based combat system

Contrib - Tim Ashley Jenkins 2017, Refactor by Griatch 2022

This is a framework for a simple turn-based combat system, similar
to those used in D&D-style tabletop role playing games. It allows
any character to start a fight in a room, at which point initiative
is rolled and a turn order is established. Each participant in combat
has a limited time to decide their action for that turn (30 seconds by
default), and combat progresses through the turn order, looping through
the participants until the fight ends.

Only simple rolls for attacking are implemented here, but this system
is easily extensible and can be used as the foundation for implementing
the rules from your turn-based tabletop game of choice or making your
own battle system.

To install and test, import this module's TBBasicCharacter object into
your game's character.py module:

    from evennia.contrib.game_systems.turnbattle.tb_basic import TBBasicCharacter

And change your game's character typeclass to inherit from TBBasicCharacter
instead of the default:

    class Character(TBBasicCharacter):

Next, import this module into your default_cmdsets.py module:

    from evennia.contrib.game_systems.turnbattle import tb_basic

And add the battle command set to your default command set:

    #
    # any commands you add below will overload the default ones.
    #
    self.add(tb_basic.BattleCmdSet())

This module is meant to be heavily expanded on, so you may want to copy it
to your game's 'world' folder and modify it there rather than importing it
in your game and using it as-is.
"""

from random import randint

from evennia import Command, DefaultCharacter, DefaultScript, default_cmds
from evennia.commands.default.help import CmdHelp

"""
----------------------------------------------------------------------------
OPTIONS
"""

TURN_TIMEOUT = 30  # Time before turns automatically end, in seconds
ACTIONS_PER_TURN = 1  # Number of actions allowed per turn

"""
COMBAT FUNCTIONS START HERE
"""

class BasicCombatRules:
    """
    Stores all combat rules and helper methods.

    """

    def roll_init(self, character):
        """
        Rolls a number between 1-1000 to determine initiative.

        Args:
            character (obj): The character to determine initiative for

        Returns:
            initiative (int): The character's place in initiative - higher
            numbers go first.

        Notes:
            By default, does not reference the character and simply returns
            a random integer from 1 to 1000.

            Since the character is passed to this function, you can easily reference
            a character's stats to determine an initiative roll - for example, if your
            character has a 'dexterity' attribute, you can use it to give that character
            an advantage in turn order, like so:

            return (randint(1,20)) + character.db.dexterity

            This way, characters with a higher dexterity will go first more often.
        """
        return randint(1, 1000)

    def get_attack(self, attacker, defender):
        """
        Returns a value for an attack roll.

        Args:
            attacker (obj): Character doing the attacking
            defender (obj): Character being attacked

        Returns:
            attack_value (int): Attack roll value, compared against a defense value
                to determine whether an attack hits or misses.

        Notes:
            By default, returns a random integer from 1 to 100 without using any
            properties from either the attacker or defender.

            This can easily be expanded to return a value based on characters stats,
            equipment, and abilities. This is why the attacker and defender are passed
            to this function, even though nothing from either one are used in this example.
        """
        # For this example, just return a random integer up to 100.
        attack_value = randint(1, 100)
        return attack_value

    def get_defense(self, attacker, defender):
        """
        Returns a value for defense, which an attack roll must equal or exceed in order
        for an attack to hit.

        Args:

        Returns:
            defense_value (int): Defense value, compared against an attack roll

        Notes:
            By default, returns 50, not taking any properties of the defender or
            attacker into account.

            As above, this can be expanded upon based on character stats and equipment.
        """
        # For this example, just return 50, for about a 50/50 chance of hit.
        defense_value = 50
        return defense_value

    def get_damage(self, attacker, defender):
        """
        Returns a value for damage to be deducted from the defender's HP after abilities
        successful hit.

        Args:
            defender (obj): Character being damaged

        Returns:
            damage_value (int): Damage value, which is to be deducted from the defending
                character's HP.

        Notes:
            By default, returns a random integer from 15 to 25 without using any

            Again, this can be expanded upon.
        """
        # For this example, just generate a number between 15 and 25.
        damage_value = randint(15, 25)
        return damage_value

    def apply_damage(self, defender, damage):
        """
        Applies damage to a target, reducing their HP by the damage amount to a
        minimum of 0.

        Args:
            defender (obj): Character taking damage
            damage (int): Amount of damage being taken
        """
        defender.db.hp -= damage  # Reduce defender's HP by the damage dealt.
        # If this reduces it to 0 or less, set HP to 0.
        if defender.db.hp <= 0:
            defender.db.hp = 0

    def at_defeat(self, defeated):
        """
        Announces the defeat of a fighter in combat.

        Args:
            defeated (obj): Fighter that's been defeated.

        Notes:
            All this does is announce a defeat message by default, but if you
            want anything else to happen to defeated fighters (like putting them
            into a dying state or something similar) then this is the place to
            do it.
        """
        defeated.location.msg_contents("%s has been defeated!" % defeated)

    def resolve_attack(self, attacker, defender, attack_value=None, defense_value=None):
        """
        Resolves an attack and outputs the result.

        Args:

        Notes:
            Even though the attack and defense values are calculated
            extremely simply, they are separated out into their own functions
            so that they are easier to expand upon.
        """
        # Get an attack roll from the attacker.
        if not attack_value:
            attack_value = self.get_attack(attacker, defender)
        # Get a defense value from the defender.
        if not defense_value:
            defense_value = self.get_defense(attacker, defender)
        # If the attack value is lower than the defense value, miss. Otherwise, hit.
        if attack_value < defense_value:
            attacker.location.msg_contents("%s's attack misses %s!" % (attacker, defender))
        else:
            damage_value = self.get_damage(attacker, defender)  # Calculate damage value.
            # Announce damage dealt and apply damage.
            attacker.location.msg_contents(
                "%s hits %s for %i damage!" % (attacker, defender, damage_value)
            )
            self.apply_damage(defender, damage_value)
            # If defender HP is reduced to 0 or less, call at_defeat.
            if defender.db.hp <= 0:
                self.at_defeat(defender)

    def combat_cleanup(self, character):
        """
        Cleans up all the temporary combat-related attributes on a character.

        Args:
            character (obj): Character to have their combat attributes removed

        Notes:
            Any attribute whose key begins with 'combat_' is temporary and no
            longer needed once a fight ends.
        """
        for attr in character.attributes.all():
            if attr.key[:7] == "combat_":  # If the attribute name starts with 'combat_'...
                character.attributes.remove(key=attr.key)  # ...then delete it!

    def is_in_combat(self, character):
        """
        Returns true if the given character is in combat.

        Args:
            character (obj): Character to determine if is in combat or not

        Returns:
            (bool): True if in combat or False if not in combat
        """
        return bool(character.db.combat_turnhandler)

    def is_turn(self, character):
        """
        Returns true if it's currently the given character's turn in combat.

        Args:
            character (obj): Character to determine if it is their turn or not

        Returns:
            (bool): True if it is their turn or False otherwise
        """
        turnhandler = character.db.combat_turnhandler
        currentchar = turnhandler.db.fighters[turnhandler.db.turn]
        return bool(character == currentchar)

    def spend_action(self, character, actions, action_name=None):
        """
        Spends a character's available combat actions and checks for end of turn.

        Args:
            character (obj): Character spending the action
            actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions

            action_name (str or None): If a string is given, sets character's last action in
            combat to provided string
        """
        if action_name:
            character.db.combat_lastaction = action_name
        if actions == "all":  # If spending all actions
            character.db.combat_actionsleft = 0  # Set actions to 0
        else:
            character.db.combat_actionsleft -= actions  # Use up actions.
            if character.db.combat_actionsleft < 0:
                character.db.combat_actionsleft = 0  # Can't have fewer than 0 actions
        character.db.combat_turnhandler.turn_end_check(character)  # Signal potential end of turn.

COMBAT_RULES = BasicCombatRules()

"""
CHARACTER TYPECLASS
"""

class TBBasicCharacter(DefaultCharacter):
    """
    A character able to participate in turn-based combat. Has attributes for current
    and maximum HP, and access to combat commands.
    """

    rules = COMBAT_RULES

        """
        Called once, when this object is first created. This is the
        normal hook to overload for most object types.
        """
        self.db.max_hp = 100  # Set maximum HP to 100
        self.db.hp = self.db.max_hp  # Set current HP to maximum
        """
        Adds attributes for a character's current and maximum HP.
        We're just going to set this value at '100' by default.

        You may want to expand this to include various 'stats' that
        can be changed at creation and factor into combat calculations.
        """

    def at_pre_move(self, destination, move_type="move", **kwargs):
        """
        destination.

        Args:
            destination (Object): The object we are moving to

        Returns:
            shouldmove (bool): If we should move or not.

        Notes:

        """
        # Keep the character from moving if at 0 HP or in combat.
        if self.rules.is_in_combat(self):
            self.msg("You can't exit a room while in combat!")
            return False  # Returning false keeps the character from moving.
        if self.db.HP <= 0:
            self.msg("You can't move, you've been defeated!")

"""
SCRIPTS START HERE
"""

class TBBasicTurnHandler(DefaultScript):
    """
    This is the script that handles the progression of combat through turns.
    On creation (when a fight is started) it adds all combat-ready characters
    to its roster and then sorts them into a turn order. There can only be one
    fight going on in a single room at a time, so the script is assigned to a
    room as its object.

    Fights persist until only one participant is left with any HP or all
    remaining participants choose to end the combat with the 'disengage' command.
    """

        """
        Called once, when the script is created.
        """
        self.key = "Combat Turn Handler"
        self.interval = 5  # Once every 5 seconds
        self.db.fighters = []

        # Add all fighters in the room with at least 1 HP to the combat."
        for thing in self.obj.contents:
            if thing.db.hp:
                self.db.fighters.append(thing)

        # Initialize each fighter for combat
        for fighter in self.db.fighters:
            self.initialize_for_combat(fighter)

        # Add a reference to this script to the room
        self.obj.db.combat_turnhandler = self

        # Roll initiative and sort the list of fighters depending on who rolls highest to determine
        # turn order.  The initiative roll is determined by the roll_init method and can be
        # customized easily.
        ordered_by_roll = sorted(self.db.fighters, key=self.rules.roll_init, reverse=True)
        self.db.fighters = ordered_by_roll

        # Announce the turn order.
        self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters))

        # Start first fighter's turn.
        self.start_turn(self.db.fighters[0])

        # Set up the current turn and turn timeout delay.
        self.db.turn = 0
        self.db.timer = TURN_TIMEOUT  # Set timer to turn timeout specified in options

    def at_stop(self):
        """
        Called at script termination.
        """
            if fighter:
                # Clean up the combat attributes for every fighter.
                self.rules.combat_cleanup(fighter)
        self.obj.db.combat_turnhandler = None  # Remove reference to turn handler in location

        """
        Called once every self.interval seconds.
        """
        currentchar = self.db.fighters[
            self.db.turn
        ]  # Note the current character in the turn order.
        self.db.timer -= self.interval  # Count down the timer.

        if self.db.timer <= 0:
            # Force current character to disengage if timer runs out.
            self.obj.msg_contents("%s's turn timed out!" % currentchar)
            self.rules.spend_action(
                currentchar, "all", action_name="disengage"
            )  # Spend all remaining actions.
            return
        elif self.db.timer <= 10 and not self.db.timeout_warning_given:  # 10 seconds left
            # Warn the current character if they're about to time out.
            currentchar.msg("WARNING: About to time out!")
            self.db.timeout_warning_given = True

    def initialize_for_combat(self, character):
        """
        Prepares a character for combat when starting or entering a fight.

        Args:
            character (obj): Character to initialize for combat.
        """
        # Clean up leftover combat attributes beforehand, just in case.
        self.rules.combat_cleanup(character)
        character.db.combat_actionsleft = (
            0  # Actions remaining - start of turn adds to this, turn ends when it reaches 0
        )
        character.db.combat_turnhandler = (
            self  # Add a reference to this turn handler script to the character
        )
        character.db.combat_lastaction = "null"  # Track last action taken in combat

    def start_turn(self, character):
        """
        Readies a character for the start of their turn by replenishing their
        available actions and notifying them that their turn has come up.

        Args:
            character (obj): Character to be readied.

        Notes:
            Here, you only get one action per turn, but you might want to allow more than
            one per turn, or even grant a number of actions based on a character's
            attributes. You can even add multiple different kinds of actions, I.E. actions
            separated for movement, by adding "character.db.combat_movesleft = 3" or
            something similar.
        """
        character.db.combat_actionsleft = ACTIONS_PER_TURN  # Replenish actions
        # Prompt the character for their turn and give some information.
        character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp)

    def next_turn(self):
        """
        Advances to the next character in the turn order.
        """

        # Check to see if every character disengaged as their last action. If so, end combat.
        disengage_check = True
            if (
                fighter.db.combat_lastaction != "disengage"
            ):  # If a character has done anything but disengage
                disengage_check = False
        if disengage_check:  # All characters have disengaged
            self.obj.msg_contents("All fighters have disengaged! Combat is over!")
            self.stop()  # Stop this script and end combat.
            return

        # Check to see if only one character is left standing. If so, end combat.
        defeated_characters = 0
            if fighter.db.HP == 0:
                defeated_characters += 1  # Add 1 for every fighter with 0 HP left (defeated)
        if defeated_characters == (
            len(self.db.fighters) - 1
        ):  # If only one character isn't defeated
            for fighter in self.db.fighters:
                if fighter.db.HP != 0:
                    LastStanding = fighter  # Pick the one fighter left with HP remaining
            self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding)
            return

        # Cycle to the next turn.
        currentchar = self.db.fighters[self.db.turn]
        self.db.turn += 1  # Go to the next in the turn order.
        if self.db.turn > len(self.db.fighters) - 1:
            self.db.turn = 0  # Go back to the first in the turn order once you reach the end.
        newchar = self.db.fighters[self.db.turn]  # Note the new character
        self.db.timer = TURN_TIMEOUT + self.time_until_next_repeat()  # Reset the timer.
        self.db.timeout_warning_given = False  # Reset the timeout warning.
        self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar))
        self.start_turn(newchar)  # Start the new character's turn.

    def turn_end_check(self, character):
        """
        Tests to see if a character's turn is over, and cycles to the next turn if it is.

        Args:
            character (obj): Character to test for end of turn
        """
        if not character.db.combat_actionsleft:  # Character has no actions remaining
            self.next_turn()
            return

    def join_fight(self, character):
        """
        Adds a new character to a fight already in progress.

        Args:
            character (obj): Character to be added to the fight.
        """
        # Inserts the fighter to the turn order, right behind whoever's turn it currently is.
        self.db.fighters.insert(self.db.turn, character)
        # Tick the turn counter forward one to compensate.
        self.db.turn += 1
        # Initialize the character like you do at the start.
        self.initialize_for_combat(character)

"""
COMMANDS START HERE
"""

class CmdFight(Command):
    """
    Starts a fight with everyone in the same room as you.

    Usage:
      fight

    When you start a fight, everyone in the room who is able to
    fight is added to combat, and a turn order is randomly rolled.
    When it's your turn, you can attack other characters.
    """

    key = "fight"
    help_category = "combat"

    combat_handler_class = TBBasicTurnHandler

        """
        """
        here = self.caller.location
        fighters = []

        if not self.caller.db.hp:  # If you don't have any hp
            self.caller.msg("You can't start a fight if you've been defeated!")
            return
        if self.rules.is_in_combat(self.caller):  # Already in a fight
            self.caller.msg("You're already in a fight!")
            return
        for thing in here.contents:  # Test everything in the room to add it to the fight.
            if thing.db.HP:  # If the object has HP...
                fighters.append(thing)  # ...then add it to the fight.
        if len(fighters) <= 1:  # If you're the only able fighter in the room
            self.caller.msg("There's nobody here to fight!")
            return
        if here.db.combat_turnhandler:  # If there's already a fight going on...
            here.msg_contents("%s joins the fight!" % self.caller)
            here.db.combat_turnhandler.join_fight(self.caller)  # Join the fight!
            return
        here.msg_contents("%s starts a fight!" % self.caller)
        # Add a turn handler script to the room, which starts combat.
        here.scripts.add(self.command_handler_class)

class CmdAttack(Command):
    """
    Attacks another character.

    Usage:
      attack <target>

    When in a fight, you may attack another character. The attack has
    a chance to hit, and if successful, will deal damage.
    """

    key = "attack"

        "This performs the actual command."
        "Set the attacker to the caller and the defender to the target."

        if not self.rules.is_in_combat(self.caller):  # If not in combat, can't attack.
            self.caller.msg("You can only do that in combat. (see: help fight)")
            return

        if not self.rules.is_turn(self.caller):  # If it's not your turn, can't attack.
            self.caller.msg("You can only do that on your turn.")
            return

        if not self.caller.db.hp:  # Can't attack if you have no HP.
            self.caller.msg("You can't attack, you've been defeated.")
            return

        attacker = self.caller
        defender = self.caller.search(self.args)

        if not defender:  # No valid target given.
            return

        if not defender.db.hp:  # Target object has no HP left or to begin with
            self.caller.msg("You can't fight that!")
            return

        if attacker == defender:  # Target and attacker are the same
            self.caller.msg("You can't attack yourself!")
            return

        "If everything checks out, call the attack resolving function."
        self.rules.resolve_attack(attacker, defender)
        self.rules.spend_action(self.caller, 1, action_name="attack")  # Use up one action.

class CmdPass(Command):
    """
    Passes on your turn.

    Usage:
      pass

    When in a fight, you can use this command to end your turn early, even
    if there are still any actions you can take.
    """

    key = "pass"
    aliases = ["wait", "hold"]

        """
        """
        if not self.rules.is_in_combat(self.caller):  # Can only pass a turn in combat.
            return

        if not self.rules.is_turn(self.caller):  # Can only pass if it's your turn.
            return

            "%s takes no further action, passing the turn." % self.caller
        )
        # Spend all remaining actions.
        self.rules.spend_action(self.caller, "all", action_name="pass")

class CmdDisengage(Command):
    """
    Passes your turn and attempts to end combat.

    Usage:
      disengage

    Ends your turn early and signals that you're trying to end
    the fight. If all participants in a fight disengage, the
    fight ends.
    """

    key = "disengage"
    aliases = ["spare"]

        """
        """
        if not self.rules.is_in_combat(self.caller):  # If you're not in combat
            return

        if not self.rules.is_turn(self.caller):  # If it's not your turn
            return

        self.caller.location.msg_contents("%s disengages, ready to stop fighting." % self.caller)
        self.rules.spend_action(self.caller, "all", action_name="disengage")
        """
        The action_name kwarg sets the character's last action to "disengage", which is checked by
        the turn handler script to see if all fighters have disengaged.
        """

class CmdRest(Command):
    """
    Recovers damage.

    Usage:
      rest

    Resting recovers your HP to its maximum, but you can only
    rest if you're not in a fight.
    """

    key = "rest"

        if self.rules.is_in_combat(self.caller):  # If you're in combat
            self.caller.msg("You can't rest while you're in combat.")
            return

        self.caller.db.hp = self.caller.db.max_hp  # Set current HP to maximum
        self.caller.location.msg_contents("%s rests to recover HP." % self.caller)
        """
        You'll probably want to replace this with your own system for recovering HP.
        """

class CmdCombatHelp(CmdHelp):
    """
    View help or a list of topics

    Usage:
      help <topic or command>
      help list
      help all

    This will search for help on commands and other
    topics related to the game.
    """

    combat_help_text = (
        "Available combat commands:|/"
        "|wAttack:|n Attack a target, attempting to deal damage.|/"
        "|wPass:|n Pass your turn without further action.|/"
        "|wDisengage:|n End your turn and attempt to end combat.|/"
    )

    # Just like the default help command, but will give quick
    # tips on combat when used in a fight with no arguments.

        # In combat and entered 'help' alone
        if self.rules.is_in_combat(self.caller) and not self.args:
            self.caller.msg(self.combat_help_text)
        else:
            super().func()  # Call the default help command

class BattleCmdSet(default_cmds.CharacterCmdSet):
    """
    This command set includes all the commmands used in the battle system.
    """

        """
        """
        self.add(CmdFight())
        self.add(CmdAttack())
        self.add(CmdRest())
        self.add(CmdPass())
        self.add(CmdDisengage())
        self.add(CmdCombatHelp())

"""
Simple turn-based combat system with equipment

This is a version of the 'turnbattle' contrib with a basic system for
weapons and armor implemented. Weapons can have unique damage ranges
and accuracy modifiers, while armor can reduce incoming damage and
change one's chance of getting hit. The 'wield' command is used to
equip weapons and the 'don' command is used to equip armor.

Some prototypes are included at the end of this module - feel free to
copy them into your game's prototypes.py module in your 'world' folder
and create them with the @spawn command. (See the tutorial for using
the @spawn command for details.)

For the example equipment given, heavier weapons deal more damage
but are less accurate, while light weapons are more accurate but
deal less damage. Similarly, heavy armor reduces incoming damage by
a lot but increases your chance of getting hit, while light armor is
easier to dodge in but reduces incoming damage less. Light weapons are
more effective against lightly armored opponents and heavy weapons are
more damaging against heavily armored foes, but heavy weapons and armor
are slightly better than light weapons and armor overall.

This is a fairly bare implementation of equipment that is meant to be
expanded to fit your game - weapon and armor slots, damage types and
damage bonuses, etc. should be fairly simple to implement according to
the rules of your preferred system or the needs of your own game.

To install and test, import this module's TBEquipCharacter object into

    from evennia.contrib.game_systems.turnbattle.tb_equip import TBEquipCharacter

And change your game's character typeclass to inherit from TBEquipCharacter

    class Character(TBEquipCharacter):

    from evennia.contrib.game_systems.turnbattle import tb_equip

    #
    #
    self.add(tb_equip.BattleCmdSet())

"""

from evennia import Command, DefaultObject, default_cmds

from . import tb_basic

"""
OPTIONS
"""

"""
"""

class EquipmentCombatRules(tb_basic.BasicCombatRules):
    """
    Has all the methods of the basic combat, with the addition of equipment.

    """

        """

        Args:

        Returns:

        Notes:
            In this example, a weapon's accuracy bonus is factored into the attack
            roll. Lighter weapons are more accurate but less damaging, and heavier
            weapons are less accurate but deal more damage. Of course, you can
            change this paradigm completely in your own game.
        """
        # Start with a roll from 1 to 100.
        accuracy_bonus = 0
        # If armed, add weapon's accuracy bonus.
        if attacker.db.wielded_weapon:
            weapon = attacker.db.wielded_weapon
            accuracy_bonus += weapon.db.accuracy_bonus
        # If unarmed, use character's unarmed accuracy bonus.
        else:
            accuracy_bonus += attacker.db.unarmed_accuracy
        # Add the accuracy bonus to the attack roll.
        attack_value += accuracy_bonus

        """

        Args:

        Returns:

        Notes:
            Characters are given a default defense value of 50 which can be
            modified up or down by armor. In this example, wearing armor actually
            makes you a little easier to hit, but reduces incoming damage.
        """
        # Start with a defense value of 50 for a 50/50 chance to hit.
        # Modify this value based on defender's armor.
        if defender.db.worn_armor:
            armor = defender.db.worn_armor
            defense_value += armor.db.defense_modifier

        """

        Args:

        Returns:

        Notes:
            Damage is determined by the attacker's wielded weapon, or the attacker's
            unarmed damage range if no weapon is wielded. Incoming damage is reduced
            by the defender's armor.
        """
        damage_value = 0
        # Generate a damage value from wielded weapon if armed
            # Roll between minimum and maximum damage
            damage_value = randint(weapon.db.damage_range[0], weapon.db.damage_range[1])
        # Use attacker's unarmed damage otherwise
        else:
            damage_value = randint(
                attacker.db.unarmed_damage_range[0], attacker.db.unarmed_damage_range[1]
            )
        # If defender is armored, reduce incoming damage
            damage_value -= armor.db.damage_reduction
        # Make sure minimum damage is 0
        if damage_value < 0:
            damage_value = 0

        """

        Args:

        Notes:
        """
        # Get the attacker's weapon type to reference in combat messages.
        attackers_weapon = "attack"
            attackers_weapon = weapon.db.weapon_type_name
                "%s's %s misses %s!" % (attacker, attackers_weapon, defender)
            )
        else:
            if damage_value > 0:
                attacker.location.msg_contents(
                    "%s's %s strikes %s for %i damage!"
                    % (attacker, attackers_weapon, defender, damage_value)
                )
            else:
                    "%s's %s bounces harmlessly off %s!" % (attacker, attackers_weapon, defender)
                )

COMBAT_RULES = EquipmentCombatRules()

"""
"""

class TBEquipTurnHandler(tb_basic.TBBasicTurnHandler):
    """

    """

"""
TYPECLASSES START HERE
"""

class TBEWeapon(DefaultObject):
    """
    A weapon which can be wielded in combat with the 'wield' command.

    """

        """
        """
        self.db.damage_range = (15, 25)  # Minimum and maximum damage on hit
        self.db.accuracy_bonus = 0  # Bonus to attack rolls (or penalty if negative)
        self.db.weapon_type_name = (
            "weapon"  # Single word for weapon - I.E. "dagger", "staff", "scimitar"
        )

    def at_drop(self, dropper):
        """
        Stop being wielded if dropped.
        """
        if dropper.db.wielded_weapon == self:
            dropper.db.wielded_weapon = None
            dropper.location.msg_contents("%s stops wielding %s." % (dropper, self))

    def at_give(self, giver, getter):
        """
        Stop being wielded if given.
        """
        if giver.db.wielded_weapon == self:
            giver.db.wielded_weapon = None
            giver.location.msg_contents("%s stops wielding %s." % (giver, self))

class TBEArmor(DefaultObject):
    """
    A set of armor which can be worn with the 'don' command.
    """

        """
        """
        self.db.damage_reduction = 4  # Amount of incoming damage reduced by armor
        self.db.defense_modifier = (
            -4
        )  # Amount to modify defense value (pos = harder to hit, neg = easier)

    def at_pre_drop(self, dropper):
        """
        Can't drop in combat.
        """
        if self.rules.is_in_combat(dropper):
            dropper.msg("You can't doff armor in a fight!")

        """
        """
        if dropper.db.worn_armor == self:
            dropper.db.worn_armor = None
            dropper.location.msg_contents("%s removes %s." % (dropper, self))

    def at_pre_give(self, giver, getter):
        """
        Can't give away in combat.
        """
        if self.rules.is_in_combat(giver):

        """
        """
        if giver.db.worn_armor == self:
            giver.db.worn_armor = None
            giver.location.msg_contents("%s removes %s." % (giver, self))

class TBEquipCharacter(tb_basic.TBBasicCharacter):
    """
    """

        """
        """
        self.db.wielded_weapon = None  # Currently used weapon
        self.db.worn_armor = None  # Currently worn armor
        self.db.unarmed_damage_range = (5, 15)  # Minimum and maximum unarmed damage
        self.db.unarmed_accuracy = 30  # Accuracy bonus for unarmed attacks

        """

        """

"""
"""

class CmdFight(tb_basic.CmdFight):
    """

    Usage:
      fight

    """

    command_handler_class = TBEquipTurnHandler

class CmdAttack(tb_basic.CmdAttack):
    """

    Usage:

    """

class CmdPass(tb_basic.CmdPass):
    """

    Usage:
      pass

    """

class CmdDisengage(tb_basic.CmdDisengage):
    """

    Usage:
      disengage

    """

class CmdRest(tb_basic.CmdRest):
    """

    Usage:
      rest

    """

class CmdCombatHelp(tb_basic.CmdCombatHelp):
    """

    Usage:
      help list
      help all

    """

class CmdWield(Command):
    """
    Wield a weapon you are carrying

    Usage:
      wield <weapon>

    Select a weapon you are carrying to wield in combat. If
    you are already wielding another weapon, you will switch
    to the weapon you specify instead. Using this command in
    combat will spend your action for your turn. Use the
    "unwield" command to stop wielding any weapon you are
    currently wielding.
    """

    key = "wield"

        """
        """
        # If in combat, check to see if it's your turn.
        if self.rules.is_in_combat(self.caller):
            if not self.rules.is_turn(self.caller):
                self.caller.msg("You can only do that on your turn.")
                return
            self.caller.msg("Usage: wield <obj>")
            return
        weapon = self.caller.search(self.args, candidates=self.caller.contents)
        if not weapon:
            return
        if not weapon.is_typeclass(
            "evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon", exact=True
        ):
            self.caller.msg("That's not a weapon!")
            # Remember to update the path to the weapon typeclass if you move this module!
            return

        if not self.caller.db.wielded_weapon:
            self.caller.db.wielded_weapon = weapon
            self.caller.location.msg_contents("%s wields %s." % (self.caller, weapon))
        else:
            old_weapon = self.caller.db.wielded_weapon
                "%s lowers %s and wields %s." % (self.caller, old_weapon, weapon)
            )
        # Spend an action if in combat.
            self.rules.spend_action(self.caller, 1, action_name="wield")  # Use up one action.

class CmdUnwield(Command):
    """
    Stop wielding a weapon.

    Usage:
      unwield

    After using this command, you will stop wielding any
    weapon you are currently wielding and become unarmed.
    """

    key = "unwield"

        """
        """
                return
            self.caller.msg("You aren't wielding a weapon!")
        else:
            self.caller.db.wielded_weapon = None
            self.caller.location.msg_contents("%s lowers %s." % (self.caller, old_weapon))

class CmdDon(Command):
    """
    Don armor that you are carrying

    Usage:
      don <armor>

    Select armor to wear in combat. You can't use this
    command in the middle of a fight. Use the "doff"
    command to remove any armor you are wearing.
    """

    key = "don"

        """
        """
        # Can't do this in combat
            self.caller.msg("You can't don armor in a fight!")
            return
            self.caller.msg("Usage: don <obj>")
            return
        armor = self.caller.search(self.args, candidates=self.caller.contents)
        if not armor:
            return
        if not armor.is_typeclass(
            "evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor", exact=True
        ):
            self.caller.msg("That's not armor!")
            # Remember to update the path to the armor typeclass if you move this module!
            return

        if not self.caller.db.worn_armor:
            self.caller.db.worn_armor = armor
            self.caller.location.msg_contents("%s dons %s." % (self.caller, armor))
        else:
            old_armor = self.caller.db.worn_armor
                "%s removes %s and dons %s." % (self.caller, old_armor, armor)
            )

class CmdDoff(Command):
    """
    Stop wearing armor.

    Usage:
      doff

    After using this command, you will stop wearing any
    armor you are currently using and become unarmored.
    You can't use this command in combat.
    """

    key = "doff"

        """
        """
            self.caller.msg("You can't doff armor in a fight!")
            return
            self.caller.msg("You aren't wearing any armor!")
        else:
            self.caller.db.worn_armor = None
            self.caller.location.msg_contents("%s removes %s." % (self.caller, old_armor))

    """
    """

        """
        """
        self.add(CmdWield())
        self.add(CmdUnwield())
        self.add(CmdDon())
        self.add(CmdDoff())

"""
PROTOTYPES START HERE
"""

BASEWEAPON = {"typeclass": "evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon"}

BASEARMOR = {"typeclass": "evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor"}

DAGGER = {
    "prototype": "BASEWEAPON",
    "damage_range": (10, 20),
    "accuracy_bonus": 30,
    "key": "a thin steel dagger",
    "weapon_type_name": "dagger",
}

BROADSWORD = {
    "damage_range": (15, 30),
    "accuracy_bonus": 15,
    "key": "an iron broadsword",
    "weapon_type_name": "broadsword",
}

GREATSWORD = {
    "damage_range": (20, 40),
    "accuracy_bonus": 0,
    "key": "a rune-etched greatsword",
    "weapon_type_name": "greatsword",
}

LEATHERARMOR = {
    "prototype": "BASEARMOR",
    "damage_reduction": 2,
    "defense_modifier": -2,
    "key": "a suit of leather armor",
}

SCALEMAIL = {
    "damage_reduction": 4,
    "defense_modifier": -4,
    "key": "a suit of scale mail",
}

PLATEMAIL = {
    "damage_reduction": 6,
    "defense_modifier": -6,
    "key": "a suit of plate mail",
}

"""
Simple turn-based combat system with items and status effects

Contrib - Tim Ashley Jenkins 2017

This is a version of the 'turnbattle' combat system that includes
conditions and usable items, which can instill these conditions, cure
them, or do just about anything else.

Conditions are stored on characters as a dictionary, where the key
is the name of the condition and the value is a list of two items:
an integer representing the number of turns left until the condition
runs out, and the character upon whose turn the condition timer is
ticked down. Unlike most combat-related attributes, conditions aren't
wiped once combat ends - if out of combat, they tick down in real time
instead.

This module includes a number of example conditions:

    Regeneration: Character recovers HP every turn
    Poisoned: Character loses HP every turn
    Accuracy Up: +25 to character's attack rolls
    Accuracy Down: -25 to character's attack rolls
    Damage Up: +5 to character's damage
    Damage Down: -5 to character's damage
    Defense Up: +15 to character's defense
    Defense Down: -15 to character's defense
    Haste: +1 action per turn
    Paralyzed: No actions per turn
    Frightened: Character can't use the 'attack' command

Since conditions can have a wide variety of effects, their code is
scattered throughout the other functions wherever they may apply.

Items aren't given any sort of special typeclass - instead, whether or
not an object counts as an item is determined by its attributes. To make
an object into an item, it must have the attribute 'item_func', with
the value given as a callable - this is the function that will be called
when an item is used. Other properties of the item, such as how many
uses it has, whether it's destroyed when its uses are depleted, and such
can be specified on the item as well, but they are optional.

To install and test, import this module's TBItemsCharacter object into

    from evennia.contrib.game_systems.turnbattle.tb_items import TBItemsCharacter

And change your game's character typeclass to inherit from TBItemsCharacter

    class Character(TBItemsCharacter):

    from evennia.contrib.game_systems.turnbattle import tb_items

    #
    #
    self.add(tb_items.BattleCmdSet())

"""

from evennia import TICKER_HANDLER as tickerhandler
from evennia import Command, default_cmds

"""
OPTIONS
"""

NONCOMBAT_TURN_TIME = 30  # Time per turn count out of combat

# Condition options start here.
# If you need to make changes to how your conditions work later,
# it's best to put the easily tweakable values all in one place!

REGEN_RATE = (4, 8)  # Min and max HP regen for Regeneration
POISON_RATE = (4, 8)  # Min and max damage for Poisoned
ACC_UP_MOD = 25  # Accuracy Up attack roll bonus
ACC_DOWN_MOD = -25  # Accuracy Down attack roll penalty
DMG_UP_MOD = 5  # Damage Up damage roll bonus
DMG_DOWN_MOD = -5  # Damage Down damage roll penalty
DEF_UP_MOD = 15  # Defense Up defense bonus
DEF_DOWN_MOD = -15  # Defense Down defense penalty

"""
"""

class ItemCombatRules(tb_basic.BasicCombatRules):
        """

        Args:

        Returns:

        Notes:
            This is where conditions affecting attack rolls are applied, as well.
            Accuracy Up and Accuracy Down are also accounted for in itemfunc_attack(),
            so that attack items' accuracy is affected as well.
        """
        # Add to the roll if the attacker has the "Accuracy Up" condition.
        if "Accuracy Up" in attacker.db.conditions:
            attack_value += ACC_UP_MOD
        # Subtract from the roll if the attack has the "Accuracy Down" condition.
        if "Accuracy Down" in attacker.db.conditions:
            attack_value += ACC_DOWN_MOD

        """

        Args:

        Returns:

        Notes:
            This is where conditions affecting defense are accounted for.
        """
        # Add to defense if the defender has the "Defense Up" condition.
        if "Defense Up" in defender.db.conditions:
            defense_value += DEF_UP_MOD
        # Subtract from defense if the defender has the "Defense Down" condition.
        if "Defense Down" in defender.db.conditions:
            defense_value += DEF_DOWN_MOD

        """

        Args:

        Returns:

        Notes:
            This is where conditions affecting damage are accounted for. Since attack items
            roll their own damage in itemfunc_attack(), their damage is unaffected by any
            conditions.
        """
        # Add to damage roll if attacker has the "Damage Up" condition.
        if "Damage Up" in attacker.db.conditions:
            damage_value += DMG_UP_MOD
        # Subtract from the roll if the attacker has the "Damage Down" condition.
        if "Damage Down" in attacker.db.conditions:
            damage_value += DMG_DOWN_MOD

    def resolve_attack(
        self,
        attacker,
        defender,
        attack_value=None,
        defense_value=None,
        damage_value=None,
        inflict_condition=[],
    ):
        """

        Args:

        Options:
            attack_value (int): Override for attack roll
            defense_value (int): Override for defense value
            damage_value (int): Override for damage value
            inflict_condition (list): Conditions to inflict upon hit, a
                list of tuples formated as (condition(str), duration(int))

        Notes:
            This function is called by normal attacks as well as attacks
            made with items.
        """
        else:
            if not damage_value:
                damage_value = self.get_damage(attacker, defender)  # Calculate damage value.
            )
            # Inflict conditions on hit, if any specified
            for condition in inflict_condition:
                self.add_condition(defender, attacker, condition[0], condition[1])

    def spend_item_use(self, item, user):
        """
        Spends one use on an item with limited uses.

        Args:
            item (obj): Item being used
            user (obj): Character using the item

        Notes:
            If item.db.item_consumable is 'True', the item is destroyed if it
            runs out of uses - if it's a string instead of 'True', it will also
            spawn a new object as residue, using the value of item.db.item_consumable
            as the name of the prototype to spawn.
        """
        item.db.item_uses -= 1  # Spend one use

        if item.db.item_uses > 0:  # Has uses remaining
            # Inform the player
            user.msg("%s has %i uses remaining." % (item.key.capitalize(), item.db.item_uses))

        else:  # All uses spent
            if not item.db.item_consumable:  # Item isn't consumable
                # Just inform the player that the uses are gone
                user.msg("%s has no uses remaining." % item.key.capitalize())

            else:  # If item is consumable
                # If the value is 'True', just destroy the item
                if item.db.item_consumable:
                    user.msg("%s has been consumed." % item.key.capitalize())
                    item.delete()  # Delete the spent item

                else:  # If a string, use value of item_consumable to spawn an object in its place
                    residue = spawn({"prototype": item.db.item_consumable})[0]  # Spawn the residue
                    # Move the residue to the same place as the item
                    residue.location = item.location
                    user.msg("After using %s, you are left with %s." % (item, residue))

    def use_item(self, user, item, target):
        """
        Performs the action of using an item.

        Args:
            target (obj): Target of the item use
        """
        # If item is self only and no target given, set target to self.
        if item.db.item_selfonly and target is None:
            target = user

        # If item is self only, abort use if used on others.
        if item.db.item_selfonly and user != target:
            user.msg("%s can only be used on yourself." % item)
            return

        # Set kwargs to pass to item_func
        kwargs = {}
        if item.db.item_kwargs:
            kwargs = item.db.item_kwargs

        # Match item_func string to function
        try:
            item_func = ITEMFUNCS[item.db.item_func]
        except KeyError:  # If item_func string doesn't match to a function in ITEMFUNCS
            user.msg("ERROR: %s not defined in ITEMFUNCS" % item.db.item_func)
            return

        # Call the item function - abort if it returns False, indicating an error.
        # This performs the actual action of using the item.
        # Regardless of what the function returns (if anything), it's still executed.
        if not item_func(item, user, target, **kwargs):
            return

        # If we haven't returned yet, we assume the item was used successfully.
        # Spend one use if item has limited uses
        if item.db.item_uses:
            self.spend_item_use(item, user)

        # Spend an action if in combat
        if self.is_in_combat(user):
            self.spend_action(user, 1, action_name="item")

    def condition_tickdown(self, character, turnchar):
        """
        Ticks down the duration of conditions on a character at the start of a given character's
        turn.

        Args:
            character (obj): Character to tick down the conditions of
            turnchar (obj): Character whose turn it currently is

        Notes:
            In combat, this is called on every fighter at the start of every character's turn. Out
            of combat, it's instead called when a character's at_update() hook is called, which is
            every 30 seconds by default.
        """

        for key in character.db.conditions:
            # The first value is the remaining turns - the second value is whose turn to count down
            # on.
            condition_duration = character.db.conditions[key][0]
            condition_turnchar = character.db.conditions[key][1]
            # If the duration is 'True', then the condition doesn't tick down - it lasts
            # indefinitely.
            if condition_duration is not True:
                # Count down if the given turn character matches the condition's turn character.
                if condition_turnchar == turnchar:
                    character.db.conditions[key][0] -= 1
                if character.db.conditions[key][0] <= 0:
                    # If the duration is brought down to 0, remove the condition and inform
                    # everyone.
                    character.location.msg_contents(
                        "%s no longer has the '%s' condition." % (str(character), str(key))
                    )
                    del character.db.conditions[key]

    def add_condition(self, character, turnchar, condition, duration):
        """
        Adds a condition to a fighter.

        Args:
            character (obj): Character to give the condition to
            turnchar (obj): Character whose turn to tick down the condition on in combat
            condition (str): Name of the condition
            duration (int or True): Number of turns the condition lasts, or True for indefinite
        """
        # The first value is the remaining turns - the second value is whose turn to count down on.
        character.db.conditions.update({condition: [duration, turnchar]})
        # Tell everyone!
        character.location.msg_contents("%s gains the '%s' condition." % (character, condition))

    # ----------------------------------------------------------------------------
    # ITEM FUNCTIONS START HERE

    # These functions carry out the action of using an item - every item should
    # contain a db entry "item_func", with its value being a string that is
    # matched to one of these functions in the ITEMFUNCS dictionary below.

    # Every item function must take the following arguments:
    #     item (obj): The item being used
    #     user (obj): The character using the item
    #     target (obj): The target of the item use

    # Item functions must also accept **kwargs - these keyword arguments can be
    # used to define how different items that use the same function can have
    # different effects (for example, different attack items doing different
    # amounts of damage).

    # Each function below contains a description of what kwargs the function will
    # take and the effect they have on the result.

    def itemfunc_heal(self, item, user, target, **kwargs):
        """
        Item function that heals HP.

        kwargs:
            min_healing(int): Minimum amount of HP recovered
            max_healing(int): Maximum amount of HP recovered
        """
            target = user  # Target user if none specified

        if not target.attributes.has("max_hp"):  # Has no HP to speak of
            user.msg("You can't use %s on that." % item)
            return False  # Returning false aborts the item use

        if target.db.hp >= target.db.max_hp:
            user.msg("%s is already at full health." % target)

        min_healing = 20
        max_healing = 40

        # Retrieve healing range from kwargs, if present
        if "healing_range" in kwargs:
            min_healing = kwargs["healing_range"][0]
            max_healing = kwargs["healing_range"][1]

        to_heal = randint(min_healing, max_healing)  # Restore 20 to 40 hp
        if target.db.hp + to_heal > target.db.max_hp:
            to_heal = target.db.max_hp - target.db.hp  # Cap healing to max HP
        target.db.hp += to_heal

        user.location.msg_contents("%s uses %s! %s regains %i HP!" % (user, item, target, to_heal))

    def itemfunc_add_condition(self, item, user, target, **kwargs):
        """
        Item function that gives the target one or more conditions.

        kwargs:
            conditions (list): Conditions added by the item
               formatted as a list of tuples: (condition (str), duration (int or True))

        Notes:
            Should mostly be used for beneficial conditions - use itemfunc_attack
            for an item that can give an enemy a harmful condition.
        """
        conditions = [("Regeneration", 5)]

        if not target.attributes.has("max_hp"):  # Is not a fighter

        # Retrieve condition / duration from kwargs, if present
        if "conditions" in kwargs:
            conditions = kwargs["conditions"]

        user.location.msg_contents("%s uses %s!" % (user, item))

        # Add conditions to the target
        for condition in conditions:
            self.add_condition(target, user, condition[0], condition[1])

    def itemfunc_cure_condition(self, item, user, target, **kwargs):
        """
        Item function that'll remove given conditions from a target.

        kwargs:
            to_cure(list): List of conditions (str) that the item cures when used
        """
        to_cure = ["Poisoned"]

        # Retrieve condition(s) to cure from kwargs, if present
        if "to_cure" in kwargs:
            to_cure = kwargs["to_cure"]

        item_msg = "%s uses %s! " % (user, item)

        for key in target.db.conditions:
            if key in to_cure:
                # If condition specified in to_cure, remove it.
                item_msg += "%s no longer has the '%s' condition. " % (str(target), str(key))
                del target.db.conditions[key]

        user.location.msg_contents(item_msg)

    def itemfunc_attack(self, item, user, target, **kwargs):
        """
        Item function that attacks a target.

        kwargs:
            min_damage(int): Minimum damage dealt by the attack
            max_damage(int): Maximum damage dealth by the attack
            accuracy(int): Bonus / penalty to attack accuracy roll
            inflict_condition(list): List of conditions inflicted on hit,
                formatted as a (str, int) tuple containing condition name
                and duration.

        Notes:
            Calls resolve_attack at the end.
        """
        if not self.is_in_combat(user):
            user.msg("You can only use that in combat.")

            user.msg("You have to specify a target to use %s! (use <item> = <target>)" % item)

        if target == user:
            user.msg("You can't attack yourself!")

        if not target.db.hp:  # Has no HP

        min_damage = 20
        max_damage = 40
        accuracy = 0
        inflict_condition = []

        # Retrieve values from kwargs, if present
        if "damage_range" in kwargs:
            min_damage = kwargs["damage_range"][0]
            max_damage = kwargs["damage_range"][1]
        if "accuracy" in kwargs:
            accuracy = kwargs["accuracy"]
        if "inflict_condition" in kwargs:
            inflict_condition = kwargs["inflict_condition"]

        # Roll attack and damage
        attack_value = randint(1, 100) + accuracy
        damage_value = randint(min_damage, max_damage)

        # Account for "Accuracy Up" and "Accuracy Down" conditions
        if "Accuracy Up" in user.db.conditions:
            attack_value += 25
        if "Accuracy Down" in user.db.conditions:
            attack_value -= 25

        user.location.msg_contents("%s attacks %s with %s!" % (user, target, item))
        self.resolve_attack(
            user,
            target,
            attack_value=attack_value,
            damage_value=damage_value,
            inflict_condition=inflict_condition,
        )

COMBAT_RULES = ItemCombatRules()

# Match strings to item functions here. We can't store callables on
# prototypes, so we store a string instead, matching that string to
# a callable in this dictionary.
ITEMFUNCS = {
    "heal": COMBAT_RULES.itemfunc_heal,
    "attack": COMBAT_RULES.itemfunc_attack,
    "add_condition": COMBAT_RULES.itemfunc_add_condition,
    "cure_condition": COMBAT_RULES.itemfunc_cure_condition,
}

"""

You can paste these prototypes into your game's prototypes.py module in your
/world/ folder, and use the spawner to create them - they serve as examples
of items you can make and a handy way to demonstrate the system for
conditions as well.

Items don't have any particular typeclass - any object with a db entry
"item_func" that references one of the functions given above can be used as
an item with the 'use' command.

Only "item_func" is required, but item behavior can be further modified by
specifying any of the following:

    item_uses (int): If defined, item has a limited number of uses

    item_selfonly (bool): If True, user can only use the item on themself

    item_consumable(True or str): If True, item is destroyed when it runs
        out of uses. If a string is given, the item will spawn a new
        object as it's destroyed, with the string specifying what prototype
        to spawn.

    item_kwargs (dict): Keyword arguments to pass to the function defined in
        item_func. Unique to each function, and can be used to make multiple
        items using the same function work differently.
"""

MEDKIT = {
    "key": "a medical kit",
    "aliases": ["medkit"],
    "desc": "A standard medical kit. It can be used a few times to heal wounds.",
    "item_func": "heal",
    "item_uses": 3,
    "item_consumable": True,
    "item_kwargs": {"healing_range": (15, 25)},
}

GLASS_BOTTLE = {"key": "a glass bottle", "desc": "An empty glass bottle."}

HEALTH_POTION = {
    "key": "a health potion",
    "desc": "A glass bottle full of a mystical potion that heals wounds when used.",
    "item_uses": 1,
    "item_consumable": "GLASS_BOTTLE",
    "item_kwargs": {"healing_range": (35, 50)},
}

REGEN_POTION = {
    "key": "a regeneration potion",
    "desc": "A glass bottle full of a mystical potion that regenerates wounds over time.",
    "item_func": "add_condition",
    "item_kwargs": {"conditions": [("Regeneration", 10)]},
}

HASTE_POTION = {
    "key": "a haste potion",
    "desc": "A glass bottle full of a mystical potion that hastens its user.",
    "item_kwargs": {"conditions": [("Haste", 10)]},
}

BOMB = {
    "key": "a rotund bomb",
    "desc": "A large black sphere with a fuse at the end. Can be used on enemies in combat.",
    "item_func": "attack",
    "item_kwargs": {"damage_range": (25, 40), "accuracy": 25},
}

POISON_DART = {
    "key": "a poison dart",
    "desc": "A thin dart coated in deadly poison. Can be used on enemies in combat",
    "item_kwargs": {
        "damage_range": (5, 10),
        "accuracy": 25,
        "inflict_condition": [("Poisoned", 10)],
    },
}

TASER = {
    "key": "a taser",
    "desc": "A device that can be used to paralyze enemies in combat.",
        "damage_range": (10, 20),
        "accuracy": 0,
        "inflict_condition": [("Paralyzed", 1)],
    },
}

GHOST_GUN = {
    "key": "a ghost gun",
    "desc": "A gun that fires scary ghosts at people. Anyone hit by a ghost becomes frightened.",
    "item_uses": 6,
        "accuracy": 15,
        "inflict_condition": [("Frightened", 1)],
    },
}

ANTIDOTE_POTION = {
    "key": "an antidote potion",
    "desc": "A glass bottle full of a mystical potion that cures poison when used.",
    "item_func": "cure_condition",
    "item_kwargs": {"to_cure": ["Poisoned"]},
}

AMULET_OF_MIGHT = {
    "key": "The Amulet of Might",
    "desc": "The one who holds this amulet can call upon its power to gain great strength.",
    "item_selfonly": True,
    "item_kwargs": {"conditions": [("Damage Up", 3), ("Accuracy Up", 3), ("Defense Up", 3)]},
}

AMULET_OF_WEAKNESS = {
    "key": "The Amulet of Weakness",
    "desc": "The one who holds this amulet can call upon its power to gain great weakness. "
    "It's not a terribly useful artifact.",
    "item_kwargs": {"conditions": [("Damage Down", 3), ("Accuracy Down", 3), ("Defense Down", 3)]},
}

"""
"""

class TBItemsCharacter(tb_basic.TBBasicCharacter):
    """
    """

    rules = ItemCombatRules()

        """
        """
        self.db.conditions = {}  # Set empty dict for conditions
        # Subscribe character to the ticker handler
        tickerhandler.add(NONCOMBAT_TURN_TIME, self.at_update, idstring="update")
        """

        An empty dictionary is created to store conditions later,
        and the character is subscribed to the Ticker Handler, which
        will call at_update() on the character, with the interval
        specified by NONCOMBAT_TURN_TIME above. This is used to tick
        down conditions out of combat.

        """

    def at_turn_start(self):
        """
        Hook called at the beginning of this character's turn in combat.
        """
        self.msg("|wIt's your turn! You have %i HP remaining.|n" % self.db.hp)

        # Apply conditions that fire at the start of each turn.
        self.apply_turn_conditions()

    def apply_turn_conditions(self):
        """
        Applies the effect of conditions that occur at the start of each
        turn in combat, or every 30 seconds out of combat.
        """
        # Regeneration: restores 4 to 8 HP at the start of character's turn
        if "Regeneration" in self.db.conditions:
            to_heal = randint(REGEN_RATE[0], REGEN_RATE[1])  # Restore HP
            if self.db.hp + to_heal > self.db.max_hp:
                to_heal = self.db.max_hp - self.db.hp  # Cap healing to max HP
            self.db.hp += to_heal
            self.location.msg_contents("%s regains %i HP from Regeneration." % (self, to_heal))

        # Poisoned: does 4 to 8 damage at the start of character's turn
        if "Poisoned" in self.db.conditions:
            to_hurt = randint(POISON_RATE[0], POISON_RATE[1])  # Deal damage
            self.rules.apply_damage(self, to_hurt)
            self.location.msg_contents("%s takes %i damage from being Poisoned." % (self, to_hurt))
            if self.db.hp <= 0:
                # Call at_defeat if poison defeats the character
                self.rules.at_defeat(self)

        # Haste: Gain an extra action in combat.
        if self.rules.is_in_combat(self) and "Haste" in self.db.conditions:
            self.db.combat_actionsleft += 1
            self.msg("You gain an extra action this turn from Haste!")

        # Paralyzed: Have no actions in combat.
        if self.rules.is_in_combat(self) and "Paralyzed" in self.db.conditions:
            self.db.combat_actionsleft = 0
            self.location.msg_contents("%s is Paralyzed, and can't act this turn!" % self)
            self.db.combat_turnhandler.turn_end_check(self)

    def at_update(self):
        """
        Fires every 30 seconds.
        """
        if not self.rules.is_in_combat(self):  # Not in combat
            # Change all conditions to update on character's turn.
            for key in self.db.conditions:
                self.db.conditions[key][1] = self
            # Apply conditions that fire every turn
            self.apply_turn_conditions()
            # Tick down condition durations
            self.rules.condition_tickdown(self, self)

class TBItemsCharacterTest(TBItemsCharacter):
    """
    Just like the TBItemsCharacter, but doesn't subscribe to the TickerHandler.
    This makes it easier to run unit tests on.
    """

"""
"""

class TBItemsTurnHandler(tb_basic.TBBasicTurnHandler):
    """

    """

        """
        """
        super().next_turn()

        # Count down condition timers.
        next_fighter = self.db.fighters[self.db.turn]
            self.rules.condition_tickdown(fighter, next_fighter)

"""
"""

    """

    Usage:
      fight

    """

    combat_handler_class = TBItemsTurnHandler

    """

    Usage:

    """

    """

    Usage:
      pass

    """

    """

    Usage:
      disengage

    """

    """

    Usage:
      rest

    """

    """

    Usage:
      help list
      help all

    """

        "|wUse:|n Use an item you're carrying."
    )

class CmdUse(MuxCommand):
    """
    Use an item.

    Usage:
      use <item> [= target]

    An item can have various function - looking at the item may
    provide information as to its effects. Some items can be used
    to attack others, and as such can only be used in combat.
    """

    key = "use"

        """
        """
        # Search for item
        item = self.caller.search(self.lhs, candidates=self.caller.contents)
        if not item:
            return

        # Search for target, if any is given
            target = self.caller.search(self.rhs)
                return

        # If in combat, can only use items on your turn
                self.caller.msg("You can only use items on your turn.")
                return

        if not item.db.item_func:  # Object has no item_func, not usable
            self.caller.msg("'%s' is not a usable item." % item.key.capitalize())
            return

        if item.attributes.has("item_uses"):  # Item has limited uses
            if item.db.item_uses <= 0:  # Limited uses are spent
                self.caller.msg("'%s' has no uses remaining." % item.key.capitalize())
                return

        # If everything checks out, call the use_item function
        self.rules.use_item(self.caller, item, target)

    """
    """

        """
        """
        self.add(CmdUse())

"""
Simple turn-based combat system with spell casting

Contrib - Tim Ashley Jenkins 2017, Refactor by Griatch, 2022

This is a version of the 'turnbattle' contrib that includes a basic,
expandable framework for a 'magic system', whereby players can spend
a limited resource (MP) to achieve a wide variety of effects, both in
and out of combat. This does not have to strictly be a system for
magic - it can easily be re-flavored to any other sort of resource
based mechanic, like psionic powers, special moves and stamina, and
so forth.

In this system, spells are learned by name with the 'learnspell'
command, and then used with the 'cast' command. Spells can be cast in or
out of combat - some spells can only be cast in combat, some can only be
cast outside of combat, and some can be cast any time. However, if you
are in combat, you can only cast a spell on your turn, and doing so will
typically use an action (as specified in the spell's funciton).

Spells are defined at the end of the module in a database that's a
dictionary of dictionaries - each spell is matched by name to a function,
along with various parameters that restrict when the spell can be used and
what the spell can be cast on. Included is a small variety of spells that
damage opponents and heal HP, as well as one that creates an object.

Because a spell can call any function, a spell can be made to do just
about anything at all. The SPELLS dictionary at the bottom of the module
even allows kwargs to be passed to the spell function, so that the same
function can be re-used for multiple similar spells.

Spells in this system work on a very basic resource: MP, which is spent
when casting spells and restored by resting. It shouldn't be too difficult
to modify this system to use spell slots, some physical fuel or resource,
or whatever else your game requires.

To install and test, import this module's TBMagicCharacter object into

    from evennia.contrib.game_systems.turnbattle.tb_magic import TBMagicCharacter

And change your game's character typeclass to inherit from TBMagicCharacter

    class Character(TBMagicCharacter):

Note: If your character already existed you need to also make sure
to re-run the creation hooks on it to set the needed Attributes.
Use `update self` to try on yourself or use py to call `at_object_creation()`
on all existing Characters.

    from evennia.contrib.game_systems.turnbattle import tb_magic

    #
    #
    self.add(tb_magic.BattleCmdSet())

"""

from evennia import Command, DefaultScript, create_object, default_cmds

"""
SPELL FUNCTIONS START HERE

These are the functions that are called by the 'cast' command to perform the
effects of various spells. Which spells execute which functions and what
parameters are passed to them are specified at the bottom of the module, in
the 'SPELLS' dictionary.

All of these functions take the same arguments:
    caster (obj): Character casting the spell
    spell_name (str): Name of the spell being cast
    targets (list): List of objects targeted by the spell
    cost (int): MP cost of casting the spell

These functions also all accept **kwargs, and how these are used is specified
in the docstring for each function.
"""

class MagicCombatRules(tb_basic.BasicCombatRules):
    def spell_healing(self, caster, spell_name, targets, cost, **kwargs):
        """
        Spell that restores HP to a target or targets.

        kwargs:
            healing_range (tuple): Minimum and maximum amount healed to
                each target. (20, 40) by default.
        """
        spell_msg = "%s casts %s!" % (caster, spell_name)

        for character in targets:
            to_heal = randint(min_healing, max_healing)  # Restore 20 to 40 hp
            if character.db.hp + to_heal > character.db.max_hp:
                to_heal = character.db.max_hp - character.db.hp  # Cap healing to max HP
            character.db.hp += to_heal
            spell_msg += " %s regains %i HP!" % (character, to_heal)

        caster.db.mp -= cost  # Deduct MP cost

        caster.location.msg_contents(spell_msg)  # Message the room with spell results

        if self.is_in_combat(caster):  # Spend action if in combat
            self.spend_action(caster, 1, action_name="cast")

    def spell_attack(self, caster, spell_name, targets, cost, **kwargs):
        """
        Spell that deals damage in combat. Similar to resolve_attack.

        kwargs:
            attack_name (tuple): Single and plural describing the sort of
                attack or projectile that strikes each enemy.
            damage_range (tuple): Minimum and maximum damage dealt by the
                spell. (10, 20) by default.
            accuracy (int): Modifier to the spell's attack roll, determining
                an increased or decreased chance to hit. 0 by default.
            attack_count (int): How many individual attacks are made as part
                of the spell. If the number of attacks exceeds the number of
                targets, the first target specified will be attacked more
                than once. Just 1 by default - if the attack_count is less
                than the number targets given, each target will only be
                attacked once.
        """

        atkname_single = "The spell"
        atkname_plural = "spells"
        min_damage = 10
        max_damage = 20
        attack_count = 1

        # Retrieve some variables from kwargs, if present
        if "attack_name" in kwargs:
            atkname_single = kwargs["attack_name"][0]
            atkname_plural = kwargs["attack_name"][1]
        if "attack_count" in kwargs:
            attack_count = kwargs["attack_count"]

        to_attack = []
        # If there are more attacks than targets given, attack first target multiple times
        if len(targets) < attack_count:
            to_attack = to_attack + targets
            extra_attacks = attack_count - len(targets)
            for n in range(extra_attacks):
                to_attack.insert(0, targets[0])
        else:

        # Set up dictionaries to track number of hits and total damage
        total_hits = {}
        total_damage = {}
        for fighter in targets:
            total_hits.update({fighter: 0})
            total_damage.update({fighter: 0})

        # Resolve attack for each target
        for fighter in to_attack:
            attack_value = randint(1, 100) + accuracy  # Spell attack roll
            defense_value = self.get_defense(caster, fighter)
            if attack_value >= defense_value:
                spell_dmg = randint(min_damage, max_damage)  # Get spell damage
                total_hits[fighter] += 1
                total_damage[fighter] += spell_dmg

            # Construct combat message
            if total_hits[fighter] == 0:
                spell_msg += " The spell misses %s!" % fighter
            elif total_hits[fighter] > 0:
                attack_count_str = atkname_single + " hits"
                if total_hits[fighter] > 1:
                    attack_count_str = "%i %s hit" % (total_hits[fighter], atkname_plural)
                spell_msg += " %s %s for %i damage!" % (
                    attack_count_str,
                    fighter,
                    total_damage[fighter],
                )

            # Apply damage
            self.apply_damage(fighter, total_damage[fighter])
            # If fighter HP is reduced to 0 or less, call at_defeat.
            if fighter.db.hp <= 0:
                self.at_defeat(fighter)

    def spell_conjure(self, caster, spell_name, targets, cost, **kwargs):
        """
        Spell that creates an object.

        kwargs:
            obj_key (str): Key of the created object.
            obj_desc (str): Desc of the created object.
            obj_typeclass (str): Typeclass path of the object.

        If you want to make more use of this particular spell funciton,
        you may want to modify it to use the spawner (in evennia.utils.spawner)
        instead of creating objects directly.
        """

        obj_key = "a nondescript object"
        obj_desc = "A perfectly generic object."
        obj_typeclass = "evennia.objects.objects.DefaultObject"

        if "obj_key" in kwargs:
            obj_key = kwargs["obj_key"]
        if "obj_desc" in kwargs:
            obj_desc = kwargs["obj_desc"]
        if "obj_typeclass" in kwargs:
            obj_typeclass = kwargs["obj_typeclass"]

        conjured_obj = create_object(
            obj_typeclass, key=obj_key, location=caster.location
        )  # Create object
        conjured_obj.db.desc = obj_desc  # Add object desc

        # Message the room to announce the creation of the object
        caster.location.msg_contents(
            "%s casts %s, and %s appears!" % (caster, spell_name, conjured_obj)
        )

COMBAT_RULES = MagicCombatRules()

"""
SPELL DEFINITIONS START HERE
In this section, each spell is matched to a function, and given parameters
that determine its MP cost, valid type and number of targets, and what
function casting the spell executes.

This data is given as a dictionary of dictionaries - the key of each entry
is the spell's name, and the value is a dictionary of various options and
parameters, some of which are required and others which are optional.

Required values for spells:

    target (str): Valid targets for the spell. Can be any of:
        "none" - No target needed
        "self" - Self only
        "any" - Any object
        "anyobj" - Any object that isn't a character
        "anychar" - Any character
        "other" - Any object excluding the caster
        "otherchar" - Any character excluding the caster
    spellfunc (callable): Function that performs the action of the spell.
        Must take the following arguments: caster (obj), spell_name (str),
        targets (list), and cost (int), as well as **kwargs.

Optional values for spells:

    combat_spell (bool): If the spell can be cast in combat. True by default.
    noncombat_spell (bool): If the spell can be cast out of combat. True by default.
    max_targets (int): Maximum number of objects that can be targeted by the spell.
        1 by default - unused if target is "none" or "self"

Any other values specified besides the above will be passed as kwargs to 'spellfunc'.
You can use kwargs to effectively re-use the same function for different but similar
spells - for example, 'magic missile' and 'flame shot' use the same function, but
behave differently, as they have different damage ranges, accuracy, amount of attacks
made as part of the spell, and so forth. If you make your spell functions flexible
enough, you can make a wide variety of spells just by adding more entries to this
dictionary.
"""

SPELLS = {
    "magic missile": {
        "spellfunc": COMBAT_RULES.spell_attack,
        "target": "otherchar",
        "cost": 3,
        "noncombat_spell": False,
        "max_targets": 3,
        "attack_name": ("A bolt", "bolts"),
        "damage_range": (4, 7),
        "accuracy": 999,
        "attack_count": 3,
    },
    "flame shot": {
        "cost": 3,
        "attack_name": ("A jet of flame", "jets of flame"),
        "damage_range": (25, 35),
    },
    "cure wounds": {"spellfunc": COMBAT_RULES.spell_healing, "target": "anychar", "cost": 5},
    "mass cure wounds": {
        "spellfunc": COMBAT_RULES.spell_healing,
        "target": "anychar",
        "cost": 10,
        "max_targets": 5,
    },
    "full heal": {
        "cost": 12,
        "healing_range": (100, 100),
    },
    "cactus conjuration": {
        "spellfunc": COMBAT_RULES.spell_conjure,
        "target": "none",
        "cost": 2,
        "combat_spell": False,
        "obj_key": "a cactus",
        "obj_desc": "An ordinary green cactus with little spines.",
    },
}

"""
OPTIONS
"""

"""
"""

class TBMagicCharacter(tb_basic.TBBasicCharacter):
    """
    and maximum HP, access to combat commands and magic.

    """

        """

        """
        self.db.spells_known = []  # Set empty spells known list
        self.db.max_mp = 20  # Set maximum MP to 20
        self.db.mp = self.db.max_mp  # Set current MP to maximum

"""
"""

class TBMagicTurnHandler(tb_basic.TBBasicTurnHandler):
    """

    """

"""
"""

    """

    Usage:
      fight

    """

    combat_handler_class = TBMagicTurnHandler

    """

    Usage:

    """

    """

    Usage:
      pass

    """

    """

    Usage:
      disengage

    """

class CmdLearnSpell(Command):
    """
    Learn a magic spell.

    Usage:
        learnspell <spell name>

    Adds a spell by name to your list of spells known.

    The following spells are provided as examples:

        |wmagic missile|n (3 MP): Fires three missiles that never miss. Can target
            up to three different enemies.

        |wflame shot|n (3 MP): Shoots a high-damage jet of flame at one target.

        |wcure wounds|n (5 MP): Heals damage on one target.

        |wmass cure wounds|n (10 MP): Like 'cure wounds', but can heal up to 5
            targets at once.

        |wfull heal|n (12 MP): Heals one target back to full HP.

        |wcactus conjuration|n (2 MP): Creates a cactus.
    """

    key = "learnspell"
    help_category = "magic"

        """
        """
        spell_list = sorted(SPELLS.keys())
        args = self.args.lower()
        args = args.strip(" ")
        spell_to_learn = []

        if not args or len(args) < 3:  # No spell given
            caller.msg("Usage: learnspell <spell name>")
            return

        for spell in spell_list:  # Match inputs to spells
            if args in spell.lower():
                spell_to_learn.append(spell)

        if spell_to_learn == []:  # No spells matched
            caller.msg("There is no spell with that name.")
            return
        if len(spell_to_learn) > 1:  # More than one match
            matched_spells = ", ".join(spell_to_learn)
            caller.msg("Which spell do you mean: %s?" % matched_spells)
            return

        if len(spell_to_learn) == 1:  # If one match, extract the string
            spell_to_learn = spell_to_learn[0]

        if spell_to_learn not in self.caller.db.spells_known:  # If the spell isn't known...
            caller.db.spells_known.append(spell_to_learn)  # ...then add the spell to the character
            caller.msg("You learn the spell '%s'!" % spell_to_learn)
            return
        if spell_to_learn in self.caller.db.spells_known:  # Already has the spell specified
            caller.msg("You already know the spell '%s'!" % spell_to_learn)
        """
        You will almost definitely want to replace this with your own system
        for learning spells, perhaps tied to character advancement or finding
        items in the game world that spells can be learned from.
        """

class CmdCast(MuxCommand):
    """
    Cast a magic spell that you know, provided you have the MP
    to spend on its casting.

    Usage:
        cast <spellname> [= <target1>, <target2>, etc...]

    Some spells can be cast on multiple targets, some can be cast
    on only yourself, and some don't need a target specified at all.
    Typing 'cast' by itself will give you a list of spells you know.
    """

        """

        Note: This is a quite long command, since it has to cope with all
        the different circumstances in which you may or may not be able
        to cast a spell. None of the spell's effects are handled by the
        command - all the command does is verify that the player's input
        is valid for the spell being cast and then call the spell's
        function.
        """

        if not self.lhs or len(self.lhs) < 3:  # No spell name given
            caller.msg("Usage: cast <spell name> = <target>, <target2>, ...")
            if not caller.db.spells_known:
                caller.msg("You don't know any spells.")
                return
            else:
                caller.db.spells_known = sorted(caller.db.spells_known)
                spells_known_msg = "You know the following spells:|/" + "|/".join(
                    caller.db.spells_known
                )
                caller.msg(spells_known_msg)  # List the spells the player knows
                return

        spellname = self.lhs.lower()  # noqa  - not used but potentially useful
        spell_to_cast = []
        spell_targets = []

            spell_targets = []
        elif self.rhs.lower() in ["me", "self", "myself"]:
            spell_targets = [caller]
        elif len(self.rhs) > 2:
            spell_targets = self.rhslist

        for spell in caller.db.spells_known:  # Match inputs to spells
            if self.lhs in spell.lower():
                spell_to_cast.append(spell)

        if spell_to_cast == []:  # No spells matched
            caller.msg("You don't know a spell of that name.")
            return
        if len(spell_to_cast) > 1:  # More than one match
            matched_spells = ", ".join(spell_to_cast)
            return

        if len(spell_to_cast) == 1:  # If one match, extract the string
            spell_to_cast = spell_to_cast[0]

        if spell_to_cast not in SPELLS:  # Spell isn't defined
            caller.msg("ERROR: Spell %s is undefined" % spell_to_cast)
            return

        # Time to extract some info from the chosen spell!
        spelldata = SPELLS[spell_to_cast]

        # Add in some default data if optional parameters aren't specified
        if "combat_spell" not in spelldata:
            spelldata.update({"combat_spell": True})
        if "noncombat_spell" not in spelldata:
            spelldata.update({"noncombat_spell": True})
        if "max_targets" not in spelldata:
            spelldata.update({"max_targets": 1})

        # Store any superfluous options as kwargs to pass to the spell function
        kwargs = {}
        spelldata_opts = [
            "spellfunc",
            "target",
            "cost",
            "combat_spell",
            "noncombat_spell",
            "max_targets",
        ]
        for key in spelldata:
            if key not in spelldata_opts:
                kwargs.update({key: spelldata[key]})

        # If caster doesn't have enough MP to cover the spell's cost, give error and return
        if spelldata["cost"] > caller.db.mp:
            caller.msg("You don't have enough MP to cast '%s'." % spell_to_cast)
            return

        # If in combat and the spell isn't a combat spell, give error message and return
        if spelldata["combat_spell"] is False and self.rules.is_in_combat(caller):
            caller.msg("You can't use the spell '%s' in combat." % spell_to_cast)
            return

        # If not in combat and the spell isn't a non-combat spell, error ms and return.
        if spelldata["noncombat_spell"] is False and self.rules.is_in_combat(caller) is False:
            caller.msg("You can't use the spell '%s' outside of combat." % spell_to_cast)
            return

        # If spell takes no targets and one is given, give error message and return
        if len(spell_targets) > 0 and spelldata["target"] == "none":
            caller.msg("The spell '%s' isn't cast on a target." % spell_to_cast)
            return

        # If no target is given and spell requires a target, give error message
        if spelldata["target"] not in ["self", "none"]:
            if len(spell_targets) == 0:
                caller.msg("The spell '%s' requires a target." % spell_to_cast)
                return

        # If more targets given than maximum, give error message
        if len(spell_targets) > spelldata["max_targets"]:
            targplural = "target"
            if spelldata["max_targets"] > 1:
                targplural = "targets"
                "The spell '%s' can only be cast on %i %s."
                % (spell_to_cast, spelldata["max_targets"], targplural)
            )
            return

        # Set up our candidates for targets
        target_candidates = []

        # If spell targets 'any' or 'other', any object in caster's inventory or location
        # can be targeted by the spell.
        if spelldata["target"] in ["any", "other"]:
            target_candidates = caller.location.contents + caller.contents

        # If spell targets 'anyobj', only non-character objects can be targeted.
        if spelldata["target"] == "anyobj":
            prefilter_candidates = caller.location.contents + caller.contents
            for thing in prefilter_candidates:
                if not thing.attributes.has("max_hp"):  # Has no max HP, isn't a fighter
                    target_candidates.append(thing)

        # If spell targets 'anychar' or 'otherchar', only characters can be targeted.
        if spelldata["target"] in ["anychar", "otherchar"]:
            prefilter_candidates = caller.location.contents
                if thing.attributes.has("max_hp"):  # Has max HP, is a fighter

        # Now, match each entry in spell_targets to an object in the search candidates
        matched_targets = []
        for target in spell_targets:
            match = caller.search(target, candidates=target_candidates)
            matched_targets.append(match)
        spell_targets = matched_targets

        # If no target is given and the spell's target is 'self', set target to self
        if len(spell_targets) == 0 and spelldata["target"] == "self":

        # Give error message if trying to cast an "other" target spell on yourself
        if spelldata["target"] in ["other", "otherchar"]:
            if caller in spell_targets:
                caller.msg("You can't cast '%s' on yourself." % spell_to_cast)
                return

        # Return if "None" in target list, indicating failed match
        if None in spell_targets:
            # No need to give an error message, as 'search' gives one by default.
            return

        # Give error message if repeats in target list
        if len(spell_targets) != len(set(spell_targets)):
            caller.msg("You can't specify the same target more than once!")
            return

        # Finally, we can cast the spell itself. Note that MP is not deducted here!
        try:
            spelldata["spellfunc"](
                caller, spell_to_cast, spell_targets, spelldata["cost"], **kwargs
            )
            log_trace("Error in callback for spell: %s." % spell_to_cast)

    """
    Recovers damage and restores MP.

    Usage:
      rest

    Resting recovers your HP and MP to their maximum, but you can
    only rest if you're not in a fight.
    """

            return

        self.caller.db.mp = self.caller.db.max_mp  # Set current MP to maximum
        self.caller.location.msg_contents("%s rests to recover HP and MP." % self.caller)
        # You'll probably want to replace this with your own system for recovering HP and MP.

class CmdStatus(Command):
    """
    Gives combat information.

    Usage:
      status

    Shows your current and maximum HP and your distance from
    other targets in combat.
    """

        char = self.caller

        if not char.db.max_hp:  # Character not initialized, IE in unit tests
            char.db.hp = 100
            char.db.max_hp = 100
            char.db.spells_known = []
            char.db.max_mp = 20
            char.db.mp = char.db.max_mp

        char.msg(
            "You have %i / %i HP and %i / %i MP."
            % (char.db.hp, char.db.max_hp, char.db.mp, char.db.max_mp)
        )

    """

    Usage:
      help list
      help all

    """

    """
    """

        """
        """
        self.add(CmdLearnSpell())
        self.add(CmdCast())

"""
Simple turn-based combat system with range and movement

This is a version of the 'turnbattle' contrib that includes a system
for abstract movement and positioning in combat, including distinction
between melee and ranged attacks. In this system, a fighter or object's
exact position is not recorded - only their relative distance to other
actors in combat.

In this example, the distance between two objects in combat is expressed
as an integer value: 0 for "engaged" objects that are right next to each
other, 1 for "reach" which is for objects that are near each other but
not directly adjacent, and 2 for "range" for objects that are far apart.

When combat starts, all fighters are at reach with each other and other
objects, and at range from any exits. On a fighter's turn, they can use
the "approach" command to move closer to an object, or the "withdraw"
command to move further away from an object, either of which takes an
action in combat. In this example, fighters are given two actions per
turn, allowing them to move and attack in the same round, or to attack
twice or move twice.

When you move toward an object, you will also move toward anything else
that's close to your target - the same goes for moving away from a target,
which will also move you away from anything close to your target. Moving
toward one target may also move you away from anything you're already
close to, but withdrawing from a target will never inadvertently bring
you closer to anything else.

In this example, there are two attack commands. 'Attack' can only hit
targets that are 'engaged' (range 0) with you. 'Shoot' can hit any target
on the field, but cannot be used if you are engaged with any other fighters.
In addition, strikes made with the 'attack' command are more accurate than
'shoot' attacks. This is only to provide an example of how melee and ranged
attacks can be made to work differently - you can, of course, modify this
to fit your rules system.

When in combat, the ranges of objects are also accounted for - you can't
pick up an object unless you're engaged with it, and can't give an object
to another fighter without being engaged with them either. Dropped objects
are automatically assigned a range of 'engaged' with the fighter who dropped
them. Additionally, giving or getting an object will take an action in combat.
Dropping an object does not take an action, but can only be done on your turn.

When combat ends, all range values are erased and all restrictions on getting
or getting objects are lifted - distances are no longer tracked and objects in
the same room can be considered to be in the same space, as is the default
behavior of Evennia and most MUDs.

This system allows for strategies in combat involving movement and
positioning to be implemented in your battle system without the use of
a 'grid' of coordinates, which can be difficult and clunky to navigate
in text and disadvantageous to players who use screen readers. This loose,
narrative method of tracking position is based around how the matter is
handled in tabletop RPGs played without a grid - typically, a character's
exact position in a room isn't important, only their relative distance to
other actors.

You may wish to expand this system with a method of distinguishing allies
from enemies (to prevent allied characters from blocking your ranged attacks)
as well as some method by which melee-focused characters can prevent enemies
from withdrawing or punish them from doing so, such as by granting "attacks of
opportunity" or something similar. If you wish, you can also expand the breadth
of values allowed for range - rather than just 0, 1, and 2, you can allow ranges
to go up to much higher values, and give attacks and movements more varying
values for distance for a more granular system. You may also want to implement
a system for fleeing or changing rooms in combat by approaching exits, which
are objects placed in the range field like any other.

To install and test, import this module's TBRangeCharacter object into

    from evennia.contrib.game_systems.turnbattle.tb_range import TBRangeCharacter

And change your game's character typeclass to inherit from TBRangeCharacter

    class Character(TBRangeCharacter):

Do the same thing in your game's objects.py module for TBRangeObject:

    from evennia.contrib.game_systems.turnbattle.tb_range import TBRangeObject
    class Object(TBRangeObject):

    from evennia.contrib.game_systems.turnbattle import tb_range

    #
    #
    self.add(tb_range.BattleCmdSet())

"""

from evennia import Command, DefaultObject, DefaultScript, default_cmds

"""
OPTIONS
"""

ACTIONS_PER_TURN = 2  # Number of actions allowed per turn

"""
"""

class RangedCombatRules(tb_basic.BasicCombatRules):
    def get_attack(self, attacker, defender, attack_type):
        """

        Args:
            attack_type (str): Type of attack ('melee' or 'ranged')

        Returns:

        Notes:
            By default, generates a random integer from 1 to 100 without using any
            properties from either the attacker or defender, and modifies the result
            based on whether it's for a melee or ranged attack.

        """
        # Make melee attacks more accurate, ranged attacks less accurate
        if attack_type == "melee":
            attack_value += 15
        if attack_type == "ranged":
            attack_value -= 15

    def get_defense(self, attacker, defender, attack_type="melee"):
        """

        Args:

        Returns:

        Notes:

        """

    def get_range(self, obj1, obj2):
        """
        Gets the combat range between two objects.

        Args:
            obj1 (obj): First object
            obj2 (obj): Second object

        Returns:
            range (int or None): Distance between two objects or None if not applicable
        """
        # Return None if not applicable.
        if not obj1.db.combat_range:
        if not obj2.db.combat_range:
        if obj1 not in obj2.db.combat_range:
        if obj2 not in obj1.db.combat_range:
        # Return the range between the two objects.
        return obj1.db.combat_range[obj2]

    def distance_inc(self, mover, target):
        """
        Function that increases distance in range field between mover and target.

        Args:
            mover (obj): The object moving
            target (obj): The object to be moved away from
        """
        mover.db.combat_range[target] += 1
        target.db.combat_range[mover] = mover.db.combat_range[target]
        # Set a cap of 2:
        if self.get_range(mover, target) > 2:
            target.db.combat_range[mover] = 2
            mover.db.combat_range[target] = 2

    def distance_dec(self, mover, target):
        """
        Helper function that decreases distance in range field between mover and target.

        Args:
            target (obj): The object to be moved toward
        """
        mover.db.combat_range[target] -= 1
        # If this brings mover to range 0 (Engaged):
        if self.get_range(mover, target) <= 0:
            # Reset range to each other to 0 and copy target's ranges to mover.
            target.db.combat_range[mover] = 0
            mover.db.combat_range = target.db.combat_range
            # Assure everything else has the same distance from the mover and target, now that
            # they're together
            for thing in mover.location.contents:
                if thing != mover and thing != target:
                    thing.db.combat_range[mover] = thing.db.combat_range[target]

    def approach(self, mover, target):
        """
        Manages a character's whole approach, including changes in ranges to other characters.

        Args:

        Notes:
            The mover will also automatically move toward any objects that are closer to the
            target than the mover is. The mover will also move away from anything they started
            out close to.
        """

        contents = mover.location.contents

        for thing in contents:
            if thing != mover and thing != target:
                # Move closer to each object closer to the target than you.
                if self.get_range(mover, thing) > self.get_range(target, thing):
                    self.distance_dec(mover, thing)
                # Move further from each object that's further from you than from the target.
                if self.get_range(mover, thing) < self.get_range(target, thing):
                    self.distance_inc(mover, thing)
        # Lastly, move closer to your target.
        self.distance_dec(mover, target)

    def withdraw(self, mover, target):
        """
        Manages a character's whole withdrawal, including changes in ranges to other characters.

        Args:

        Notes:
            The mover will also automatically move away from objects that are close to the target
            of their withdrawl. The mover will never inadvertently move toward anything else while
            withdrawing - they can be considered to be moving to open space.
        """

                # Move away from each object closer to the target than you, if it's also closer to
                # you than you are to the target.
                if self.get_range(mover, thing) >= self.get_range(target, thing) and self.get_range(
                    mover, thing
                ) < self.get_range(mover, target):
                # Move away from anything your target is engaged with
                if self.get_range(target, thing) == 0:
                # Move away from anything you're engaged with.
                if self.get_range(mover, thing) == 0:
        # Then, move away from your target.
        self.distance_inc(mover, target)

        self, attacker, defender, attack_value=None, defense_value=None, attack_type="melee"
    ):
        """

        Args:
            attack_type (str): Type of attack (melee or ranged)

        Notes:

        """
            attack_value = self.get_attack(attacker, defender, attack_type)
            defense_value = self.get_defense(attacker, defender, attack_type)
                "%s's %s attack misses %s!" % (attacker, attack_type, defender)
            )
        else:
                "%s hits %s with a %s attack for %i damage!"
                % (attacker, defender, attack_type, damage_value)
            )

    def combat_status_message(self, fighter):
        """
        Sends a message to a player with their current HP and
        distances to other fighters and objects. Called at turn
        start and by the 'status' command.
        """
        if not fighter.db.max_hp:
            fighter.db.hp = 100
            fighter.db.max_hp = 100

        status_msg = "HP Remaining: %i / %i" % (fighter.db.hp, fighter.db.max_hp)

        if not self.is_in_combat(fighter):
            fighter.msg(status_msg)
            return

        engaged_obj = []
        reach_obj = []
        range_obj = []

        for thing in fighter.db.combat_range:
            if thing != fighter:
                if fighter.db.combat_range[thing] == 0:
                    engaged_obj.append(thing)
                if fighter.db.combat_range[thing] == 1:
                    reach_obj.append(thing)
                if fighter.db.combat_range[thing] > 1:
                    range_obj.append(thing)

        if engaged_obj:
            status_msg += "|/Engaged targets: %s" % ", ".join(obj.key for obj in engaged_obj)
        if reach_obj:
            status_msg += "|/Reach targets: %s" % ", ".join(obj.key for obj in reach_obj)
        if range_obj:
            status_msg += "|/Ranged targets: %s" % ", ".join(obj.key for obj in range_obj)

        fighter.msg(status_msg)

COMBAT_RULES = RangedCombatRules()

"""
"""

class TBRangeTurnHandler(tb_basic.TBBasicTurnHandler):
    """

    remaining participants choose to end the combat with the 'disengage'
    command.
    """

    def init_range(self, to_init):
        """
        Initializes range values for an object at the start of a fight.

        Args:
            to_init (object): Object to initialize range field for.
        """
        rangedict = {}
        # Get a list of objects in the room.
        objectlist = self.obj.contents
        for thing in objectlist:
            # Object always at distance 0 from itself
            if thing == to_init:
                rangedict.update({thing: 0})
            else:
                if thing.destination or to_init.destination:
                    # Start exits at range 2 to put them at the 'edges'
                    rangedict.update({thing: 2})
                else:
                    # Start objects at range 1 from other objects
                    rangedict.update({thing: 1})
        to_init.db.combat_range = rangedict

    def join_rangefield(self, to_init, anchor_obj=None, add_distance=0):
        """
        Adds a new object to the range field of a fight in progress.

        Args:
            anchor_obj (object): Object to copy range values from, or None for a random object.
            add_distance (int): Distance to put between to_init object and anchor object.

        """
        # Get a list of room's contents without to_init object.
        contents = self.obj.contents
        contents.remove(to_init)
        # If no anchor object given, pick one in the room at random.
        if not anchor_obj:
            anchor_obj = contents[randint(0, (len(contents) - 1))]
        # Copy the range values from the anchor object.
        to_init.db.combat_range = anchor_obj.db.combat_range
        # Add the new object to everyone else's ranges.
            new_objects_range = thing.db.combat_range[anchor_obj]
            thing.db.combat_range.update({to_init: new_objects_range})
        # Set the new object's range to itself to 0.
        to_init.db.combat_range.update({to_init: 0})
        # Add additional distance from anchor object, if any.
        for n in range(add_distance):
            self.rules.withdraw(to_init, anchor_obj)

        """

        Args:

        Notes:
            In this example, characters are given two actions per turn. This allows
            characters to both move and attack in the same turn (or, alternately,
            move twice or attack twice).
        """
        super().start_turn(character)
        character.db.combat_actionsleft = ACTIONS_PER_TURN

        """

        Args:
        """
        # Add the character to the rangefield, at range from everyone, if they're not on it already.
        if not character.db.combat_range:
            self.join_rangefield(character, add_distance=2)

"""
"""

class TBRangeCharacter(tb_basic.TBBasicCharacter):
    """
    """

class TBRangeObject(DefaultObject):
    """
    An object that is assigned range values in combat. Getting, giving, and dropping
    the object has restrictions in combat - you must be next to an object to get it,
    must be next to your target to give them something, and can only interact with
    objects on your own turn.
    """

        """
        Called by the default `drop` command before this object has been
        dropped.

        Args:
            dropper (Object): The object which will drop this object.

        Returns:
            shoulddrop (bool): If the object should be dropped or not.

        Notes:
            If this method returns False/None, the dropping is cancelled

        """
        # Can't drop something if in combat and it's not your turn
        if self.rules.is_in_combat(dropper) and not self.rules.is_turn(dropper):
            dropper.msg("You can only drop things on your turn!")

        """
        Called by the default `drop` command when this object has been
        dropped.

        Args:
            dropper (Object): The object which just dropped this object.

        Notes:
            This hook cannot stop the drop from happening. Use
            permissions or the at_pre_drop() hook for that.

        """
        # If dropper is currently in combat
        if dropper.location.db.combat_turnhandler:
            # Object joins the range field
            self.db.combat_range = {}
            dropper.location.db.combat_turnhandler.join_rangefield(self, anchor_obj=dropper)

    def at_pre_get(self, getter):
        """
        Called by the default `get` command before this object has been
        picked up.

        Args:
            getter (Object): The object about to get this object.

        Returns:
            shouldget (bool): If the object should be gotten or not.

        Notes:
            If this method returns False/None, the getting is cancelled
        """
        # Restrictions for getting in combat
        if self.rules.is_in_combat(getter):
            if not self.rules.is_turn(getter):  # Not your turn
                getter.msg("You can only get things on your turn!")
            if self.rules.get_range(self, getter) > 0:  # Too far away
                getter.msg("You aren't close enough to get that! (see: help approach)")

        """
        Called by the default `get` command when this object has been
        picked up.

        Args:
            getter (Object): The object getting this object.

        Notes:
            This hook cannot stop the pickup from happening. Use
            permissions or the at_pre_get() hook for that.

        """
        # If gotten, erase range values
        if self.db.combat_range:
            del self.db.combat_range
        # Remove this object from everyone's range fields
        for thing in getter.location.contents:
            if thing.db.combat_range:
                if self in thing.db.combat_range:
                    thing.db.combat_range.pop(self, None)
        # If in combat, getter spends an action
            self.rules.spend_action(getter, 1, action_name="get")  # Use up one action.

        """
        Called by the default `give` command before this object has been
        given.

        Args:
            giver (Object): The object about to give this object.

        Returns:
            shouldgive (bool): If the object should be given or not.

        Notes:
            If this method returns False/None, the giving is cancelled

        """
        # Restrictions for giving in combat
            if not self.rules.is_turn(giver):  # Not your turn
                giver.msg("You can only give things on your turn!")
            if self.rules.get_range(giver, getter) > 0:  # Too far away from target
                giver.msg(
                    "You aren't close enough to give things to %s! (see: help approach)" % getter
                )

        """
        Called by the default `give` command when this object has been
        given.

        Args:
            giver (Object): The object giving this object.

        Notes:
            This hook cannot stop the give from happening. Use
            permissions or the at_pre_give() hook for that.

        """
            self.rules.spend_action(giver, 1, action_name="give")  # Use up one action.

"""
"""

    """

    Usage:
      fight

    """

    combat_handler_class = TBRangeTurnHandler

    """
    Attacks another character in melee.

    Usage:

    a chance to hit, and if successful, will deal damage. You can only
    attack engaged targets - that is, targets that are right next to
    you. Use the 'approach' command to get closer to a target.
    """

            return

            return

            return

            return

            return

            return

        if not self.rules.get_range(attacker, defender) == 0:  # Target isn't in melee
            self.caller.msg(
                "%s is too far away to attack - you need to get closer! (see: help approach)"
                % defender
            )
            return

        self.rules.resolve_attack(attacker, defender, "melee")

class CmdShoot(Command):
    """
    Attacks another character from range.

    Usage:
      shoot <target>

    When in a fight, you may shoot another character. The attack has
    a chance to hit, and if successful, will deal damage. You can attack
    any target in combat by shooting, but can't shoot if there are any
    targets engaged with you. Use the 'withdraw' command to retreat from
    nearby enemies.
    """

    key = "shoot"

            return

            return

            return

            return

            return

            return

        # Test to see if there are any nearby enemy targets.
        in_melee = []
        for target in attacker.db.combat_range:
            # Object is engaged and has HP
            if (
                self.rules.get_range(attacker, defender) == 0
                and target.db.hp
                and target != self.caller
            ):
                in_melee.append(target)  # Add to list of targets in melee

        if len(in_melee) > 0:
                "You can't shoot because there are fighters engaged with you (%s) - you need "
                "to retreat! (see: help withdraw)" % ", ".join(obj.key for obj in in_melee)
            )
            return

        self.rules.resolve_attack(attacker, defender, "ranged")

class CmdApproach(Command):
    """
    Approaches an object.

    Usage:
      approach <target>

    Move one space toward a character or object. You can only attack
    characters you are 0 spaces away from.
    """

    key = "approach"

        if not self.rules.is_in_combat(self.caller):  # If not in combat, can't approach.
            return

        if not self.rules.is_turn(self.caller):  # If it's not your turn, can't approach.
            return

        if not self.caller.db.hp:  # Can't approach if you have no HP.
            self.caller.msg("You can't move, you've been defeated.")
            return

        mover = self.caller
        target = self.caller.search(self.args)

        if not target:  # No valid target given.
            return

        if not target.db.combat_range:  # Target object is not on the range field
            self.caller.msg("You can't move toward that!")
            return

        if mover == target:  # Target and mover are the same
            self.caller.msg("You can't move toward yourself!")
            return

        if self.rules.get_range(mover, target) <= 0:  # Already engaged with target
            self.caller.msg("You're already next to that target!")
            return

        # If everything checks out, call the approach resolving function.
        self.rules.approach(mover, target)
        mover.location.msg_contents("%s moves toward %s." % (mover, target))
        self.rules.spend_action(self.caller, 1, action_name="move")  # Use up one action.

class CmdWithdraw(Command):
    """
    Moves away from an object.

    Usage:
      withdraw <target>

    Move one space away from a character or object.
    """

    key = "withdraw"

        if not self.rules.is_in_combat(self.caller):  # If not in combat, can't withdraw.
            return

        if not self.rules.is_turn(self.caller):  # If it's not your turn, can't withdraw.
            return

        if not self.caller.db.hp:  # Can't withdraw if you have no HP.
            return

            return

            self.caller.msg("You can't move away from that!")
            return

            self.caller.msg("You can't move away from yourself!")
            return

        if mover.db.combat_range[target] >= 3:  # Already at maximum distance
            self.caller.msg("You're as far as you can get from that target!")
            return

        self.rules.withdraw(mover, target)
        mover.location.msg_contents("%s moves away from %s." % (mover, target))

    """

    Usage:
      pass

    """

    """

    Usage:
      disengage

    """

    """

    Usage:
      rest

    """

    """

    Usage:
      status

    """

        self.rules.combat_status_message(self.caller)

    """

    Usage:
      help list
      help all

    """

        "|wAttack:|n Attack an engaged target, attempting to deal damage.|/"
        "|wShoot:|n Attack from a distance, if not engaged with other fighters.|/"
        "|wApproach:|n Move one step cloer to a target.|/"
        "|wWithdraw:|n Move one step away from a target.|/"
        "|wStatus:|n View current HP and ranges to other targets.|/"
    )

    """
    """

        """
        """
        self.add(CmdShoot())
        self.add(CmdApproach())
        self.add(CmdWithdraw())

"""
Turnbattle tests.

"""

from mock import MagicMock, patch

from . import tb_basic, tb_equip, tb_items, tb_magic, tb_range

class TestTurnBattleBasicCmd(BaseEvenniaCommandTest):
    # Test basic combat commands
    def test_turnbattlecmd(self):
        self.call(tb_basic.CmdFight(), "", "You can't start a fight if you've been defeated!")
        self.call(tb_basic.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_basic.CmdPass(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_basic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.")

class TestTurnBattleEquipCmd(BaseEvenniaCommandTest):
        self.testweapon = create_object(tb_equip.TBEWeapon, key="test weapon")
        self.testarmor = create_object(tb_equip.TBEArmor, key="test armor")
        self.testweapon.move_to(self.char1)
        self.testarmor.move_to(self.char1)

    # Test equipment commands
    def test_turnbattleequipcmd(self):
        # Start with equip module specific commands.
        self.call(tb_equip.CmdWield(), "weapon", "Char wields test weapon.")
        self.call(tb_equip.CmdUnwield(), "", "Char lowers test weapon.")
        self.call(tb_equip.CmdDon(), "armor", "Char dons test armor.")
        self.call(tb_equip.CmdDoff(), "", "Char removes test armor.")
        # Also test the commands that are the same in the basic module
        self.call(tb_equip.CmdFight(), "", "You can't start a fight if you've been defeated!")
        self.call(tb_equip.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_equip.CmdPass(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_equip.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.")

class TestTurnBattleRangeCmd(BaseEvenniaCommandTest):
    # Test range commands
    def test_turnbattlerangecmd(self):
        # Start with range module specific commands.
        self.call(tb_range.CmdShoot(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_range.CmdApproach(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_range.CmdWithdraw(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_range.CmdStatus(), "", "HP Remaining: 100 / 100")
        self.call(tb_range.CmdFight(), "", "There's nobody here to fight!")
        self.call(tb_range.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_range.CmdPass(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_range.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_range.CmdRest(), "", "Char rests to recover HP.")

class TestTurnBattleItemsCmd(BaseEvenniaCommandTest):
        self.testitem = create_object(key="test item")
        self.testitem.move_to(self.char1)

    # Test item commands
    def test_turnbattleitemcmd(self):
        self.call(tb_items.CmdUse(), "item", "'Test item' is not a usable item.")
        self.call(tb_items.CmdFight(), "", "You can't start a fight if you've been defeated!")
        self.call(tb_items.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_items.CmdPass(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_items.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_items.CmdRest(), "", "Char rests to recover HP.")

class TestTurnBattleMagicCmd(BaseEvenniaCommandTest):
    # Test magic commands
    def test_turnbattlemagiccmd(self):
        self.call(tb_magic.CmdStatus(), "", "You have 100 / 100 HP and 20 / 20 MP.")
        self.call(tb_magic.CmdLearnSpell(), "test spell", "There is no spell with that name.")
        self.call(tb_magic.CmdCast(), "", "Usage: cast <spell name> = <target>, <target2>")
        self.call(tb_magic.CmdFight(), "", "There's nobody here to fight!")
        self.call(tb_magic.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_magic.CmdPass(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_magic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
        self.call(tb_magic.CmdRest(), "", "Char rests to recover HP and MP.")

class TestTurnBattleBasicFunc(BaseEvenniaTest):
        self.testroom = create_object(DefaultRoom, key="Test Room")
        self.attacker = create_object(
            tb_basic.TBBasicCharacter, key="Attacker", location=self.testroom
        )
        self.defender = create_object(
            tb_basic.TBBasicCharacter, key="Defender", location=self.testroom
        )
        self.joiner = create_object(tb_basic.TBBasicCharacter, key="Joiner", location=None)

        self.turnhandler.stop()
        self.attacker.delete()
        self.defender.delete()
        self.joiner.delete()
        self.testroom.delete()

    # Test combat functions
    def test_tbbasicfunc(self):
        # Initiative roll
        initiative = tb_basic.COMBAT_RULES.roll_init(self.attacker)
        self.assertTrue(initiative >= 0 and initiative <= 1000)
        # Attack roll
        attack_roll = tb_basic.COMBAT_RULES.get_attack(self.attacker, self.defender)
        self.assertTrue(attack_roll >= 0 and attack_roll <= 100)
        # Defense roll
        defense_roll = tb_basic.COMBAT_RULES.get_defense(self.attacker, self.defender)
        self.assertTrue(defense_roll == 50)
        # Damage roll
        damage_roll = tb_basic.COMBAT_RULES.get_damage(self.attacker, self.defender)
        self.assertTrue(damage_roll >= 15 and damage_roll <= 25)
        # Apply damage
        self.defender.db.hp = 10
        tb_basic.COMBAT_RULES.apply_damage(self.defender, 3)
        self.assertTrue(self.defender.db.hp == 7)
        # Resolve attack
        self.defender.db.hp = 40
        tb_basic.COMBAT_RULES.resolve_attack(
            self.attacker, self.defender, attack_value=20, defense_value=10
        )
        self.assertTrue(self.defender.db.hp < 40)
        # Combat cleanup
        self.attacker.db.Combat_attribute = True
        tb_basic.COMBAT_RULES.combat_cleanup(self.attacker)
        self.assertFalse(self.attacker.db.combat_attribute)
        # Is in combat
        self.assertFalse(tb_basic.COMBAT_RULES.is_in_combat(self.attacker))
        # Set up turn handler script for further tests
        self.attacker.location.scripts.add(tb_basic.TBBasicTurnHandler)
        self.turnhandler = self.attacker.db.combat_turnHandler
        self.assertTrue(self.attacker.db.combat_turnHandler)
        # Set the turn handler's interval very high to keep it from repeating during tests.
        self.turnhandler.interval = 10000
        # Force turn order
        self.turnhandler.db.fighters = [self.attacker, self.defender]
        self.turnhandler.db.turn = 0
        # Test is turn
        self.assertTrue(tb_basic.COMBAT_RULES.is_turn(self.attacker))
        # Spend actions
        self.attacker.db.Combat_ActionsLeft = 1
        tb_basic.COMBAT_RULES.spend_action(self.attacker, 1, action_name="Test")
        self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0)
        self.assertTrue(self.attacker.db.Combat_LastAction == "Test")
        # Initialize for combat
        self.attacker.db.Combat_ActionsLeft = 983
        self.turnhandler.initialize_for_combat(self.attacker)
        self.assertTrue(self.attacker.db.Combat_LastAction == "null")
        # Start turn
        self.defender.db.Combat_ActionsLeft = 0
        self.turnhandler.start_turn(self.defender)
        self.assertTrue(self.defender.db.Combat_ActionsLeft == 1)
        # Next turn
        self.turnhandler.next_turn()
        self.assertTrue(self.turnhandler.db.turn == 1)
        # Turn end check
        self.attacker.db.Combat_ActionsLeft = 0
        self.turnhandler.turn_end_check(self.attacker)
        # Join fight
        self.joiner.location = self.testroom
        self.turnhandler.join_fight(self.joiner)
        self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender])

class TestTurnBattleEquipFunc(BaseEvenniaTest):
            tb_equip.TBEquipCharacter, key="Attacker", location=self.testroom
        )
            tb_equip.TBEquipCharacter, key="Defender", location=self.testroom
        )
        self.joiner = create_object(tb_equip.TBEquipCharacter, key="Joiner", location=None)

    # Test the combat functions in tb_equip too. They work mostly the same.
    def test_tbequipfunc(self):
        initiative = tb_equip.COMBAT_RULES.roll_init(self.attacker)
        attack_roll = tb_equip.COMBAT_RULES.get_attack(self.attacker, self.defender)
        self.assertTrue(attack_roll >= -50 and attack_roll <= 150)
        defense_roll = tb_equip.COMBAT_RULES.get_defense(self.attacker, self.defender)
        damage_roll = tb_equip.COMBAT_RULES.get_damage(self.attacker, self.defender)
        self.assertTrue(damage_roll >= 0 and damage_roll <= 50)
        tb_equip.COMBAT_RULES.apply_damage(self.defender, 3)
        tb_equip.COMBAT_RULES.resolve_attack(
        )
        tb_equip.COMBAT_RULES.combat_cleanup(self.attacker)
        self.assertFalse(tb_equip.COMBAT_RULES.is_in_combat(self.attacker))
        self.attacker.location.scripts.add(tb_equip.TBEquipTurnHandler)
        self.assertTrue(tb_equip.COMBAT_RULES.is_turn(self.attacker))
        tb_equip.COMBAT_RULES.spend_action(self.attacker, 1, action_name="Test")
        # Next turn

class TestTurnBattleRangeFunc(BaseEvenniaTest):
            tb_range.TBRangeCharacter, key="Attacker", location=self.testroom
        )
            tb_range.TBRangeCharacter, key="Defender", location=self.testroom
        )
        self.joiner = create_object(tb_range.TBRangeCharacter, key="Joiner", location=self.testroom)

    # Test combat functions in tb_range too.
    def test_tbrangefunc(self):
        initiative = tb_range.COMBAT_RULES.roll_init(self.attacker)
        attack_roll = tb_range.COMBAT_RULES.get_attack(
            self.attacker, self.defender, attack_type="test"
        )
        defense_roll = tb_range.COMBAT_RULES.get_defense(
        )
        damage_roll = tb_range.COMBAT_RULES.get_damage(self.attacker, self.defender)
        tb_range.COMBAT_RULES.apply_damage(self.defender, 3)
        tb_range.COMBAT_RULES.resolve_attack(
            self.attacker, self.defender, attack_type="test", attack_value=20, defense_value=10
        )
        tb_range.COMBAT_RULES.combat_cleanup(self.attacker)
        self.assertFalse(tb_range.COMBAT_RULES.is_in_combat(self.attacker))
        self.attacker.location.scripts.add(tb_range.TBRangeTurnHandler)
        self.assertTrue(tb_range.COMBAT_RULES.is_turn(self.attacker))
        tb_range.COMBAT_RULES.spend_action(self.attacker, 1, action_name="Test")
        # Set up ranges again, since initialize_for_combat clears them
        self.attacker.db.combat_range = {}
        self.attacker.db.combat_range[self.attacker] = 0
        self.attacker.db.combat_range[self.defender] = 1
        self.defender.db.combat_range = {}
        self.defender.db.combat_range[self.defender] = 0
        self.defender.db.combat_range[self.attacker] = 1
        self.assertEqual(self.defender.db.Combat_ActionsLeft, 2)
        # Next turn
        # Now, test for approach/withdraw functions
        self.assertTrue(tb_range.COMBAT_RULES.get_range(self.attacker, self.defender) == 1)
        # Approach
        tb_range.COMBAT_RULES.approach(self.attacker, self.defender)
        self.assertTrue(tb_range.COMBAT_RULES.get_range(self.attacker, self.defender) == 0)
        # Withdraw
        tb_range.COMBAT_RULES.withdraw(self.attacker, self.defender)

class TestTurnBattleItemsFunc(BaseEvenniaTest):
    @patch("evennia.contrib.game_systems.turnbattle.tb_items.tickerhandler", new=MagicMock())
            tb_items.TBItemsCharacter, key="Attacker", location=self.testroom
        )
            tb_items.TBItemsCharacter, key="Defender", location=self.testroom
        )
        self.joiner = create_object(tb_items.TBItemsCharacter, key="Joiner", location=self.testroom)
        self.user = create_object(tb_items.TBItemsCharacter, key="User", location=self.testroom)
        self.test_healpotion = create_object(key="healing potion")
        self.test_healpotion.db.item_func = "heal"
        self.test_healpotion.db.item_uses = 3

        self.user.delete()

    # Test functions in tb_items.
    def test_tbitemsfunc(self):
        initiative = tb_items.COMBAT_RULES.roll_init(self.attacker)
        attack_roll = tb_items.COMBAT_RULES.get_attack(self.attacker, self.defender)
        defense_roll = tb_items.COMBAT_RULES.get_defense(self.attacker, self.defender)
        damage_roll = tb_items.COMBAT_RULES.get_damage(self.attacker, self.defender)
        tb_items.COMBAT_RULES.apply_damage(self.defender, 3)
        tb_items.COMBAT_RULES.resolve_attack(
        )
        tb_items.COMBAT_RULES.combat_cleanup(self.attacker)
        self.assertFalse(tb_items.COMBAT_RULES.is_in_combat(self.attacker))
        self.attacker.location.scripts.add(tb_items.TBItemsTurnHandler)
        self.assertTrue(tb_items.COMBAT_RULES.is_turn(self.attacker))
        tb_items.COMBAT_RULES.spend_action(self.attacker, 1, action_name="Test")
        # Next turn
        # Now time to test item stuff.
        # Spend item use
        tb_items.COMBAT_RULES.spend_item_use(self.test_healpotion, self.user)
        self.assertTrue(self.test_healpotion.db.item_uses == 2)
        # Use item
        self.user.db.hp = 2
        tb_items.COMBAT_RULES.use_item(self.user, self.test_healpotion, self.user)
        self.assertTrue(self.user.db.hp > 2)
        # Add contition
        tb_items.COMBAT_RULES.add_condition(self.user, self.user, "Test", 5)
        self.assertTrue(self.user.db.conditions == {"Test": [5, self.user]})
        # Condition tickdown
        tb_items.COMBAT_RULES.condition_tickdown(self.user, self.user)
        self.assertEqual(self.user.db.conditions, {"Test": [4, self.user]})
        # Test item functions now!
        # Item heal
        tb_items.COMBAT_RULES.itemfunc_heal(self.test_healpotion, self.user, self.user)
        # Item add condition
        self.user.db.conditions = {}
        tb_items.COMBAT_RULES.itemfunc_add_condition(self.test_healpotion, self.user, self.user)
        self.assertTrue(self.user.db.conditions == {"Regeneration": [5, self.user]})
        # Item cure condition
        self.user.db.conditions = {"Poisoned": [5, self.user]}
        tb_items.COMBAT_RULES.itemfunc_cure_condition(self.test_healpotion, self.user, self.user)
        self.assertTrue(self.user.db.conditions == {})

class TestTurnBattleMagicFunc(BaseEvenniaTest):
            tb_magic.TBMagicCharacter, key="Attacker", location=self.testroom
        )
            tb_magic.TBMagicCharacter, key="Defender", location=self.testroom
        )
        self.joiner = create_object(tb_magic.TBMagicCharacter, key="Joiner", location=self.testroom)

    # Test combat functions in tb_magic.
        initiative = tb_magic.COMBAT_RULES.roll_init(self.attacker)
        attack_roll = tb_magic.COMBAT_RULES.get_attack(self.attacker, self.defender)
        defense_roll = tb_magic.COMBAT_RULES.get_defense(self.attacker, self.defender)
        damage_roll = tb_magic.COMBAT_RULES.get_damage(self.attacker, self.defender)
        tb_magic.COMBAT_RULES.apply_damage(self.defender, 3)
        tb_magic.COMBAT_RULES.resolve_attack(
        )
        tb_magic.COMBAT_RULES.combat_cleanup(self.attacker)
        self.assertFalse(tb_magic.COMBAT_RULES.is_in_combat(self.attacker))
        self.attacker.location.scripts.add(tb_magic.TBMagicTurnHandler)
        self.assertTrue(tb_magic.COMBAT_RULES.is_turn(self.attacker))
        tb_magic.COMBAT_RULES.spend_action(self.attacker, 1, action_name="Test")
        # Next turn

"""
Extended Room

Evennia Contribution - Griatch 2012, vincent-lg 2019, Griatch 2023

This is an extended Room typeclass for Evennia, supporting descriptions that vary
by season, time-of-day or arbitrary states (like burning). It has details, embedded
state tags, support for repeating random messages as well as a few extra commands.

- The room description can be set to change depending on the season or time of day.
- Parts of the room description can be set to change depending on arbitrary states (like burning).
- Details can be added to the room, which can be looked at like objects.
- Alternative text sections can be added to the room description, which will only show if
  the room is in a given state.
- Random messages can be set to repeat at a given rate.

Installation/testing:

Adding the `ExtendedRoomCmdset` to the default character cmdset will add all
new commands for use.

In more detail, in mygame/commands/default_cmdsets.py:

```
...
from evennia.contrib import extended_room   # <---

class CharacterCmdset(default_cmds.Character_CmdSet):
    ...
        ...
        self.add(extended_room.ExtendedRoomCmdSet)  # <---

```

Then reload to make the bew commands available. Note that they only work
on rooms with the typeclass `ExtendedRoom`. Create new rooms with the right
typeclass or use the `typeclass` command to swap existing rooms.

"""

import re
from collections import deque

    CmdSet,
    EvEditor,
    FuncParser,
    gametime,
    utils,
)
from evennia.typeclasses.attributes import AttributeProperty
from evennia.utils.utils import list_to_string, repeat

# error return function, needed by Extended Look command
_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))

# funcparser callable for the ExtendedRoom

def func_state(roomstate, *args, looker=None, room=None, **kwargs):
    """
    Usage: $state(roomstate, text)

    Funcparser callable for ExtendedRoom. This is called by the FuncParser when it
    returns the description of the room. Use 'default' for a default text when no
    other states are set.

    Args:
        roomstate (str): A roomstate, like "morning", "raining". This is case insensitive.
        *args: All these will be combined into one string separated by commas.

        looker (Object): The object looking at the room. Unused by default.
        room (ExtendedRoom): The room being looked at.

    Example:

        $state(morning, It is a beautiful morning!)

    Notes:
        We try to merge all args into one text, since this function doesn't require more than one
        argument. That way, one may be able to get away without using quotes.

    """
    roomstate = str(roomstate).lower()
    text = ", ".join(args)
    # make sure we have a room and a caller and not something parsed from the string
    if not (roomstate and looker and room) or isinstance(looker, str) or isinstance(room, str):
        return ""

    try:
        if roomstate in room.room_states or roomstate == room.get_time_of_day():
            return text
        if roomstate == "default" and not room.room_states:
            # return this if no roomstate is set
    except AttributeError:
        # maybe used on a non-ExtendedRoom?
        pass
    return ""

class ExtendedRoom(DefaultRoom):
    """
    An Extended Room

    Room states:
      A room state is set as a Tag with category "roomstate" and tagkey "on_fire" or "flooded"
      etc).

    Alternative descriptions:
    - Add an Attribute `desc_<roomstate>` to the room, where <roomstate> is the name of the
      roomstate to use this for, like `desc_on_fire` or `desc_flooded`. If not given, seasonal
      descriptions given in desc_spring/summer/autumn/winter will be used, and last the
      regular `desc` Attribute.

    Alternative text sections
    - Used to add alternative text sections to the room description. These are embedded in the
      description by adding `$state(roomstate, txt)`. They will show only if the room is in the
      given roomstate. These are managed via the add/remove/get_alt_text methods.

    Details:
    - This is set as an Attribute `details` (a dict) on the room, with the detail name as key.
      When looking at this room, the detail name can be used as a target to look at without having
      to add an actual database object for it. The `detail` command is used to add/remove details.

    Room messages
    - Set `room_message_rate > 0` and add a list of `room_messages`. These will be randomly
      echoed to the room at the given rate.

    """

    # fallback description if nothing else is set
    fallback_desc = "You see nothing special."

    # tag room_state category
    room_state_tag_category = "room_state"

    # time setup
    months_per_year = 12
    hours_per_day = 24

    # seasons per year, given as (start, end) boundaries, each a fraction of a year. These
    # will change the description. The last entry should wrap around to the first.
    seasons_per_year = {
        "spring": (3 / months_per_year, 6 / months_per_year),  # March - May
        "summer": (6 / months_per_year, 9 / months_per_year),  # June - August
        "autumn": (9 / months_per_year, 12 / months_per_year),  # September - November
        "winter": (12 / months_per_year, 3 / months_per_year),  # December - February
    }

    # time-dependent room descriptions (these must match the `seasons_per_year` above).
    desc_spring = AttributeProperty("", autocreate=False)
    desc_summer = AttributeProperty("", autocreate=False)
    desc_autumn = AttributeProperty("", autocreate=False)
    desc_winter = AttributeProperty("", autocreate=False)

    # time-dependent embedded descriptions, usable as $timeofday(morning, text)
    # (start, end) boundaries, each a fraction of a day. The last one should
    # end at 0 (not 24) to wrap around to midnight.
    times_of_day = {
        "night": (0, 6 / hours_per_day),  # midnight - 6AM
        "morning": (6 / hours_per_day, 12 / hours_per_day),  # 6AM - noon
        "afternoon": (12 / hours_per_day, 18 / hours_per_day),  # noon - 6PM
        "evening": (18 / hours_per_day, 0),  # 6PM - midnight
    }

    # normal vanilla description if no other `*_desc` matches or are set.
    desc = AttributeProperty("", autocreate=False)

    # look-targets without database objects
    details = AttributeProperty(dict, autocreate=False)

    # messages to send to the room
    room_message_rate = 0  # set >0s to enable
    room_messages = AttributeProperty(list, autocreate=False)

    # Broadcast message

    def _get_funcparser(self, looker):
        return FuncParser(
            {"state": func_state},
            looker=looker,
            room=self,
        )

    def _start_broadcast_repeat_task(self):
        if self.room_message_rate and self.room_messages and not self.ndb.broadcast_repeat_task:
            self.ndb.broadcast_repeat_task = repeat(
                self.room_message_rate, self.repeat_broadcast_msg_to_room, persistent=False
            )

        """Evennia hook. Start up repeating function whenever object loads into memory."""
        self._start_broadcast_repeat_task()

    def start_repeat_broadcast_messages(self):
        """
        Start repeating the broadcast messages. Only needs to be called if adding messages
        and not having reloaded the server.

        """

    def repeat_broadcast_message_to_room(self):
        """
        Send a message to the room at room_message_rate. By default
        we will randomize which one to send.

        """
        self.msg_contents(random.choice(self.room_messages))

    def get_time_of_day(self):
        """
        Get the current time of day.

        Override to customize.

        Returns:
            str: The time of day, such as 'morning', 'afternoon', 'evening' or 'night'.

        """
        timestamp = gametime.gametime(absolute=True)
        datestamp = datetime.datetime.fromtimestamp(timestamp)
        timeslot = float(datestamp.hour) / self.hours_per_day

        for time_of_day, (start, end) in self.times_of_day.items():
            if start < end and start <= timeslot < end:
                return time_of_day
        return time_of_day  # final back to the beginning

    def get_season(self):
        """
        Get the current season.

        Returns:
            str: The season, such as 'spring', 'summer', 'autumn' or 'winter'.

        """
        timeslot = float(datestamp.month) / self.months_per_year

        for season_of_year, (start, end) in self.seasons_per_year.items():
                return season_of_year
        return season_of_year  # final step is back to beginning

    # manipulate room states

    @property
    def room_states(self):
        """
        Get all room_states set on this room.

        """
        return list(sorted(self.tags.get(category=self.room_state_tag_category, return_list=True)))

    def add_room_state(self, *room_states):
        """
        Set a room-state or room-states to the room.

        Args:
            *room_state (str): A room state like 'on_fire' or 'flooded'. This will affect
                what `desc_*` and `roomstate_*` descriptions/inlines are used. You can add
                more than one at a time.

        Notes:
            You can also set time-based room_states this way, like 'morning' or 'spring'. This
            can be useful to force a particular description, but while this state is
            set this way, that state will be unaffected by the passage of time. Remove
            the state to let the current game time determine this type of states.

        """
        self.tags.batch_add(*((state, self.room_state_tag_category) for state in room_states))

    def remove_room_state(self, *room_states):
        """
        Remove a roomstate from the room.

        Args:
            *room_state (str): A roomstate like 'on_fire' or 'flooded'. If the
            room did not have this state, nothing happens.You can remove more than one at a time.

        """
        for room_state in room_states:
            self.tags.remove(room_state, category=self.room_state_tag_category)

    def clear_room_state(self):
        """
        Clear all room states.

        Note that fallback time-of-day and seasonal states are not affected by this, only
        custom states added with `.add_room_state()`.

        """
        self.tags.clear(category="room_state")

    # control the available room descriptions

    def add_desc(self, desc, room_state=None):
        """
        Add a custom description, matching a particular room state.

        Args:
            desc (str): The description to use when this roomstate is active.
            roomstate (str, None): The roomstate to match, like 'on_fire', 'flooded', or "spring".
                If `None`, set the default `desc` fallback.

        """
        if room_state is None:
            self.attributes.add("desc", desc)
        else:
            self.attributes.add(f"desc_{room_state}", desc)

    def remove_desc(self, room_state):
        """
        Remove a custom description.

        Args:
            room_state (str): The room-state description to remove.

        """
        self.attributes.remove(f"desc_{room_state}")

    def all_desc(self):
        """
        Get all available descriptions.

        Returns:
            dict: A mapping of roomstate to description. The `None` key indicates the
                base subscription (stored in the `desc` Attribute).

        """
        return {
            **{None: self.db.desc or ""},
            **{
                attr.key[5:]: attr.value
                for attr in self.db_attributes.filter(db_key__startswith="desc_").order_by("db_key")
            },
        }

    def get_stateful_desc(self):
        """
        Get the currently active room description based on the current roomstate.

        Returns:
            str: The current description.

        Note:
            Only one description can be active at a time. Priority order is as follows:

            Priority order is as follows:

                1. Room-states set by `add_roomstate()` that are not seasons.
                   If multiple room_states are set, the first one is used, sorted alphabetically.
                2. Seasons set by `add_room_state()`. This allows to 'pin' a season.
                3. Time-based seasons based on the current in-game time.
                4. None, if no seasons are defined in `.seasons_per_year`.

            If either of the above is found, but doesn't have a matching `desc_<roomstate>`
            description, we move on to the next priority. If no matches are found, the `desc`
            Attribute is used.

        """

        room_states = self.room_states
        seasons = self.seasons_per_year.keys()
        seasonal_room_states = []

        # get all available descriptions on this room
        # note: *_desc is the old form, we support it for legacy
        descriptions = dict(
            self.db_attributes.filter(
                Q(db_key__startswith="desc_") | Q(db_key__endswith="_desc")
            ).values_list("db_key", "db_value")
        )

        for roomstate in sorted(room_states):
            if roomstate not in seasons:
                # if we have a roomstate that is not a season, use it
                if desc := descriptions.get(f"desc_{roomstate}") or descriptions.get(
                    "{roomstate}_desc"
                ):
                    return desc
            else:
                seasonal_room_states.append(roomstate)

        if not seasons:
            # no seasons defined, so just return the default desc
            return self.attributes.get("desc")

        for seasonal_roomstate in seasonal_room_states:
            # explicit setting of season outside of automatic time keeping
            if desc := descriptions.get(f"desc_{seasonal_roomstate}"):
                return desc

        # no matching room_states, use time-based seasons. Also support legacy *_desc form
        season = self.get_season()
        if desc := descriptions.get(f"desc_{season}") or descriptions.get(f"{season}_desc"):
            return desc

        # fallback to normal desc Attribute
        return self.attributes.get("desc", self.fallback_desc)

    def replace_legacy_time_of_day_markup(self, desc):
        """
        Filter description by legacy markup like `<morning>...</morning>`. Filter
        out all such markings that does not match the current time. Supports
        'morning', 'afternoon', 'evening' and 'night'.

        Args:
            desc (str): The unmodified description.

        Returns:
            str: A possibly modified description.

        Notes:
            This is legacy. Use the $state markup for new rooms instead.

        """
        desc = desc or ""
        time_of_day = self.get_time_of_day()

        # regexes for in-desc replacements (gets cached)
        if not hasattr(self, "legacy_timeofday_regex_map"):
            timeslots = deque()
            for time_of_day in self.times_of_day:
                timeslots.append(
                    (
                        time_of_day,
                        re.compile(rf"<{time_of_day}>(.*?)</{time_of_day}>", re.IGNORECASE),
                    )
                )

            # map the regexes cyclically, so each one is first once
            self.legacy_timeofday_regex_map = {}
            for i in range(len(timeslots)):
                # mapping {"morning": [morning_regex, ...], ...}
                self.legacy_timeofday_regex_map[timeslots[0][0]] = [tup[1] for tup in timeslots]
                timeslots.rotate(-1)

        # do the replacement
        regextuple = self.legacy_timeofday_regex_map[time_of_day]
        desc = regextuple[0].sub(r"\1", desc)
        desc = regextuple[1].sub("", desc)
        desc = regextuple[2].sub("", desc)
        return regextuple[3].sub("", desc)

        """
        Evennia standard hook. Dynamically get the 'desc' component of the object description. This
        is called by the return_appearance method and in turn by the 'look' command.

        Args:
            looker (Object): Object doing the looking (unused by default).
        Returns:

        """
        # get the current description based on the roomstate
        desc = self.get_stateful_desc()
        # parse for legacy <morning>...</morning> markers
        desc = self.replace_legacy_time_of_day_markup(desc)
        # apply funcparser
        desc = self._get_funcparser(looker).parse(desc, **kwargs)

    # manipulate details

    def add_detail(self, key, description):
        """
        This sets a new detail, using an Attribute "details".

        Args:
            detailkey (str): The detail identifier to add (for
                aliases you need to add multiple keys to the
                same description). Case-insensitive.
            description (str): The text to return when looking
                at the given detailkey. This can contain funcparser directives.

        """
        if not self.details:
            self.details = {}  # causes it to be created as real attribute
        self.details[key.lower()] = description

    set_detail = add_detail  # legacy name

    def remove_detail(self, key, *args):
        """
        Delete a detail.

        Args:
            key (str): the detail to remove (case-insensitive).
            *args: Unused (backwards compatibility)

        The description is only included for compliance but is completely
        ignored.  Note that this method doesn't raise any exception if
        the detail doesn't exist in this room.

        """
        self.details.pop(key.lower(), None)

    del_detail = remove_detail  # legacy alias

    def get_detail(self, key, looker=None):
        """
        This will attempt to match a "detail" to look for in the room.
        This will do a lower-case match followed by a startsby match. This
        is called by the new `look` Command.

        Args:
            key (str): A detail identifier.
            looker (Object, optional): The one looking.

        Returns:
            detail (str or None): A detail matching the given key, or `None` if
            it was not found.

        Notes:
            A detail is a way to offer more things to look at in a room
            without having to add new objects. For this to work, we
            require a custom `look` command that allows for `look <detail>`
            - the look command should defer to this method on
            the current location (if it exists) before giving up on
            finding the target.

        """
        detail_keys = tuple(self.details.keys())

        detail = None
        if key in detail_keys:
            # exact match
            detail = self.details[key]
        else:
            # find closest match starting with key (shortest difference in length)
            lkey = len(key)
            startswith_matches = sorted(
                (
                    (detail_key, abs(lkey - len(detail_key)))
                    for detail_key in detail_keys
                    if detail_key.startswith(key)
                ),
                key=lambda tup: tup[1],
            )
            if startswith_matches:
                # use the matching startswith-detail with the shortest difference in length
                detail = self.details[startswith_matches[0][0]]

        if detail:
            detail = self._get_funcparser(looker).parse(detail)

        return detail

    return_detail = get_detail  # legacy name

# Custom Look command supporting Room details. Add this to
# the Default cmdset to use.

class CmdExtendedRoomLook(default_cmds.CmdLook):
    """
    look

    Usage:
      look
      look <obj>
      look <room detail>

    Observes your location, details at your location or objects in your vicinity.
    """

    def look_detail(self):
        """
        Look for detail on room.
        """
        if hasattr(self.caller.location, "get_detail"):
            detail = self.caller.location.get_detail(self.args, looker=self.caller)
            if detail:
                caller.location.msg_contents(
                    f"$You() $conj(look) closely at {self.args}.\n",
                    from_obj=caller,
                    exclude=caller,
                )
                caller.msg(detail)

        """
        """
                return
        else:
            # search, waiting to return errors so we can also check details
            target = caller.search(self.args, quiet=True)
            # if there's no target, check details
                # no target AND no detail means run the normal no-results message
                if not self.look_detail():
                    _AT_SEARCH_RESULT(target, caller, self.args, quiet=False)
                return
            # otherwise, run normal search result handling
            target = _AT_SEARCH_RESULT(target, caller, self.args, quiet=False)
                return

# Custom build commands for setting seasonal descriptions
# and detailing extended rooms.

    return caller.db.eveditor_target.db.desc or ""

    """
    """
    roomstates = caller.db.eveditor_roomstates
    target = caller.db.eveditor_target

    if not roomstates or not hasattr(target, "add_desc"):
        # normal description
        target.db.desc = buf
    elif roomstates:
        for roomstate in roomstates:
            target.add_desc(buf, room_state=roomstate)
    else:

    caller.attributes.remove("eveditor_target")

class CmdExtendedRoomDesc(default_cmds.CmdDesc):
    """

    Usage:
      @desc[/switch] [<obj> =] <description>

    Switches:
      del - Delete the description of an object. If another state is given, its description
        will be deleted.
      spring|summer|autumn|winter - room description to use in respective in-game season
      <other> - room description to use with an arbitrary room state.

    Sets the description an object. If an object is not given,
    describe the current room, potentially showing any additional stateful descriptions. The room
    states only work with rooms.

    Examples:
        @desc/winter A cold winter scene.
        @desc/edit/summer
        @desc/burning This room is burning!
        @desc A normal room with no state.
        @desc/del/burning

    Rooms will automatically change season as the in-game time changes. You can
    set a specific room-state with the |wroomstate|n command.

    """

    switch_options = None

        self.delete_mode = "del" in self.switches
        self.edit_mode = not self.delete_mode and "edit" in self.switches

        self.object_mode = "=" in self.args

        # all other switches are names of room-states
        self.roomstates = [state for state in self.switches if state not in ("edit", "del")]

            return
        else:
        if not obj:
            return

            self.caller.msg(f"You don't have permission to edit the description of {obj.key}.")
            return

        self.caller.db.eveditor_target = obj
        self.caller.db.eveditor_roomstates = self.roomstates
        EvEditor(
        )
        return

    def show_stateful_descriptions(self):
        location = self.caller.location
        room_states = location.room_states
        season = location.get_season()
        time_of_day = location.get_time_of_day()
        stateful_descs = location.all_desc()

        output = [
            f"Room {location.get_display_name(self.caller)} "
            f"Season: {season}. Time: {time_of_day}. "
            f"States: {', '.join(room_states) if room_states else 'None'}"
        ]
        other_active = False
        for state, desc in stateful_descs.items():
            if state is None:
                continue
            if state == season or state in room_states:
                output.append(f"Room state |w{state}|n |g(active)|n:\n{desc}")
                other_active = True
            else:
                output.append(f"Room state |w{state}|n:\n{desc}")

        active = " |g(active)|n" if not other_active else ""
        output.append(f"Room state |w(default)|n{active}:\n{location.db.desc}")

        sep = "\n" + "-" * 78 + "\n"
        self.caller.msg(sep.join(output))

        if not self.args and "edit" not in self.switches and "del" not in self.switches:
            if caller.location:
                # show stateful descs on the room
                self.show_stateful_descriptions()
                return
            else:
                caller.msg("You have no location to describe!")
                return

        if self.edit_mode:
            return

        if self.object_mode:
            # We are describing an object
            target = caller.search(self.lhs)
                return
        else:
            # we are describing the current room
            target = caller.location or self.msg("|rYou don't have a location to describe.|n")
                return

        roomstates = self.roomstates
        if target.access(self.caller, "control") or target.access(self.caller, "edit"):
            if not roomstates or not hasattr(target, "add_desc"):
                # normal description
                target.db.desc = desc
            elif roomstates:
                for roomstate in roomstates:
                    if self.delete_mode:
                        target.remove_desc(roomstate)
                        caller.msg(f"The {roomstate}-description was deleted, if it existed.")
                    else:
                        target.add_desc(desc, room_state=roomstate)
                            f"The {roomstate}-description was set on"
                            f" {target.get_display_name(caller)}."
                        )
            else:
                caller.msg(f"The description was set on {target.get_display_name(caller)}.")
        else:
                "You don't have permission to edit the description "
                f"of {target.get_display_name(caller)}."
            )

class CmdExtendedRoomDetail(default_cmds.MuxCommand):

    """
    sets a detail on a room

    Usage:
        @detail[/del] <key> [= <description>]
        @detail <key>;<alias>;... = description

    Example:
        @detail
        @detail walls = The walls are covered in ...
        @detail castle;ruin;tower = The distant ruin ...
        @detail/del wall
        @detail/del castle;ruin;tower

    This command allows to show the current room details if you enter it
    without any argument.  Otherwise, sets or deletes a detail on the current
    room, if this room supports details like an extended room. To add new
    detail, just use the @detail command, specifying the key, an equal sign
    and the description.  You can assign the same description to several
    details using the alias syntax (replace key by alias1;alias2;alias3;...).
    To remove one or several details, use the @detail/del switch.

    """

    key = "@detail"
    locks = "cmd:perm(Builder)"

            details = location.db.details
            if not details:
                self.msg(
                    f"|rThe room {location.get_display_name(self.caller)} doesn't have any"
                    " details.|n"
                )
            else:
                details = sorted(["|y{}|n: {}".format(key, desc) for key, desc in details.items()])
                self.msg("Details on Room:\n" + "\n".join(details))
            return

        if not self.rhs and "del" not in self.switches:
            detail = location.return_detail(self.lhs)
            if detail:
                self.msg("Detail '|y{}|n' on Room:\n{}".format(self.lhs, detail))
            else:
                self.msg("Detail '{}' not found.".format(self.lhs))
            return

        method = "add_detail" if "del" not in self.switches else "remove_detail"
        if not hasattr(location, method):
            self.caller.msg("Details cannot be set on %s." % location)
            return
        for key in self.lhs.split(";"):
            # loop over all aliases, if any (if not, this will just be
            # the one key to loop over)
            getattr(location, method)(key, self.rhs)
            self.caller.msg(f"Deleted detail '{self.lhs}', if it existed.")
        else:
            self.caller.msg(f"Set detail '{self.lhs}': '{self.rhs}'")

class CmdExtendedRoomState(default_cmds.MuxCommand):
    """
    Toggle and view room state for the current room.

    Usage:
        @roomstate [<roomstate>]

    Examples:
        @roomstate spring
        @roomstate burning
        @roomstate burning      (a second time toggles it off)

    If the roomstate was already set, it will be disabled. Use
    without arguments to see the roomstates on the current room.

    """

    key = "@roomstate"

        if not self.room or not hasattr(self.room, "room_states"):
            self.caller.msg("You have no current location, or it doesn't support room states.")
            raise InterruptCommand()

        self.room_state = self.args.strip().lower()

        room_state = self.room_state

        if room_state:
            # toggle room state
            if room_state in room.room_states:
                room.remove_room_state(room_state)
                caller.msg(f"Cleared room state '{room_state}' from this room.")
            else:
                room.add_room_state(room_state)
                caller.msg(f"Added room state '{room_state}' to this room.")
        else:
            # view room states
            room_states = list_to_string(
                [f"'{state}'" for state in room.room_states] if room.room_states else ("None",)
            )
                "Room states (not counting automatic time/season) on"
                f" {room.get_display_name(caller)}:\n {room_states}"
            )

class CmdExtendedRoomGameTime(default_cmds.MuxCommand):
    """
    Check the game time.

    Usage:
        time

    Shows the current in-game time and season.

    """

    key = "time"

        if (
            not location
            or not hasattr(location, "get_time_of_day")
            or not hasattr(location, "get_season")
        ):
            self.caller.msg("No location available - you are outside time.")
        self.location = location

        location = self.location

        timeslot = location.get_time_of_day()

        prep = "an" if season == "autumn" else "a"
        self.caller.msg(f"It's {prep} {season} day, in the {timeslot}.")

# CmdSet for easily install all commands

class ExtendedRoomCmdSet(CmdSet):
    """
    Groups the extended-room commands.

    """

        self.add(CmdExtendedRoomLook())
        self.add(CmdExtendedRoomDesc())
        self.add(CmdExtendedRoomDetail())
        self.add(CmdExtendedRoomState())
        self.add(CmdExtendedRoomGameTime())

"""
Testing of ExtendedRoom contrib

"""

from evennia import create_object
from evennia.utils.test_resources import BaseEvenniaCommandTest, EvenniaTestCase

from . import extended_room

def _get_timestamp(season, time_of_day):
    """
    Utility to get a timestamp for a given season and time of day.

    """
    # grab a month / time given a season and time of day
    seasons = {"spring": 3, "summer": 6, "autumn": 9, "winter": 12}
    times_of_day = {"morning": 6, "afternoon": 12, "evening": 18, "night": 0}
    # return a datetime object for the 1st of the month at the given hour
    return datetime.datetime(2064, seasons[season], 1, times_of_day[time_of_day]).timestamp()

class TestExtendedRoom(EvenniaTestCase):
    """
    Test Extended Room typeclass.

    """

    base_room_desc = "Base room description."

        self.room = create_object(extended_room.ExtendedRoom, key="Test Room")
        self.room.desc = self.base_room_desc

    def test_room_description(self):
        """
        Test that the vanilla room description is returned as expected.
        """
        room_desc = self.room.get_display_desc(None)
        self.assertEqual(room_desc, self.base_room_desc)

        [
            ("spring", "Spring room description."),
            ("summer", "Summer room description."),
            ("autumn", "Autumn room description."),
            ("winter", "Winter room description."),
        ]
    )
    @patch("evennia.utils.gametime.gametime")
    def test_seasonal_room_descriptions(self, season, desc, mock_gametime):
        """
        Test that the room description changes with the season.
        """
        mock_gametime.return_value = _get_timestamp(season, "morning")
        self.room.add_desc(desc, room_state=season)

        self.assertEqual(room_desc, desc)

        [
            ("morning", "Morning room description."),
            ("afternoon", "Afternoon room description."),
            ("evening", "Evening room description."),
            ("night", "Night room description."),
        ]
    )
    def test_get_time_of_day_tags(self, time_of_day, desc, mock_gametime):
        """
        Test room with $
        """
        mock_gametime.return_value = _get_timestamp("spring", time_of_day)
        room_time_of_day = self.room.get_time_of_day()
        self.assertEqual(room_time_of_day, time_of_day)

        self.room.add_desc(
            "$state(morning, Morning room description.)"
            "$state(afternoon, Afternoon room description.)"
            "$state(evening, Evening room description.)"
            "$state(night, Night room description.)"
            " What a great day!"
        )
        char = Mock()
        room_desc = self.room.get_display_desc(char)
        self.assertEqual(room_desc, f"{desc} What a great day!")

    def test_room_states(self):
        """
        Test rooms with custom game states.

        """
            "$state(under_construction, This room is under construction.)"
            " $state(under_repair, This room is under repair.)"
        )
        self.room.add_room_state("under_construction")
        self.assertEqual(self.room.room_states, ["under_construction"])
        self.assertEqual(self.room.get_display_desc(char), "This room is under construction. ")

        self.room.add_room_state("under_repair")
        self.assertEqual(set(self.room.room_states), set(["under_construction", "under_repair"]))
            self.room.get_display_desc(char),
            "This room is under construction. This room is under repair.",
        )

        self.room.remove_room_state("under_construction")
            " This room is under repair.",
        )

    def test_alternative_descs(self):
        """
        Test rooms with alternate descriptions.

        """
        from evennia import ObjectDB

        ObjectDB.objects.all()  # TODO - fixes an issue with home FK missing

        self.room.add_desc("The room is burning!", room_state="burning")
        self.room.add_desc("The room is flooding!", room_state="flooding")
        self.assertEqual(self.room.get_display_desc(None), self.base_room_desc)

        self.room.add_room_state("burning")
        self.assertEqual(self.room.get_display_desc(None), "The room is burning!")

        self.room.add_room_state("flooding")
        self.room.remove_room_state("burning")
        self.assertEqual(self.room.get_display_desc(None), "The room is flooding!")

        self.room.clear_room_state()

    def test_details(self):
        """
        Test room details.

        """
        self.room.add_detail("test", "Test detail.")
        self.room.add_detail("test2", "Test detail 2.")
        self.room.add_detail("window", "Window detail.")
        self.room.add_detail("window pane", "Window Pane detail.")

        self.assertEqual(self.room.get_detail("test"), "Test detail.")
        self.assertEqual(self.room.get_detail("test2"), "Test detail 2.")
        self.assertEqual(self.room.get_detail("window"), "Window detail.")
        self.assertEqual(self.room.get_detail("window pane"), "Window Pane detail.")
        self.assertEqual(self.room.get_detail("win"), "Window detail.")
        self.assertEqual(self.room.get_detail("window p"), "Window Pane detail.")

        self.room.remove_detail("test")
        self.assertEqual(self.room.get_detail("test"), "Test detail 2.")  # finding nearest
        self.room.remove_detail("test2")
        self.assertEqual(self.room.get_detail("test"), None)  # all test* gone

class TestExtendedRoomCommands(BaseEvenniaCommandTest):
    """
    Test the ExtendedRoom commands.

    """

        self.room1.swap_typeclass("evennia.contrib.grid.extended_room.ExtendedRoom")
        self.room1.desc = self.base_room_desc

    def test_cmd_desc(self, mock_gametime):
        """Test new desc command"""

        mock_gametime.return_value = _get_timestamp("autumn", "afternoon")

        # view base desc
            extended_room.CmdExtendedRoomDesc(),
            "",
            f"""
Room Room(#{self.room1.id}) Season: autumn. Time: afternoon. States: None

Room state (default) (active):
Base room description.
                  """.strip(),
        )

        # add spring desc
            "/spring Spring description.",
            "The spring-description was set on Room",
        )
            "/burning Burning description.",
            "The burning-description was set on Room",
        )

            "",
            f"""

Room state burning:
Burning description.

Room state spring:
Spring description.

                 """.strip(),
        )

        # remove a desc
            "/del/burning/spring",
            "The burning-description was deleted, if it existed.|The spring-description was"
            " deleted, if it existed",
        )
        # add autumn, which should be active
            "/autumn Autumn description.",
            "The autumn-description was set on Room",
        )
            "",
            f"""

Room state autumn (active):
Autumn description.

Room state (default):
        )

    def test_cmd_detail(self):
        """Test adding details"""
            extended_room.CmdExtendedRoomDetail(),
            "test=Test detail.",
            "Set detail 'test': 'Test detail.'",
        )

            "",
            """
Details on Room:
test: Test detail.
            """.strip(),
        )

        # remove a detail
            "/del test",
            "Deleted detail 'test', if it existed.",
        )

            "",
            f"""
The room Room(#{self.room1.id}) doesn't have any details.
        )

    def test_cmd_roomstate(self, mock_gametime):
        """
        Test the roomstate command

        """

        # show existing room states (season/time doesn't count)

        self.assertEqual(self.room1.room_states, [])

            extended_room.CmdExtendedRoomState(),
            "",
            f"Room states (not counting automatic time/season) on Room(#{self.room1.id}):\n None",
        )

        # add room states
            "burning",
            "Added room state 'burning' to this room.",
        )
            "windy",
            "Added room state 'windy' to this room.",
        )
            "",
            f"Room states (not counting automatic time/season) on Room(#{self.room1.id}):\n "
            "'burning' and 'windy'",
        )
        # toggle windy
            "windy",
            "Cleared room state 'windy' from this room.",
        )
            "",
            "'burning'",
        )
        # add a autumn state and make sure we override it
        self.room1.add_desc("Autumn description.", room_state="autumn")
        self.room1.add_desc("Spring description.", room_state="spring")

        self.assertEqual(self.room1.get_stateful_desc(), "Autumn description.")
            "spring",
            "Added room state 'spring' to this room.",
        )
        self.assertEqual(self.room1.get_stateful_desc(), "Spring description.")

    def test_cmd_roomtime(self, mock_gametime):
        """
        Test the time command
        """

            extended_room.CmdExtendedRoomGameTime(), "", "It's an autumn day, in the afternoon."
        )

    def test_cmd_look(self, mock_gametime):
        """
        Test the look command.
        """

        autumn_desc = (
            "This is a nice autumnal forest."
            "$state(morning,|_The morning sun is just rising)"
            "$state(afternoon,|_The afternoon sun is shining through the trees)"
            "$state(burning,|_and this place is on fire!)"
            "$state(afternoon, .)"
            "$state(flooded, and it's raining heavily!)"
        )
        self.room1.add_desc(autumn_desc, room_state="autumn")

            extended_room.CmdExtendedRoomLook(),
            "",
            f"Room(#{self.room1.id})\nThis is a nice autumnal forest.",
        )
            "",
            f"Room(#{self.room1.id})\nThis is a nice autumnal forest. The afternoon sun is"
            " shining through the trees.",
        )
        self.room1.add_room_state("burning")
            "",
            " shining through the trees and this place is on fire!",
        )

"""
Basic Map - helpme 2022

This adds an ascii `map` to a given room which can be viewed with the `map` command.
You can easily alter it to add special characters, room colors etc. The map shown is
dynamically generated on use, and supports all compass directions and up/down. Other
directions are ignored.

If you don't expect the map to be updated frequently, you could choose to save the
calculated map as a .ndb value on the room and render that instead of running mapping
calculations anew each time.

An example map:
```
       |
     -[-]-
       |
       |
-[-]--[-]--[-]--[-]
  |    |    |    |
       |    |    |
     -[-]--[-]  [-]
       | \/ |    |
     \ | /\ |
     -[-]--[-]
```

Adding the `MapDisplayCmdSet` to the default character cmdset will add the `map` command.

Specifically, in `mygame/commands/default_cmdsets.py`:

```
...
from evennia.contrib.grid.ingame_map_display import MapDisplayCmdSet  # <---

    ...
        ...
        self.add(MapDisplayCmdSet)  # <---

```

Then `reload` to make the new commands available.

Additional Settings:

In order to change your default map size, you can add to `mygame/server/settings.py`:

BASIC_MAP_SIZE = 5

This changes the default map width/height. 2-5 for most clients is sensible.

If you don't want the player to be able to specify the size of the map, ignore any
arguments passed into the Map command.
"""

from evennia import CmdSet

_BASIC_MAP_SIZE = settings.BASIC_MAP_SIZE if hasattr(settings, "BASIC_MAP_SIZE") else 2
_MAX_MAP_SIZE = settings.BASIC_MAP_SIZE if hasattr(settings, "MAX_MAP_SIZE") else 10

# _COMPASS_DIRECTIONS specifies which way to move the pointer on the x/y axes and what characters to use to depict the exits on the map.
_COMPASS_DIRECTIONS = {
    "north": (0, -3, " | "),
    "south": (0, 3, " | "),
    "east": (3, 0, "-"),
    "west": (-3, 0, "-"),
    "northeast": (3, -3, "/"),
    "northwest": (-3, -3, "\\"),
    "southeast": (3, 3, "\\"),
    "southwest": (-3, 3, "/"),
    "up": (0, 0, "^"),
    "down": (0, 0, "v"),
}

class Map(object):
    def __init__(self, caller, size=_BASIC_MAP_SIZE, location=None):
        """
        Initializes the map.

        Args:
            caller (object): Any object, though generally a puppeted character.
            size (int): The seed size of the map, which will be multiplied to get the final grid size.
            location (object): The location at the map's center (will default to caller.location if none provided).
        """
        self.start_time = time.time()
        self.max_width = int(size * 2 + 1) * 5  # This must be an odd number
        self.max_length = int(size * 2 + 1) * 3  # This must be an odd number
        self.has_mapped = {}
        self.curX = None
        self.curY = None
        self.size = size
        self.location = location or caller.location

    def create_grid(self):
        """
        Create the empty grid for the map based on the configured size

        Returns:
            list: The created grid, a list of lists.
        """
        board = []
        for row in range(self.max_length):
            board.append([])
            for column in range(int(self.max_width / 5)):
                board[row].extend([" ", "   ", " "])
        return board

    def exit_name_as_ordinal(self, ex):
        """
        Get the exit name as a compass direction if possible

        Args:
            ex (Exit): The current exit being mapped.
        Returns:
            string: The exit name as a compass direction or an empty string.
        """
        exit_name = ex.name
        if exit_name not in _COMPASS_DIRECTIONS:
            compass_aliases = [
                direction in ex.aliases.all() for direction in _COMPASS_DIRECTIONS.keys()
            ]
            if compass_aliases[0]:
                exit_name = compass_aliases[0]
            if exit_name not in _COMPASS_DIRECTIONS:
                return ""
        return exit_name

    def update_pos(self, room, exit_name):
        """
        Update the position pointer.

        Args:
            room (Room): The current location.
            exit_name (str): The name of the exit to to use in this room. This must
                be a valid compass direction, or an error will be raised.
        Raises:
            KeyError: If providing a non-compass exit name.
        """
        # Update the pointer
        self.curX, self.curY = self.has_mapped[room][0], self.has_mapped[room][1]

        # Move the pointer depending on which direction the exit lies
        # exit_name has already been validated as an ordinal direction at this point
        self.curY += _COMPASS_DIRECTIONS[exit_name][0]
        self.curX += _COMPASS_DIRECTIONS[exit_name][1]

    def has_drawn(self, room):
        """
        Checks if the given room has already been drawn or not

        Args:
            room (Room): Room to check.
        Returns:
            bool: Whether or not the room has been drawn.
        """
        return True if room in self.has_mapped.keys() else False

    def draw_room_on_map(self, room, max_distance):
        """
        Draw the room and its exits on the map recursively

        Args:
            room (Room): The room to draw out.
            max_distance (int): How extensive the map is.
        """
        self.draw(room)
        self.draw_exits(room)

        if max_distance == 0:
            return

        # Check if the caller has access to the room in question. If not, don't draw it.
        # Additionally, if the name of the exit is not ordinal but an alias of it is, use that.
        for ex in [x for x in room.exits if x.access(self.caller, "traverse")]:
            ex_name = self.exit_name_as_ordinal(ex)
            if not ex_name or ex_name in ["up", "down"]:
                continue
            if self.has_drawn(ex.destination):
                continue

            self.update_pos(room, ex_name.lower())
            self.draw_room_on_map(ex.destination, max_distance - 1)

    def draw_exits(self, room):
        """
        Draw a given room's exit paths

        Args:
            room (Room): The room to draw exits of.
        """
        x, y = self.curX, self.curY
        for ex in room.exits:
            if not ex_name:
                continue

            ex_character = _COMPASS_DIRECTIONS[ex_name][2]
            delta_x = int(_COMPASS_DIRECTIONS[ex_name][1] / 3)
            delta_y = int(_COMPASS_DIRECTIONS[ex_name][0] / 3)

            # Make modifications if the exit has BOTH up and down exits
            if ex_name == "up":
                if "v" in self.grid[x][y]:
                    self.render_room(room, x, y, p1="^", p2="v")
                else:
                    self.render_room(room, x, y, here="^")
            elif ex_name == "down":
                if "^" in self.grid[x][y]:
                else:
                    self.render_room(room, x, y, here="v")
            else:
                self.grid[x + delta_x][y + delta_y] = ex_character

    def draw(self, room):
        """
        Draw the map starting from a given room and add it to the cache of mapped rooms

        Args:
            room (Room): The room to render.
        """
        # draw initial caller location on map first!
        if room == self.location:
            self.start_loc_on_grid(room)
            self.has_mapped[room] = [self.curX, self.curY]
        else:
            # map all other rooms
            self.render_room(room, self.curX, self.curY)

    def render_room(self, room, x, y, p1="[", p2="]", here=None):
        """
        Draw a given room with ascii characters

        Args:
            x (int): The x-value of the room on the grid (horizontally, east/west).
            y (int): The y-value of the room on the grid (vertically, north/south).
            p1 (str): The first character of the 3-character room depiction.
            p2 (str): The last character of the 3-character room depiction.
            here (str): Defaults to none, a special character depicting the room.
        """
        # Note: This is where you would set colors, symbols etc.
        # Render the room
        you = list("[ ]")

        you[0] = f"{p1}|n"
        you[1] = f"{here if here else you[1]}"
        if room == self.caller.location:
            you[1] = "|[x|co|n"  # Highlight the location you are currently in
        you[2] = f"{p2}|n"

        self.grid[x][y] = "".join(you)

    def start_loc_on_grid(self, room):
        """
        Set the starting location on the grid based on the maximum width and length

        Args:
            room (Room): The room to begin with.
        """
        x = int((self.max_width * 0.6 - 1) / 2)
        y = int((self.max_length - 1) / 2)

        self.render_room(room, x, y)
        self.curX, self.curY = x, y

    def show_map(self, debug=False):
        """
        Create and show the map, piecing it all together in the end

        Args:
            debug (bool): Whether or not to return the time taken to build the map.
        """
        map_string = ""
        self.grid = self.create_grid()
        self.draw_room_on_map(self.location, self.size)

        for row in self.grid:
            map_row = "".join(row)
            if map_row.strip() != "":
                map_string += f"{map_row}\n"

        elapsed = time.time() - self.start_time
        if debug:
            map_string += f"\nTook {elapsed}ms to render the map.\n"

        return "%s" % map_string

class CmdMap(MuxCommand):
    """
    Check the local map around you.

    Usage: map (optional size)
    """

    key = "map"

        size = _BASIC_MAP_SIZE
        max_size = _MAX_MAP_SIZE
        if self.args.isnumeric():
            size = min(max_size, int(self.args))

        # You can run show_map(debug=True) to see how long it takes.
        map_here = Map(self.caller, size=size).show_map()
        self.caller.msg((map_here, {"type": "map"}))

class MapDisplayCmdSet(CmdSet):
    """
    The map command.
    """

        self.add(CmdMap)

"""
SimpleDoor

Contribution - Griatch 2016

A simple two-way exit that represents a door that can be opened and
closed. Can easily be expanded from to make it lockable, destroyable
etc.  Note that the simpledoor is based on Evennia locks, so it will
not work for a superuser (which bypasses all locks) - the superuser
will always appear to be able to close/open the door over and over
without the locks stopping you. To use the door, use `@quell` or a
non-superuser account.

Import this module in mygame/commands/default_cmdsets and add
the SimpleDoorCmdSet to your CharacterCmdSet:

```python

from evennia.contrib.grid import simpledoor  <---

    # ...
        # ...
        self.add(simpledoor.SimpleDoorCmdSet)

```

Usage:

To try it out, `dig` a new room and then use the (overloaded) `@open`
commmand to open a new doorway to it like this:

    @open doorway:contrib.grid.simpledoor.SimpleDoor = otherroom

You can then use `open doorway' and `close doorway` to change the open
state. If you are not superuser (`@quell` yourself) you'll find you
cannot pass through either side of the door once it's closed from the
other side.

"""

from evennia import DefaultExit, default_cmds

class SimpleDoor(DefaultExit):
    """
    A two-way exit "door" with some methods for affecting both "sides"
    of the door at the same time. For example, set a lock on either of the two
    sides using `exitname.setlock("traverse:false())`

    """

        """
        Called the very first time the door is created.

        """
        self.db.return_exit = None

    def setlock(self, lockstring):
        """
        Sets identical locks on both sides of the door.

        Args:
            lockstring (str): A lockstring, like `"traverse:true()"`.

        """
        self.db.return_exit.locks.add(lockstring)

    def setdesc(self, description):
        """
        Sets identical descs on both sides of the door.

        Args:
            setdesc (str): A description.

        """
        self.db.desc = description
        self.db.return_exit.db.desc = description

        """
        Deletes both sides of the door.

        """
        # we have to be careful to avoid a delete-loop.
        if self.db.return_exit:
            super().delete()

    def at_failed_traverse(self, traversing_object):
        """
        Called when door traverse: lock fails.

        Args:
            traversing_object (Typeclassed entity): The object
                attempting the traversal.

        """
        traversing_object.msg("%s is closed." % self.key)

class CmdOpen(default_cmds.CmdOpen):
    __doc__ = default_cmds.CmdOpen.__doc__
    # overloading parts of the default CmdOpen command to support doors.

        """
        Simple wrapper for the default CmdOpen.create_exit
        """
        # create a new exit as normal
        new_exit = super().create_exit(
            exit_name, location, destination, exit_aliases=exit_aliases, typeclass=typeclass
        )
        if hasattr(self, "return_exit_already_created"):
            # we don't create a return exit if it was already created (because
            # we created a door)
            del self.return_exit_already_created
            return new_exit
        if inherits_from(new_exit, SimpleDoor):
            # a door - create its counterpart and make sure to turn off the default
            # return-exit creation of CmdOpen
                "Note: A door-type exit was created - ignored eventual custom return-exit type."
            )
            self.return_exit_already_created = True
            back_exit = self.create_exit(
                exit_name, destination, location, exit_aliases=exit_aliases, typeclass=typeclass
            )
            new_exit.db.return_exit = back_exit
            back_exit.db.return_exit = new_exit
        return new_exit

# A simple example of a command making use of the door exit class'
# functionality. One could easily expand it with functionality to
# operate on other types of open-able objects as needed.

class CmdOpenCloseDoor(default_cmds.MuxCommand):
    """
    Open and close a door

    Usage:
        open <door>
        close <door>

    """

    aliases = ["close"]

        "implement the door functionality"
            self.caller.msg("Usage: open||close <door>")
            return

        door = self.caller.search(self.args)
        if not door:
            return
        if not inherits_from(door, SimpleDoor):
            self.caller.msg("This is not a door.")
            return

        if self.cmdstring == "open":
            if door.locks.check(self.caller, "traverse"):
                self.caller.msg("%s is already open." % door.key)
            else:
                door.setlock("traverse:true()")
                self.caller.msg("You open %s." % door.key)
        else:  # close
            if not door.locks.check(self.caller, "traverse"):
                self.caller.msg("%s is already closed." % door.key)
            else:
                door.setlock("traverse:false()")
                self.caller.msg("You close %s." % door.key)

class SimpleDoorCmdSet(CmdSet):
        self.add(CmdOpen())
        self.add(CmdOpenCloseDoor())

"""
Tests of simpledoor.

"""

from . import simpledoor

class TestSimpleDoor(BaseEvenniaCommandTest):
    def test_cmdopen(self):
            simpledoor.CmdOpen(),
            "newdoor;door:contrib.grid.simpledoor.SimpleDoor,backdoor;door = Room2",
            "Created new Exit 'newdoor' from Room to Room2 (aliases: door).|Note: A door-type exit was "
            "created - ignored eventual custom return-exit type.|Created new Exit 'newdoor' from Room2 to Room (aliases: door).",
        )
        self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You close newdoor.", cmdstring="close")
            simpledoor.CmdOpenCloseDoor(),
            "newdoor",
            "newdoor is already closed.",
            cmdstring="close",
        )
        self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You open newdoor.", cmdstring="open")
            simpledoor.CmdOpenCloseDoor(), "newdoor", "newdoor is already open.", cmdstring="open"
        )

"""
Slow Exit typeclass

Contribution - Griatch 2014

This is an example of an Exit-type that delays its traversal. This
simulates slow movement, common in many different types of games. The
contrib also contains two commands, `CmdSetSpeed` and CmdStop for changing
the movement speed and abort an ongoing traversal, respectively.

## Installation:

To try out an exit of this type, you could connect two existing rooms
using something like this:

@open north:contrib.grid.slow_exit.SlowExit = <destination>

To make this your new default exit, modify `mygame/typeclasses/exits.py`
to import this module and change the default `Exit` class to inherit
from `SlowExit` instead.

```
# in mygame/typeclasses/exits.py

from evennia.contrib.grid.slowexit import SlowExit

class Exit(SlowExit):
    # ...

```

To get the ability to change your speed and abort your movement, import

```python

from evennia.contrib.grid import slow_exit  <---

    # ...
        # ...
        self.add(slow_exit.SlowDoorCmdSet)  <---

```

## Notes:

This implementation is efficient but not persistent; so incomplete
movement will be lost in a server reload. This is acceptable for most
game types - to simulate longer travel times (more than the couple of
seconds assumed here), a more persistent variant using Scripts or the
TickerHandler might be better.

"""

from evennia.objects.objects import DefaultExit

MOVE_DELAY = {"stroll": 6, "walk": 4, "run": 2, "sprint": 1}

class SlowExit(DefaultExit):
    """
    This overloads the way moving happens.
    """

    def at_traverse(self, traversing_object, target_location):
        """
        Implements the actual traversal, using utils.delay to delay the move_to.
        """

        # if the traverser has an Attribute move_speed, use that,
        # otherwise default to "walk" speed
        move_speed = traversing_object.db.move_speed or "walk"
        move_delay = MOVE_DELAY.get(move_speed, 4)

        def move_callback():
            "This callback will be called by utils.delay after move_delay seconds."
            source_location = traversing_object.location
            if traversing_object.move_to(target_location, move_type="traverse"):
                self.at_post_traverse(traversing_object, source_location)
            else:
                if self.db.err_traverse:
                    # if exit has a better error message, let's use it.
                    self.caller.msg(self.db.err_traverse)
                else:
                    # No shorthand error message. Call hook.
                    self.at_failed_traverse(traversing_object)

        traversing_object.msg("You start moving %s at a %s." % (self.key, move_speed))
        # create a delayed movement
        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 = t

#
# set speed - command
#

SPEED_DESCS = {"stroll": "strolling", "walk": "walking", "run": "running", "sprint": "sprinting"}

class CmdSetSpeed(Command):
    """
    set your movement speed

    Usage:
      setspeed stroll|walk|run|sprint

    This will set your movement speed, determining how long time
    it takes to traverse exits. If no speed is set, 'walk' speed
    is assumed.
    """

    key = "setspeed"

        """
        Simply sets an Attribute used by the SlowExit above.
        """
        speed = self.args.lower().strip()
        if speed not in SPEED_DESCS:
            self.caller.msg("Usage: setspeed stroll||walk||run||sprint")
        elif self.caller.db.move_speed == speed:
            self.caller.msg("You are already %s." % SPEED_DESCS[speed])
        else:
            self.caller.db.move_speed = speed
            self.caller.msg("You are now %s." % SPEED_DESCS[speed])

#
# stop moving - command
#

class CmdStop(Command):
    """
    stop moving

    Usage:
      stop

    Stops the current movement, if any.
    """

    key = "stop"

        """
        This is a very simple command, using the
        stored deferred from the exit traversal above.
        """
        currently_moving = self.caller.ndb.currently_moving
        if currently_moving and not currently_moving.called:
            currently_moving.cancel()
            self.caller.msg("You stop moving.")
            for observer in self.caller.location.contents_get(self.caller):
                observer.msg("%s stops." % self.caller.get_display_name(observer))
        else:
            self.caller.msg("You are not moving.")

class SlowExitCmdSet(CmdSet):
        self.add(CmdSetSpeed())
        self.add(CmdStop())

"""
Slow exit tests.

"""

from . import slow_exit

slow_exit.MOVE_DELAY = {"stroll": 0, "walk": 0, "run": 0, "sprint": 0}

def _cancellable_mockdelay(time, callback, *args, **kwargs):
    callback(*args, **kwargs)
    return Mock()

class TestSlowExit(BaseEvenniaCommandTest):
    @patch("evennia.utils.delay", _cancellable_mockdelay)
        exi = create_object(
            slow_exit.SlowExit, key="slowexit", location=self.room1, destination=self.room2
        )
        exi.at_traverse(self.char1, self.room2)
        self.call(slow_exit.CmdSetSpeed(), "walk", "You are now walking.")
        self.call(slow_exit.CmdStop(), "", "You stop moving.")

"""
Test wilderness

"""

from evennia import DefaultCharacter

from . import wilderness

class TestWilderness(BaseEvenniaTest):
        self.char1 = create_object(DefaultCharacter, key="char1")
        self.char2 = create_object(DefaultCharacter, key="char2")

    def get_wilderness_script(self, name="default"):
        w = wilderness.WildernessScript.objects.get("default")
        return w

    def test_create_wilderness_default_name(self):
        wilderness.create_wilderness()
        w = self.get_wilderness_script()
        self.assertIsNotNone(w)

    def test_create_wilderness_custom_name(self):
        name = "customname"
        wilderness.create_wilderness(name)
        w = self.get_wilderness_script(name)

    def test_enter_wilderness(self):
        wilderness.enter_wilderness(self.char1)
        self.assertIsInstance(self.char1.location, wilderness.WildernessRoom)
        self.assertEqual(w.itemcoordinates[self.char1], (0, 0))

    def test_enter_wilderness_custom_coordinates(self):
        wilderness.enter_wilderness(self.char1, coordinates=(1, 2))
        self.assertEqual(w.itemcoordinates[self.char1], (1, 2))

    def test_enter_wilderness_custom_name(self):
        name = "customnname"
        wilderness.enter_wilderness(self.char1, name=name)

    def test_wilderness_correct_exits(self):

        # By default we enter at a corner (0, 0), so only a few exits should
        # be visible / traversable
        exits = [
            i
            for i in self.char1.location.contents
            if i.destination and (i.access(self.char1, "view") or i.access(self.char1, "traverse"))
        ]

        self.assertEqual(len(exits), 3)
        exitsok = ["north", "northeast", "east"]
        for each_exit in exitsok:
            self.assertTrue(any([e for e in exits if e.key == each_exit]))

        # If we move to another location not on an edge, then all directions
        # should be visible / traversable
        wilderness.enter_wilderness(self.char1, coordinates=(1, 1))
        exits = [
            i
        ]
        self.assertEqual(len(exits), 8)
        exitsok = [
            "north",
            "northeast",
            "east",
            "southeast",
            "south",
            "southwest",
            "west",
            "northwest",
        ]

    def test_room_creation(self):
        # Pretend that both char1 and char2 are connected...
        self.char1.sessions.add(1)
        self.char2.sessions.add(1)
        self.assertTrue(self.char1.has_account)
        self.assertTrue(self.char2.has_account)

        # We should have no unused room after moving the first account in.
        self.assertEqual(len(w.db.unused_rooms), 0)
        w.move_obj(self.char1, (0, 0))

        # And also no unused room after moving the second one in.
        w.move_obj(self.char2, (1, 1))

        # But if char2 moves into char1's room, we should have one unused room
        # Which should be char2's old room that got created.
        w.move_obj(self.char2, (0, 0))
        self.assertEqual(len(w.db.unused_rooms), 1)
        self.assertEqual(self.char1.location, self.char2.location)

        # And if char2 moves back out, that unused room should be put back to
        # use again.
        self.assertNotEqual(self.char1.location, self.char2.location)

    def test_get_new_coordinates(self):
        loc = (1, 1)
        directions = {
            "north": (1, 2),
            "northeast": (2, 2),
            "east": (2, 1),
            "southeast": (2, 0),
            "south": (1, 0),
            "southwest": (0, 0),
            "west": (0, 1),
            "northwest": (0, 2),
        }
        for direction, correct_loc in directions.items():
            new_loc = wilderness.get_new_coordinates(loc, direction)
            self.assertEqual(new_loc, correct_loc, direction)

    def test_preserve_items(self):

        # move char and obj to wilderness
        wilderness.enter_wilderness(self.obj1)

        # move to a new room
        w.move_obj(self.char1, (1, 1))
        # the room should be remapped and 0,0 should not exist
        self.assertTrue((0, 0) not in w.db.rooms)
        self.assertEqual(1, len(w.db.rooms))
        # verify obj1 moved to None
        self.assertIsNone(self.obj1.location)

        # now change to preserve items
        w.preserve_items = True
        wilderness.enter_wilderness(self.obj1, (1, 1))
        # move the character again
        w.move_obj(self.char1, (0, 1))
        # check that the previous room was preserved
        self.assertIn((1, 1), w.db.rooms)
        self.assertEqual(2, len(w.db.rooms))
        # and verify that obj1 is still at 1,1
        self.assertEqual(self.obj1.location, w.db.rooms[(1, 1)])

"""
Wilderness system

Evennia contrib - titeuf87 2017

This contrib provides a wilderness map. This is an area that can be huge where
the rooms are mostly similar, except for some small cosmetic changes like the
room name.

## Usage

This contrib does not provide any new commands. Instead the default `py` command
is used.

A wilderness map needs to created first. There can be different maps, all
with their own name. If no name is provided, then a default one is used. Internally,
the wilderness is stored as a Script with the name you specify. If you don't
specify the name, a script named "default" will be created and used.

    py from evennia.contrib.grid import wilderness; wilderness.create_wilderness()

Once created, it is possible to move into that wilderness map:

    py from evennia.contrib.grid import wilderness; wilderness.enter_wilderness(me)

All coordinates used by the wilderness map are in the format of `(x, y)`
tuples. x goes from left to right and y goes from bottom to top. So `(0, 0)`
is the bottom left corner of the map.

## Customisation

The defaults, while useable, are meant to be customised. When creating a
new wilderness map it is possible to give a "map provider": this is a
python object that is smart enough to create the map.

The default provider, `WildernessMapProvider`, creates a grid area that
is unlimited in size.

`WildernessMapProvider` can be subclassed to create more interesting maps
and also to customize the room/exit typeclass used.

There is also no command that allows players to enter the wilderness. This
still needs to be added: it can be a command or an exit, depending on your
needs.

## Example

    To give an example of how to customize, we will create a very simple (and
    small) wilderness map that is shaped like a pyramid. The map will be
    provided as a string: a "." symbol is a location we can walk on.

    Let's create a file world/pyramid.py:

```python
map_str = '''
     .
    ...
   .....
  .......
'''

from evennia.contrib.grid import wilderness

class PyramidMapProvider(wilderness.WildernessMapProvider):

    def is_valid_coordinates(self, wilderness, coordinates):
        "Validates if these coordinates are inside the map"
        x, y = coordinates
        try:
            lines = map_str.split("\n")
            # The reverse is needed because otherwise the pyramid will be
            # upside down
            lines.reverse()
            line = lines[y]
            column = line[x]
            return column == "."

    def get_location_name(self, coordinates):
        "Set the location name"
        if y == 3:
            return "Atop the pyramid."
        else:
            return "Inside a pyramid."

    def at_prepare_room(self, coordinates, caller, room):
        "Any other changes done to the room before showing it"
        desc = "This is a room in the pyramid."
        if y == 3 :
            desc = "You can see far and wide from the top of the pyramid."
        room.ndb.active_desc = desc
```

Note that the currently active description is stored as `.ndb.active_desc`. When
looking at the room, this is what will be pulled and shown.

> Exits on a room are always present, but locks hide those not used for a
> location. So make sure to `quell` if you are a superuser (since the superuser ignores
> locks, those exits will otherwise not be hidden)

Now we can use our new pyramid-shaped wilderness map. From inside Evennia we
create a new wilderness (with the name "default") but using our new map provider:

    py from world import pyramid as p; p.wilderness.create_wilderness(mapprovider=p.PyramidMapProvider())
    py from evennia.contrib import wilderness; wilderness.enter_wilderness(me, coordinates=(4, 1))

## Implementation details

    When a character moves into the wilderness, they get their own room. If
    they move, instead of moving the character, the room changes to match the
    new coordinates.

    If a character meets another character in the wilderness, then their room
    merges. When one of the character leaves again, they each get their own
    separate rooms.

    Rooms are created as needed. Unneeded rooms are stored away to avoid the
    overhead cost of creating new rooms again in the future.

"""

    create_object,
)
from evennia.utils import inherits_from

def create_wilderness(name="default", mapprovider=None, preserve_items=False):
    """
    Creates a new wilderness map. Does nothing if a wilderness map already
    exists with the same name.

    Args:
        name (str, optional): the name to use for that wilderness map
        mapprovider (WildernessMap instance, optional): an instance of a
            WildernessMap class (or subclass) that will be used to provide the
            layout of this wilderness map. If none is provided, the default
            infinite grid map will be used.

    """
    if WildernessScript.objects.filter(db_key=name).exists():
        # Don't create two wildernesses with the same name
        return

    if not mapprovider:
        mapprovider = WildernessMapProvider()
    script = create_script(WildernessScript, key=name)
    script.db.mapprovider = mapprovider
    if preserve_items:
        script.preserve_items = True

def enter_wilderness(obj, coordinates=(0, 0), name="default"):
    """
    Moves obj into the wilderness. The wilderness needs to exist first and the
    provided coordinates needs to be valid inside that wilderness.

    Args:
        obj (object): the object to move into the wilderness
        coordinates (tuple), optional): the coordinates to move obj to into
            the wilderness. If not provided, defaults (0, 0)
        name (str, optional): name of the wilderness map, if not using the
            default one

    Returns:
        bool: True if obj successfully moved into the wilderness.
    """
    script = WildernessScript.objects.filter(db_key=name)
    if not script.exists():
    else:
        script = script[0]

    if script.is_valid_coordinates(coordinates):
        script.move_obj(obj, coordinates)
    else:

def get_new_coordinates(coordinates, direction):
    """
    Returns the coordinates of direction applied to the provided coordinates.

    Args:
        coordinates: tuple of (x, y)
        direction: a direction string (like "northeast")

    Returns:
        tuple: tuple of (x, y) coordinates
    """
    x, y = coordinates

    if direction in ("north", "northwest", "northeast"):
        y += 1
    if direction in ("south", "southwest", "southeast"):
        y -= 1
    if direction in ("northwest", "west", "southwest"):
        x -= 1
    if direction in ("northeast", "east", "southeast"):
        x += 1

    return (x, y)

class WildernessScript(DefaultScript):
    """
    This is the main "handler" for the wilderness system: inside here the
    coordinates of every item currently inside the wilderness is stored. This
    script is responsible for creating rooms as needed and storing rooms away
    into storage when they are not needed anymore.
    """

    # Stores the MapProvider class
    mapprovider = AttributeProperty()

    # Stores a dictionary of items on the map with their coordinates
    # The key is the item, the value are the coordinates as (x, y) tuple.
    itemcoordinates = AttributeProperty()

    # Determines whether or not rooms are recycled despite containing non-player objects
    # True means that leaving behind a non-player object will prevent the room from being recycled
    # in order to preserve the object
    preserve_items = AttributeProperty(default=False)

        """
        Only called once, when the script is created. This is a default Evennia
        hook.
        """

        # Store the coordinates of every item that is inside the wilderness
        # Key: object, Value: (x, y)
        self.db.itemcoordinates = {}

        # Store the rooms that are used as views into the wilderness
        # Key: (x, y), Value: room object
        self.db.rooms = {}

        # Created rooms that are not needed anymore are stored there. This
        # allows quick retrieval if a new room is needed without having to
        # create it.
        self.db.unused_rooms = []

        """
        Called after the server is started or reloaded.
        """
        for coordinates, room in self.db.rooms.items():
            room.ndb.wildernessscript = self
            room.ndb.active_coordinates = coordinates
        for item in self.db.itemcoordinates.keys():
            # Items deleted while in the wilderness can leave None-type 'ghosts'
            # These need to be cleaned up
            if item is None:
                del self.db.itemcoordinates[item]
                continue
            item.ndb.wilderness = self

    def is_valid_coordinates(self, coordinates):
        """
        Returns True if coordinates are valid (and can be travelled to).
        Otherwise returns False

        Args:
            coordinates (tuple): coordinates as (x, y) tuple

        Returns:
            bool: True if the coordinates are valid
        """
        return self.mapprovider.is_valid_coordinates(self, coordinates)

    def get_obj_coordinates(self, obj):
        """
        Returns the coordinates of obj in the wilderness.

        Returns (x, y)

        Args:
            obj (object): an object inside the wilderness

        Returns:
            tuple: (x, y) tuple of where obj is located
        """
        return self.itemcoordinates[obj]

    def get_objs_at_coordinates(self, coordinates):
        """
        Returns a list of every object at certain coordinates.

        Imeplementation detail: this uses a naive iteration through every
        object inside the wilderness which could cause slow downs when there
        are a lot of objects in the map.

        Args:
            coordinates (tuple): a coordinate tuple like (x, y)

        Returns:
            [Object, ]: list of Objects at coordinates
        """
        result = [
            item
            for item, item_coords in self.itemcoordinates.items()
            if item_coords == coordinates and item is not None
        ]
        return list(result)

    def move_obj(self, obj, new_coordinates):
        """
        Moves obj to new coordinates in this wilderness.

        Args:
            obj (object): the object to move
            new_coordinates (tuple): tuple of (x, y) where to move obj to.
        """
        # Update the position of this obj in the wilderness
        self.itemcoordinates[obj] = new_coordinates
        old_room = obj.location

        # Remove the obj's location. This is needed so that the object does not
        # appear in its old room should that room be deleted.
        obj.location = None

        # By default, we'll assume we won't be making a new room and change this flag if necessary.
        create_room = False

        # See if we already have a room for that location
        if room := self.db.rooms.get(new_coordinates):
            # There is. Try to destroy the old_room if it is not needed anymore
            self._destroy_room(old_room)
        else:
            # There is no room yet at new_location
            # Is the old room in a wilderness?
            if hasattr(old_room, "wilderness"):
                # Yes. Is it in THIS wilderness?
                if old_room.wilderness == self:
                    # Should we preserve rooms with any objects?
                    if self.preserve_items:
                        # Yes - check if ANY objects besides the exits are in old_room
                        if len(
                            [
                                ob
                                for ob in old_room.contents
                                if not inherits_from(ob, WildernessExit)
                            ]
                        ):
                            # There is, so we'll create a new room
                            room = self._create_room(new_coordinates, obj)
                        else:
                            # The room is empty, so we'll reuse it
                            room = old_room
                    else:
                        # Only preserve rooms if there are players behind
                        if len([ob for ob in old_room.contents if ob.has_account]):
                            # There is still a player there; create a new room
                        else:
                            # The room is empty of players, so we'll reuse it

                # It's in a different wilderness
                else:
                    # It does, so we make sure to leave the other wilderness properly
                    old_room.wilderness.at_post_object_leave(obj)
                    # We'll also need to create a new room in this wilderness
                    room = self._create_room(new_coordinates, obj)

            else:
                # Obj comes from outside the wilderness entirely
                # We need to make a new room
                room = self._create_room(new_coordinates, obj)

            # Set `room` to the new coordinates, however it was made
            room.set_active_coordinates(new_coordinates, obj)

        # Put obj back, now in the correct room
        obj.location = room
        obj.ndb.wilderness = self

    def _create_room(self, coordinates, report_to):
        """
        Gets a new WildernessRoom to be used for the provided coordinates.

        It first tries to retrieve a room out of storage. If there are no rooms
        left a new one will be created.

        Args:
            coordinates (tuple): coordinate tuple of (x, y)
            report_to (object): the obj to return error messages to
        """
        if self.db.unused_rooms:
            # There is still unused rooms stored in storage, let's get one of
            # those
            room = self.db.unused_rooms.pop()
        else:
            # No more unused rooms...time to make a new one.

            # First, create the room
            room = create_object(
                typeclass=self.mapprovider.room_typeclass, key="Wilderness", report_to=report_to
            )

            # Then the exits
            exits = [
                ("north", "n"),
                ("northeast", "ne"),
                ("east", "e"),
                ("southeast", "se"),
                ("south", "s"),
                ("southwest", "sw"),
                ("west", "w"),
                ("northwest", "nw"),
            ]
            for key, alias in exits:
                    typeclass=self.mapprovider.exit_typeclass,
                    key=key,
                    aliases=[alias],
                    location=room,
                    destination=room,
                    report_to=report_to,
                )

        room.ndb.active_coordinates = coordinates
        room.ndb.wildernessscript = self
        self.db.rooms[coordinates] = room

        return room

    def _destroy_room(self, room):
        """
        Moves a room back to storage. If room is not a WildernessRoom or there
        is something left inside the room, then this does nothing.

        Implementation note: If `preserve_items` is False (the default) then any
        objects left in the rooms will be moved to None. You may want to implement
        your own cleanup or recycling routine for these objects.

        Args:
            room (WildernessRoom): the room to put in storage
        """
        if not room or not inherits_from(room, WildernessRoom):
            return

        # Check the contents of the room before recycling
        for item in room.contents:
            if item.has_account:
                # There is still a player in this room, we can't delete it yet.
                return

            if not (item.destination and item.destination == room):
                # There is still a non-exit object in the room. Should we preserve it?
                if self.preserve_items:
                    # Yes, so we can't get rid of the room just yet
                    return

        # If we get here, the room can be recycled
        # Clear the location of any objects left in that room first
            if item.destination and item.destination == room:
                # Ignore the exits, they stay in the room
                continue
            item.location = None

        # Then delete its coordinate reference
        del self.db.rooms[room.ndb.active_coordinates]
        # And finally put this room away in storage
        self.db.unused_rooms.append(room)

    def at_post_object_leave(self, obj):
        """
        Called after an object left this wilderness map. Used for cleaning up.

        Args:
            obj (object): the object that left
        """
        # Try removing the object from the coordinates system
        if loc := self.db.itemcoordinates.pop(obj, None):
            # The object was removed successfully
            # Make sure there was a room at that location
            if room := self.db.rooms.get(loc):
                # If so, try to clean up the room
                self._destroy_room(room)

class WildernessRoom(DefaultRoom):
    """
    This is a single room inside the wilderness. This room provides a "view"
    into the wilderness map. When an account moves around, instead of going to
    another room as with traditional rooms, they stay in the same room but the
    room itself changes to display another area of the wilderness.
    """

    @property
    def wilderness(self):
        """
        Shortcut property to the wilderness script this room belongs to.

        Returns:
            WildernessScript: the WildernessScript attached to this room
        """
        return self.ndb.wildernessscript

    @property
    def location_name(self):
        """
        Returns the name of the wilderness at this room's coordinates.

        Returns:
            name (str)
        """
        return self.wilderness.mapprovider.get_location_name(self.coordinates)

    @property
    def coordinates(self):
        """
        Returns the coordinates of this room into the wilderness.

        Returns:
            tuple: (x, y) coordinates of where this room is inside the
                wilderness.
        """
        return self.ndb.active_coordinates

    def at_object_receive(self, moved_obj, source_location):
        """
        Called after an object has been moved into this object. This is a
        default Evennia hook.

        Args:
            moved_obj (Object): The object moved into this one.
            source_location (Object): Where `moved_obj` came from.
        """
        if isinstance(moved_obj, WildernessExit):
            # Ignore exits looping back to themselves: those are the regular
            # n, ne, ... exits.
            return

        itemcoords = self.wilderness.itemcoordinates
        if moved_obj in itemcoords:
            # This object was already in the wilderness. We need to make sure
            # it goes to the correct room it belongs to.
            coordinates = itemcoords[moved_obj]
            # Setting the location to None is important here so that we always
            # get a "fresh" room if it was in the wrong place
            moved_obj.location = None
            self.wilderness.move_obj(moved_obj, coordinates)
        else:
            # This object wasn't in the wilderness yet. Let's add it.
            itemcoords[moved_obj] = self.coordinates

        """
        Called just before an object leaves from inside this object. This is a

        Args:
            moved_obj (Object): The object leaving
            target_location (Object): Where `moved_obj` is going.

        """
        self.wilderness.at_post_object_leave(moved_obj)

    def set_active_coordinates(self, new_coordinates, obj):
        """
        Changes this room to show the wilderness map from other coordinates.

        Args:
            new_coordinates (tuple): coordinates as tuple of (x, y)
            obj (Object): the object that moved into this room and caused the
                coordinates to change
        """
        # Remove any reference for the old coordinates...
        rooms = self.wilderness.db.rooms
        if self.coordinates:
            del rooms[self.coordinates]
        # ...and add it for the new coordinates.
        self.ndb.active_coordinates = new_coordinates
        rooms[self.coordinates] = self

        # Any object inside this room will get its location set to None
        # unless it's a wilderness exit
        for item in self.contents:
            if not item.destination or item.destination != item.location:
                item.location = None
        # And every obj matching the new coordinates will get its location set
        # to this room
        for item in self.wilderness.get_objs_at_coordinates(new_coordinates):
            item.location = self

        # Fix the lockfuncs for the exit so we can't go where we're not
        # supposed to go
        for exit in self.exits:
            if exit.destination != self:
                continue
            x, y = get_new_coordinates(new_coordinates, exit.key)
            valid = self.wilderness.is_valid_coordinates((x, y))

            if valid:
                exit.locks.add("traverse:true();view:true()")
            else:
                exit.locks.add("traverse:false();view:false()")

        # Finally call the at_prepare_room hook to give a chance to further
        # customise it
        self.wilderness.mapprovider.at_prepare_room(new_coordinates, obj, self)

        """
        Displays the name of the object in a viewer-aware manner.
        This is a core evennia hook.

        Args:
            looker (TypedObject): The object or account that is looking
                at/getting inforamtion for this object.

        Returns:
            name (str): A string containing the name of the object,
                including the DBREF if this user is privileged to control
                said object and also its coordinates into the wilderness map.

        Notes:
            This function could be extended to change how object names
            appear to users in character, but be wary. This function
            does not change an object's keys or aliases when
            searching, and is expected to produce something useful for
            builders.
        """
        if self.locks.check_lockstring(looker, "perm(Builder)"):
            name = "{}(#{})".format(self.location_name, self.id)
        else:
            name = self.location_name

        name += " {0}".format(self.coordinates)
        return name

        """
        Displays the description of the room. This is a core evennia hook.

        Allows the room's description to be customized in an ndb value,
        avoiding having to write to the database on moving.
        """
        # Check if a new description was prepared by the map provider
        if self.ndb.active_desc:
            # There is one: use it
            return self.ndb.active_desc

        # Otherwise, use the normal description hook.
        return super().get_display_desc(looker, **kwargs)

class WildernessExit(DefaultExit):
    """
    This is an Exit object used inside a WildernessRoom. Instead of changing
    the location of an Object traversing through it (like a traditional exit
    would do) it changes the coordinates of that traversing Object inside
    the wilderness map.
    """

    @property
        """
        Shortcut property to the wilderness script.

        Returns:
            WildernessScript: the WildernessScript attached to this exit's room
        """
        return self.location.wilderness

    @property
    def mapprovider(self):
        """
        Shortcut property to the map provider.

        Returns:
            MapProvider object: the mapprovider object used with this
                wilderness map.
        """
        return self.wilderness.mapprovider

    def at_traverse_coordinates(self, traversing_object, current_coordinates, new_coordinates):
        """
        Called when an object wants to travel from one place inside the
        wilderness to another place inside the wilderness.

        If this returns True, then the traversing can happen. Otherwise it will
        be blocked.

        This method is similar how the `at_traverse` works on normal exits.

        Args:
            traversing_object (Object): The object doing the travelling.
            current_coordinates (tuple): (x, y) coordinates where
                `traversing_object` currently is.
            new_coordinates (tuple): (x, y) coordinates of where
                `traversing_object` wants to travel to.

        Returns:
            bool: True if traversing_object is allowed to traverse
        """

        """
        This implements the actual traversal. The traverse lock has
        already been checked (in the Exit command) at this point.

        Args:
            traversing_object (Object): Object traversing us.
            target_location (Object): Where target is going.

        Returns:
            bool: True if the traverse is allowed to happen

        """
        itemcoordinates = self.location.wilderness.db.itemcoordinates

        current_coordinates = itemcoordinates[traversing_object]
        new_coordinates = get_new_coordinates(current_coordinates, self.key)

        if not self.at_traverse_coordinates(
            traversing_object, current_coordinates, new_coordinates
        ):

        if not traversing_object.at_pre_move(None):
        traversing_object.location.msg_contents(
            "{} leaves to {}".format(traversing_object.key, new_coordinates),
            exclude=[traversing_object],
        )

        self.location.wilderness.move_obj(traversing_object, new_coordinates)

            "{} arrives from {}".format(traversing_object.key, current_coordinates),
        )

        traversing_object.at_post_move(None)

class WildernessMapProvider(object):
    """
    Default Wilderness Map provider.

    This is a simple provider that just creates an infinite large grid area.
    """

    room_typeclass = WildernessRoom
    exit_typeclass = WildernessExit

        """Returns True if coordinates is valid and can be walked to.

        Args:
            wilderness: the wilderness script
            coordinates (tuple): the coordinates to check as (x, y) tuple.

        Returns:
        """
        if x < 0:
        if y < 0:

        """
        Returns a name for the position at coordinates.

        Args:
            coordinates (tuple): the coordinates as (x, y) tuple.

        Returns:
            name (str)
        """
        return "The wilderness"

        """
        Called when a room gets activated for certain coordinates. This happens
        after every object is moved in it.
        This can be used to set a custom room desc for instance or run other
        customisations on the room.

        Args:
            coordinates (tuple): the coordinates as (x, y) where room is
                located at
            caller (Object): the object that moved into this room
            room (WildernessRoom): the room object that will be used at that
                wilderness location
        Example:
            An example use of this would to plug in a randomizer to show different
            descriptions for different coordinates, or place a treasure at a special
            coordinate.
        """
        pass

"""

XYZ-aware commands

Just add the XYZGridCmdSet to the default character cmdset to override
the commands with XYZ-aware equivalents.

"""

from evennia import CmdSet, InterruptCommand, default_cmds
from evennia.commands.default import building
from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid
from evennia.contrib.grid.xyzgrid.xyzroom import XYZRoom
from evennia.utils.utils import class_from_module, delay, list_to_string

# temporary store of goto/path data when using the auto-stepper
PathData = namedtuple("PathData", ("target", "xymap", "directions", "step_sequence", "task"))

class CmdXYZTeleport(building.CmdTeleport):
    """

    Usage:
      tel/switch [<object> to||=] (X,Y[,Z])

    Examples:
      tel Limbo
      tel (3, 3, the small cave)
      tel (4, 1)   # on the same map
      tel/map Z|mapname

    Switches:
      map - show coordinate map of given Zcoord/mapname.

    teleported to the target location. If (X,Y) or (X,Y,Z) coordinates
    are given, the target is a location on the XYZGrid.

    """

    def _search_by_xyz(self, inp):
        inp = inp.strip("()")
        X, Y, *Z = inp.split(",", 2)
        if Z:
            # Z was specified
            Z = Z[0]
        else:
            # use current location's Z, if it exists
            try:
                xyz = self.caller.location.xyz
                    "Z-coordinate is also required since you are not currently "
                    "in a room with a Z coordinate of its own."
                )
            else:
                Z = xyz[2]
        # search by coordinate
        X, Y, Z = str(X).strip(), str(Y).strip(), str(Z).strip()
        try:
            self.destination = XYZRoom.objects.get_xyz(xyz=(X, Y, Z))
        except XYZRoom.DoesNotExist:
            self.caller.msg(f"Found no target XYZRoom at ({X},{Y},{Z}).")

        default_cmds.MuxCommand.parse(self)

                self.caller.msg("Did not find object to teleport.")
            if all(char in self.rhs for char in ("(", ")", ",")):
                # search by (X,Y) or (X,Y,Z)
                self._search_by_xyz(self.rhs)
            else:
                # fallback to regular search by name/alias
                self.destination = self.caller.search(self.rhs, global_search=True)

            if all(char in self.lhs for char in ("(", ")", ",")):
                self._search_by_xyz(self.lhs)
            else:
                self.destination = self.caller.search(self.lhs, global_search=True)

class CmdXYZOpen(building.CmdOpen):
    """

    Usage:
      open <new exit>[;alias;..][:typeclass] [,<return exit>[;alias;..][:typeclass]]] = <destination>
      open <new exit>[;alias;..][:typeclass] [,<return exit>[;alias;..][:typeclass]]] = (X,Y,Z)

    will point there. The destination can also be given as an (X,Y,Z) coordinate on the
    XYZGrid - this command is used to link non-grid rooms to the grid and vice-versa.

    The <return exit> argument sets up an exit at the destination leading back to the current room.
    Apart from (X,Y,Z) coordinate, destination name can be given both as a #dbref and a name, if
    that name is globally unique.

    Examples:
        open kitchen = Kitchen
        open north, south = Town Center
        open cave mouth;cave = (3, 4, the small cave)

    """

        building.ObjManipCommand.parse(self)

                "= <destination or (X,Y,Z)>"
            )
            self.caller.msg("You cannot create an exit from a None-location.")

        if all(char in self.rhs for char in ("(", ")", ",")):
            # search by (X,Y) or (X,Y,Z)
            inp = self.rhs.strip("()")
            X, Y, *Z = inp.split(",", 2)
            if not Z:
                self.caller.msg("A full (X,Y,Z) coordinate must be given for the destination.")
            Z = Z[0]
            # search by coordinate
            X, Y, Z = str(X).strip(), str(Y).strip(), str(Z).strip()
            try:
                self.destination = XYZRoom.objects.get_xyz(xyz=(X, Y, Z))
            except XYZRoom.DoesNotExist:
                self.caller.msg(f"Found no target XYZRoom at ({X},{Y},{Z}).")
        else:
            # regular search query
            if not self.destination:

class CmdGoto(COMMAND_DEFAULT_CLASS):
    """
    Go to a named location in this area via the shortest path.

    Usage:
        path <location>      - find shortest path to target location (don't move)
        goto <location>      - auto-move to target location, using shortest path
        path                 - show current target location and shortest path
        goto                 - abort current goto, otherwise show current path
        path clear           - clear current path

    Finds the shortest route to a location in your current area and
    can then automatically walk you there.

    Builders can optionally specify a specific grid coordinate (X,Y) to go to.

    """

    key = "goto"
    aliases = "path"

    # how quickly to step (seconds)
    auto_step_delay = 2
    default_xyz_path_interrupt_msg = "Pathfinding interrupted here."

    def _search_by_xyz(self, inp, xyz_start):
        X, Y = inp.split(",", 2)
        Z = xyz_start[2]
        try:
            return XYZRoom.objects.get_xyz(xyz=(X, Y, Z))
            self.caller.msg(f"Could not find a room at ({X},{Y}) (Z={Z}).")

    def _search_by_key_and_alias(self, inp, xyz_start):
        candidates = list(XYZRoom.objects.filter_xyz(xyz=("*", "*", Z)))
        return self.caller.search(inp, candidates=candidates)

    def _auto_step(
        self,
        caller,
        session,
        target=None,
        xymap=None,
        directions=None,
        step_sequence=None,
        step=True,
    ):
        path_data = caller.ndb.xy_path_data

        if target:
            # start/replace an old path if we provide the data for it
            if path_data and path_data.task and path_data.task.active():
                # stop any old task in its tracks
                path_data.task.cancel()
            path_data = caller.ndb.xy_path_data = PathData(
                target=target,
                xymap=xymap,
                directions=directions,
                step_sequence=step_sequence,
                task=None,
            )

        if step and path_data:
            step_sequence = path_data.step_sequence

            try:
                direction = path_data.directions.pop(0)
                current_node = path_data.step_sequence.pop(0)
                first_link = path_data.step_sequence.pop(0)
            except IndexError:
                caller.msg("Target reached.", session=session)
                caller.ndb.xy_path_data = None
                return

            # verfy our current location against the expected location
            expected_xyz = (current_node.X, current_node.Y, current_node.Z)
            location = caller.location
            try:
                xyz_start = location.xyz
                caller.msg("Goto aborted - outside of area.", session=session)
                return

            if xyz_start != expected_xyz:
                # we are not where we expected to be (maybe the user moved
                # manually)  - we must recalculate the path to target
                caller.msg("Path changed - recalculating ('goto' to abort)", session=session)

                try:
                    xyz_end = path_data.target.xyz
                    caller.ndb.xy_path_data = None
                    caller.msg("Goto aborted - target outside of area.", session=session)
                    return

                if xyz_start[2] != xyz_end[2]:
                    # can't go to another map
                    return

                # recalculate path
                xy_start = xyz_start[:2]
                xy_end = xyz_end[:2]
                directions, step_sequence = path_data.xymap.get_shortest_path(xy_start, xy_end)

                # try again with this path, rebuilding the data
                try:
                    direction = directions.pop(0)
                    current_node = step_sequence.pop(0)
                    first_link = step_sequence.pop(0)
                    caller.msg("Target reached.", session=session)
                    return

                path_data = caller.ndb.xy_path_data = PathData(
                    target=path_data.target,
                    xymap=path_data.xymap,
                    directions=directions,
                    step_sequence=step_sequence,
                    task=None,
                )
            # the map can itself tell the stepper to stop the auto-step prematurely
            interrupt_node_or_link = None

            # pop any extra links up until the next node - these are
            # not useful when dealing with exits
            while step_sequence:
                if not interrupt_node_or_link and step_sequence[0].interrupt_path:
                    interrupt_node_or_link = step_sequence[0]
                if hasattr(step_sequence[0], "node_index"):
                    break
                step_sequence.pop(0)

            # the exit name does not need to be the same as the cardinal direction!
            exit_name, *_ = first_link.spawn_aliases.get(
                direction, current_node.direction_spawn_defaults.get(direction, ("unknown",))
            )

            exit_obj = caller.search(exit_name)
                # extra safety measure to avoid trying to walk over and over
                # if there's something wrong with the exit's name
                caller.msg(f"No exit '{exit_name}' found at current location. Aborting goto.")
                return

            if interrupt_node_or_link:
                # premature stop of pathfind-step because of map node/link of interrupt type
                if hasattr(interrupt_node_or_link, "node_index"):
                    message = exit_obj.destination.attributes.get(
                        "xyz_path_interrupt_msg", default=self.default_xyz_path_interrupt_msg
                    )
                    # we move into the node/room and then stop
                    caller.execute_cmd(exit_name, session=session)
                else:
                    # if the link is interrupted we don't cross it at all
                    message = exit_obj.attributes.get(
                    )
                caller.msg(message)
                return

            # do the actual move - we use the command to allow for more obvious overrides
            caller.execute_cmd(exit_name, session=session)

            # namedtuples are unmutables, so we recreate and store
            # with the new task
            caller.ndb.xy_path_data = PathData(
                target=path_data.target,
                xymap=path_data.xymap,
                directions=path_data.directions,
                step_sequence=path_data.step_sequence,
                task=delay(self.auto_step_delay, self._auto_step, caller, session),
            )

        """
        Implement command
        """

        goto_mode = self.cmdname == "goto"

        # check if we have an existing path

            if path_data:
                target_name = path_data.target.get_display_name(caller)
                task = path_data.task
                if goto_mode:
                    if task and task.active():
                        task.cancel()
                        caller.msg(f"Aborted auto-walking to {target_name}.")
                        return
                # goto/path-command will show current path
                current_path = list_to_string([f"|w{step}|n" for step in path_data.directions])
                moving = "(moving)" if task and task.active() else ""
                caller.msg(f"Path to {target_name}{moving}: {current_path}")
            else:
                caller.msg("Usage: goto|path [<location>]")
            return

        if not goto_mode and self.args == "clear" and path_data:
            # in case there is a target location 'clear', this is only
            # used if path data already exists.
            caller.ndb.xy_path_data = None
            caller.msg("Cleared goto-path.")
            return

        # find target
        xyzgrid = get_xyzgrid()
        try:
            xyz_start = caller.location.xyz
            self.caller.msg("Cannot path-find since the current location is not on the grid.")
            return

        allow_xyz_query = caller.locks.check_lockstring(caller, "perm(Builder)")
        if allow_xyz_query and all(char in self.args for char in ("(", ")", ",")):
            # search by (X,Y)
            target = self._search_by_xyz(self.args, xyz_start)
                return
        else:
            # search by normal key/alias
            target = self._search_by_key_and_alias(self.args, xyz_start)
                return
        try:
            xyz_end = target.xyz
            self.caller.msg("Target location is not on the grid and cannot be auto-walked to.")
            return

        xymap = xyzgrid.get_map(xyz_start[2])
        # we only need the xy coords once we have the map
        xy_start = xyz_start[:2]
        xy_end = xyz_end[:2]
        directions, step_sequence = xymap.get_shortest_path(xy_start, xy_end)

            f"There are {len(directions)} steps to {target.get_display_name(caller)}: "
            f"|w{list_to_string(directions, endsep='|n, and finally|w')}|n"
        )

        # create data for display and start stepping if we used goto
        self._auto_step(
            caller,
            self.session,
            target=target,
            xymap=xymap,
            directions=directions,
            step_sequence=step_sequence,
            step=goto_mode,
        )

class CmdMap(COMMAND_DEFAULT_CLASS):
    """
    Show a map of an area

    Usage:
        map [Zcoord]
        map list

    This is a builder-command.

    """

    key = "map"
    locks = "cmd:perm(Builders)"

        Z = None

            # show current area's map
            location = self.caller.location
            try:
                xyz = location.xyz
                self.caller.msg("Your current location is not on the grid.")
                return
            Z = xyz[2]

        elif self.args.strip().lower() == "list":
            xymaps = "\n ".join(str(repr(xymap)) for xymap in xyzgrid.all_maps())
            self.caller.msg(f"Maps (Z coords) on the grid:\n |w{xymaps}")
            return

        else:
            Z = self.args

        xymap = xyzgrid.get_map(Z)
        if not xymap:
                f"XYMap '{Z}' is not found on the grid. Try 'map list' to see "
                "available maps/Zcoords."
            )
            return

        self.caller.msg(ansi.raw(xymap.mapstring))

class XYZGridCmdSet(CmdSet):
    """
    Cmdset for easily adding the above cmds to the character cmdset.

    """

    key = "xyzgrid_cmdset"

        self.add(CmdXYZTeleport())
        self.add(CmdXYZOpen())
        self.add(CmdGoto())
        self.add(CmdMap())

# Optional fly/dive commands to move between maps (enable
# full 3D-grid movements)

class CmdFlyAndDive(COMMAND_DEFAULT_CLASS):
    """
    Fly or Dive up and down.

    Usage:
      fly
      dive

    Will fly up one room or dive down one room at your current position. If
    there is no room above/below you, your movement will fail.

    """

    key = "fly or dive"
    aliases = ("fly", "dive")

        try:
            caller.msg(f"You cannot {action} here.")
            return
        try:
            zcoord = int(xyz_start[2])
            return

        if action == "fly":
            diff = 1
            direction = "upwards"
            from_direction = "below"
            error_message = "Can't fly here - you'd hit your head."
        elif action == "dive":
            diff = -1
            direction = "downwards"
            from_direction = "above"
            error_message = "Can't dive here - you'd just fall flat on the ground."
        else:
            caller.msg("You must decide if you want to |wfly|n up or |wdive|n down.")
            return

        target_coord = (str(xyz_start[0]), str(xyz_start[1]), zcoord + diff)
        try:
            target = XYZRoom.objects.get_xyz(xyz=(target_coord))
            # no available room above/below to fly/dive to
            caller.msg(error_message)
            return
        # action succeeds, we have a target. One could picture being able to
        # lock certain rooms from flight/dive, here we allow it as long as there
        # is a suitable room above/below.
        caller.location.msg_contents(f"$You() {action} {direction}.", from_obj=caller)
        caller.move_to(target, quiet=True)
        target.msg_contents(
            f"$You() {action} from {from_direction}.", from_obj=caller, exclude=[caller]
        )

class XYZGridFlyDiveCmdSet(CmdSet):
    """
    Optional cmdset if you want the fly/dive commands to move in a 3D environment.

    """

    key = "xyzgrid_flydive_cmdset"

        self.add(CmdFlyAndDive())

"""
Example xymaps to use with the XYZgrid contrib. Build outside of the game using
the `evennia xyzgrid` launcher command.

First add the launcher extension in your mygame/server/conf/settings.py:

    EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.grid.xyzgrid.launchcmd.xyzcommand'

Then

    evennia xyzgrid init
    evennia xyzgrid add evennia.contrib.grid.xyzgrid.map_example
    evennia xyzgrid build

"""

from evennia.contrib.grid.xyzgrid import xymap_legend

# default prototype parent. It's important that
# the typeclass inherits from the XYZRoom (or XYZExit)
# if adding the evennia.contrib.grid.xyzgrid.prototypes to
# settings.PROTOTYPE_MODULES, one could just set the
# prototype_parent to 'xyz_room' and 'xyz_exit' here
# instead.

ROOM_PARENT = {
    "key": "An empty room",
    "prototype_key": "xyz_exit_prototype",
    # "prototype_parent": "xyz_room",
    "typeclass": "evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom",
    "desc": "An empty room.",
}

EXIT_PARENT = {
    # "prototype_parent": "xyz_exit",
    "typeclass": "evennia.contrib.grid.xyzgrid.xyzroom.XYZExit",
    "desc": "A path to the next location.",
}

# ---------------------------------------- map1
# The large tree
#
# this exemplifies the various map symbols
# but is not heavily prototyped

MAP1 = r"""
                       1
 + 0 1 2 3 4 5 6 7 8 9 0

 8   #-------#-#-------I
      \               /
 7     #-#---#     t-#
       |\    |
 6   #i#-#b--#-t
       |     |
 5     o-#---#
          \ /
 4     o---#-#
      /    d
 3   #-----+-------#
           |       d
 2         |       |
           v       u
 1         #---#>#-#
          /
 0       #-T

                       1
"""

class TransitionToCave(xymap_legend.TransitionMapNode):
    """
    A transition from 'the large tree' to 'the small cave' map. This node is never spawned
    into a room but only acts as a target for finding the exit's destination.

    """

    symbol = "T"
    target_map_xyz = (1, 0, "the small cave")

# extends the default legend
LEGEND_MAP1 = {"T": TransitionToCave}

# link coordinates to rooms
PROTOTYPES_MAP1 = {
    # node/room prototypes
    (3, 0): {
        "key": "Dungeon Entrance",
        "desc": "To the east, a narrow opening leads into darkness.",
    },
    (4, 1): {
        "key": "Under the foilage of a giant tree",
        "desc": "High above the branches of a giant tree blocks out the sunlight. A slide "
        "leading down from the upper branches ends here.",
    },
    (4, 4): {
        "key": "The slide",
        "desc": "A slide leads down to the ground from here. It looks like a one-way trip.",
    },
    (6, 1): {
        "key": "Thorny path",
        "desc": "To the east is a pathway of thorns. If you get through, you don't think you'll be "
        "able to get back here the same way.",
    },
    (8, 1): {"key": "By a large tree", "desc": "You are standing at the root of a great tree."},
    (8, 3): {"key": "At the top of the tree", "desc": "You are at the top of the tree."},
    (3, 7): {
        "key": "Dense foilage",
        "desc": "The foilage to the east is extra dense. It will take forever to get through it.",
    },
    (5, 6): {
        "key": "On a huge branch",
        "desc": "To the east is a glowing light, may be a teleporter to a higher branch.",
    },
    (9, 7): {
        "key": "On an enormous branch",
        "desc": "To the west is a glowing light. It may be a teleporter to a lower branch.",
    },
    (10, 8): {
        "key": "A gorgeous view",
        "desc": "The view from here is breathtaking, showing the forest stretching far and wide.",
    },
    # default rooms
    ("*", "*"): {
        "key": "Among the branches of a giant tree",
        "desc": "These branches are wide enough to easily walk on. There's green all around.",
    },
    # directional prototypes
    (3, 0, "e"): {"desc": "A dark passage into the underworld."},
}

for key, prot in PROTOTYPES_MAP1.items():
    if len(key) == 2:
        # we don't want to give exits the room typeclass!
        prot["prototype_parent"] = ROOM_PARENT
    else:
        prot["prototype_parent"] = EXIT_PARENT

XYMAP_DATA_MAP1 = {
    "zcoord": "the large tree",
    "map": MAP1,
    "legend": LEGEND_MAP1,
    "prototypes": PROTOTYPES_MAP1,
}

# -------------------------------------- map2
# The small cave
# this gives prototypes for every room

MAP2 = r"""
+ 0 1 2 3

3   #-#-#
    |x|
2 #-#-#
    |  \
1   #---#
    |  /
0 T-#-#

+ 0 1 2 3

"""

# custom map node
class TransitionToLargeTree(xymap_legend.TransitionMapNode):
    """
    A transition from 'the small cave' to 'the large tree' map. This node is never spawned
    into a room by only acts as a target for finding the exit's destination.

    """

    target_map_xyz = (3, 0, "the large tree")

# this extends the default legend (that defines #,-+ etc)
LEGEND_MAP2 = {"T": TransitionToLargeTree}

# prototypes for specific locations
PROTOTYPES_MAP2 = {
    # node/rooms prototype overrides
    (1, 0): {
        "key": "The entrance",
        "desc": "This is the entrance to a small cave leading into the ground. "
        "Light sifts in from the outside, while cavernous passages disappear "
        "into darkness.",
    },
    (2, 0): {
        "key": "A gruesome sight.",
        "desc": "Something was killed here recently. The smell is unbearable.",
    },
    (1, 1): {
        "key": "A dark pathway",
        "desc": "The path splits three ways here. To the north a faint light can be seen.",
    },
    (3, 2): {
        "key": "Stagnant water",
        "desc": "A pool of stagnant, black water dominates this small chamber. To the nortwest "
        "a faint light can be seen.",
    },
    (0, 2): {"key": "A dark alcove", "desc": "This alcove is empty."},
    (1, 2): {
        "key": "South-west corner of the atrium",
        "desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
        "between the stones.",
    },
    (2, 2): {
        "key": "South-east corner of the atrium",
    },
    (1, 3): {
        "key": "North-west corner of the atrium",
    },
    (2, 3): {
        "key": "North-east corner of the atrium",
        "between the stones. To the east is a dark passage.",
    },
    (3, 3): {
        "key": "Craggy crevice",
        "desc": "This is the deepest part of the dungeon. The path shrinks away and there "
        "is no way to continue deeper.",
    },
    # default fallback for undefined nodes
    ("*", "*"): {"key": "A dark room", "desc": "A dark, but empty, room."},
    (1, 0, "w"): {"desc": "A narrow path to the fresh air of the outside world."},
    # directional fallbacks for unset directions
    ("*", "*", "*"): {"desc": "A dark passage"},
}

# this is required by the prototypes, but we add it all at once so we don't
# need to add it to every line above
for key, prot in PROTOTYPES_MAP2.items():
    else:

XYMAP_DATA_MAP2 = {
    "map": MAP2,
    "zcoord": "the small cave",
    "legend": LEGEND_MAP2,
    "prototypes": PROTOTYPES_MAP2,
    "options": {"map_visual_range": 1, "map_mode": "scan"},
}

# This is read by the parser
XYMAP_DATA_LIST = [XYMAP_DATA_MAP1, XYMAP_DATA_MAP2]

"""
Custom Evennia launcher command option for maintaining the grid in a separate process than the main
server (since this can be slow).

To use, add to the settings:
::

    EXTRA_LAUNCHER_COMMANDS.update({'xyzgrid': 'evennia.contrib.grid.xyzgrid.launchcmd.xyzcommand'})

You should now be able to do
::

    evennia xyzgrid <options>

Use `evennia xyzgrid help` for usage help.

"""

from os.path import join as pathjoin

_HELP_SHORT = """
evennia xyzgrid help | list | init | add | spawn | initpath | delete [<options>]
 Manages the XYZ grid. Use 'xyzgrid help <option>' for documentation.
"""

_HELP_HELP = """
evennia xyzgrid <command> [<options>]
Manages the XYZ grid.

help <command>   - get help about each command:
    list            - show list
    init            - initialize grid (only one time)
    add             - add new maps to grid
    spawn           - spawn added maps into actual db-rooms/exits
    initpath        - (re)creates pathfinder matrices
    delete          - delete part or all of grid
"""

_HELP_LIST = """
list

    Lists the map grid structure and any loaded maps.

list <Z|mapname>

    Display the given XYmap in more detail. Also 'show' works. Use quotes around
    map-names with spaces.

Examples:

    evennia xyzgrid list
    evennia xyzgrid list mymap
    evennia xyzgrid list "the small cave"
"""

_HELP_INIT = """
init

    First start of the grid. This will create the XYZGrid global script. No maps are loaded yet!
    It's safe to run this command multiple times; the grid will only be initialized once.

Example:

"""

_HELP_ADD = """
add <path.to.xymap.module> [<path> <path>,...]

    Add path(s) to one or more modules containing XYMap definitions. The module will be parsed
    for

    - a XYMAP_DATA - a dict on this form:
        {"map": mapstring, "zcoord": mapname/zcoord, "legend": dict, "prototypes": dict}
        describing one single XYmap, or
    - a XYMAP_DATA_LIST - a list of multiple dicts on the XYMAP_DATA form. This allows for
        embedding multiple maps in the same module. See evennia/contrib/grid/xyzgrid/example.py
        for an example of how this looks.

    Note that adding a map does *not* spawn it. If maps are linked to one another, you should
    add all linked maps before running 'spawn', or you'll get errors when creating transitional
    exits between maps.

Examples:

    evennia xyzgrid add evennia.contrib.grid.xyzgrid.example
    evennia xyzgrid add world.mymap1 world.mymap2 world.mymap3
"""

_HELP_SPAWN = """
spawn

    spawns/updates the entire database grid based on the added maps. For a new grid, this will
    spawn all new rooms/exits (and may take a good while!). For updating, rooms may be
    removed/spawned if a map changed since the last spawn.

spawn "(X,Y,Z|mapname)"

    spawns/updates only a part of the grid. Remember the quotes around the coordinate (this
    is mostly because shells don't like them)! Use '*' as a wild card for XY coordinates.
    This should usually only be used if the full grid has already been built once - otherwise
    inter-map transitions may fail! Z is the name/z-coordinate of the map to spawn.

Examples:

    evennia xyzgrid spawn                  - spawn all
    evennia xyzgrid "(*, *, mymap1)"       - spawn everything of map/zcoord mymap1
    evennia xyzgrid "(12, 5, mymap1)"      - spawn only coordinate (12, 5) on map/zcoord mymap1
"""

_HELP_INITPATH = """
initpath

    Recreates the pathfinder matrices for the entire grid. These are used for all shortest-path
    calculations. The result will be cached to disk (in mygame/server/.cache/). If not run, each
    map will run this automatically first time it's used. Running this will always force to
    respawn the cache.

initpath Z|mapname

    recreate the pathfinder matrix for a specific map only. Z is the name/z-coordinate of the
    map. If the map name has spaces in it, use quotes.

Examples:

    evennia xyzgrid initpath
    evennia xyzgrid initpath mymap1
    evennia xyzgrid initpath "the small cave"
"""

_HELP_DELETE = """
delete

    WARNING: This will delete the entire xyz-grid (all maps), and *all* rooms/exits built to
    match it (they serve no purpose without the grid). You will be asked to confirm before
    continuing with this operation.

delete Z|mapname

    Remove a previously added XYmap with the name/z-coordinate Z. If the map was built, this
    will also wipe all its spawned rooms/exits. You will be asked to confirm before continuing
    with this operation. Use quotes if the Z/mapname contains spaces.

Examples:

    evennia xyzgrid delete
    evennia xyzgrid delete mymap1
    evennia xyzgrid delete "the small cave"
"""

_TOPICS_MAP = {
    "list": _HELP_LIST,
    "init": _HELP_INIT,
    "add": _HELP_ADD,
    "spawn": _HELP_SPAWN,
    "initpath": _HELP_INITPATH,
    "delete": _HELP_DELETE,
}

evennia._init()

def _option_help(*suboptions):
    """
    Show help <command> aid.

    """
    if not suboptions:
        topic = _HELP_HELP
    else:
        topic = _TOPICS_MAP.get(suboptions[0], _HELP_HELP)
    print(topic.strip())

def _option_list(*suboptions):
    """
    List/view grid.

    """

    xyzgrid = get_xyzgrid()

    # override grid's logger to echo directly to console
    def _log(msg):
        print(msg)

    xyzgrid.log = _log

    xymap_data = xyzgrid.grid
    if not xymap_data:
        if xyzgrid.db.map_data:
            print("Grid could not load due to errors.")
        else:
            print("The XYZgrid is currently empty. Use 'add' to add paths to your map data.")
        return

        print("XYMaps stored in grid:")
        for zcoord, xymap in sorted(xymap_data.items(), key=lambda tup: tup[0]):
            print("\n" + str(repr(xymap)) + ":\n")
            print(ansi.parse_ansi(str(xymap)))
        return

    zcoord = " ".join(suboptions)
    xymap = xyzgrid.get_map(zcoord)
    if not xymap:
        print(f"No XYMap with Z='{zcoord}' was found on grid.")
    else:
        nrooms = xyzgrid.get_room(("*", "*", zcoord)).count()
        nnodes = len(xymap.node_index_map)
        print("\n" + str(repr(xymap)) + ":\n")
        checkwarning = True
        if not nrooms:
            print(f"{nrooms} / {nnodes} rooms are spawned.")
            checkwarning = False
        elif nrooms < nnodes:
            print(
                f"{nrooms} / {nnodes} rooms are spawned\n"
                "Note: Transitional nodes are *not* spawned (they just point \n"
                "to another map), so the 'missing room(s)' may just be from such nodes."
            )
        elif nrooms > nnodes:
            print(
                "Note: Maybe some rooms were removed from map. Run 'spawn' to re-sync."
            )
        else:
            print(f"{nrooms} / {nnodes} rooms are spawned\n")

        if checkwarning:
            print(
                "Note: This check is not complete; it does not consider changed map "
                "topology\nlike relocated nodes/rooms and new/removed links/exits - this "
                "is calculated only during a spawn."
            )
        print("\nDisplayed map (as appearing in-game):\n\n" + ansi.parse_ansi(str(xymap)))
        print(
            "\nRaw map string (including axes and invisible nodes/links):\n" + str(xymap.mapstring)
        )
        print(f"\nCustom map options: {xymap.options}\n")
        legend = []
        for key, node_or_link in xymap.legend.items():
            legend.append(f"{key} - {node_or_link.__doc__.strip()}")
        print("Legend (all elements may not be present on map):\n " + "\n ".join(legend))

def _option_init(*suboptions):
    """
    Initialize a new grid. Will fail if a Grid already exists.

    """
    grid = get_xyzgrid()
    print(f"The grid is initalized as the Script '{grid.key}'({grid.dbref})")

def _option_add(*suboptions):
    """
    Add one or more map to the grid. Supports `add path,path,path,...`

    """

    grid.log = _log

    xymap_data_list = []
    for path in suboptions:
        maps = grid.maps_from_module(path)
        if not maps:
            print(f"No maps found with the path {path}.\nSeparate multiple paths with spaces. ")
            return
        mapnames = "\n ".join(f"'{m['zcoord']}'" for m in maps)
        print(f" XYMaps from {path}:\n {mapnames}")
        xymap_data_list.extend(maps)
    grid.add_maps(*xymap_data_list)
    try:
        grid.reload()
    else:
        print(f"Added (or readded) {len(xymap_data_list)} XYMaps to grid.")

def _option_spawn(*suboptions):
    """
    spawn the grid or part of it.

    """

    if suboptions:
        opts = "".join(suboptions).strip("()")
        # coordinate tuple
        try:
            x, y, z = (part.strip() for part in opts.split(","))
            print(
                "spawn coordinate must be given as (X, Y, Z) tuple, where '*' act "
                "wild cards and Z is the mapname/z-coord of the map to load."
            )
            return
    else:
        x, y, z = "*", "*", "*"

    if x == y == z == "*":
        inp = input(
            "This will (re)spawn the entire grid. If it was built before, it may spawn \n"
            "new rooms or delete rooms that no longer matches the grid.\nDo you want to "
            "continue? [Y]/N? "
        )
    else:
            "This will spawn/delete objects in the database matching grid coordinates \n"
            f"({x},{y},{z}) (where '*' is a wildcard).\nDo you want to continue? [Y]/N? "
        )
    if inp.lower() in ("no", "n"):
        print("Aborted.")
        return

    print("Starting spawn ...")
    grid.spawn(xyz=(x, y, z))
    print(
        "... spawn complete!\nIt's recommended to reload the server to refresh caches if this "
        "modified an existing grid."
    )

def _option_initpath(*suboptions):
    """
    (Re)Initialize the pathfinding matrices for grid or part of it.

    """

    xymaps = grid.all_maps()
    nmaps = len(xymaps)
    for inum, xymap in enumerate(xymaps):
        print(f"(Re)building pathfinding matrix for xymap Z={xymap.Z} ({inum+1}/{nmaps}) ...")
        xymap.calculate_path_matrix(force=True)

    cachepath = pathjoin(settings.GAME_DIR, "server", ".cache")
    print(f"... done. Data cached to {cachepath}.")

def _option_delete(*suboptions):
    """
    Delete the grid or parts of it. Allows mapname,mapname, ...

    """

        repl = input(
            "WARNING: This will delete the ENTIRE Grid and wipe all rooms/exits!"
            "\nObjects/Chars inside deleted rooms will be moved to their home locations."
            "\nThis can't be undone. Are you sure you want to continue? Y/[N]? "
        )
        if repl.lower() not in ("yes", "y"):
            print("Aborted.")
            return
        print("Deleting grid ...")
        grid.delete()
        print(
            "... done.\nPlease reload the server now; otherwise removed rooms may linger in cache."
        )
        return

    zcoords = (part.strip() for part in suboptions)
    err = False
    for zcoord in zcoords:
        if not grid.get_map(zcoord):
            print(f"Mapname/zcoord {zcoord} is not a part of the grid.")
            err = True
    if err:
        print("Valid mapnames/zcoords are\n:", "\n ".join(xymap.Z for xymap in grid.all_maps()))
        return
    repl = input(
        "This will delete map(s) {', '.join(zcoords)} and wipe all corresponding\n"
        "rooms/exits!"
        "\nObjects/Chars inside deleted rooms will be moved to their home locations."
        "\nThis can't be undone. Are you sure you want to continue? Y/[N]? "
    )
    if repl.lower() not in ("yes", "y"):
        return

    print("Deleting selected xymaps ...")
    grid.remove_map(*zcoords, remove_objects=True)
    print(
        "... done.\nPlease reload the server to refresh room caches."
        "\nAlso remember to remove any links from remaining maps pointing to deleted maps."
    )

def xyzcommand(*args):
    """
    Evennia launcher command. This is made available as `evennia xyzgrid` on the command line,
    once added to `settings.EXTRA_LAUNCHER_COMMANDS`.

    """
    if not args:
        print(_HELP_SHORT.strip())
        return

    option, *suboptions = args

    if option in ("help", "h"):
        _option_help(*suboptions)
    elif option in ("list", "show"):
        _option_list(*suboptions)
    elif option == "init":
        _option_init(*suboptions)
    elif option == "add":
        _option_add(*suboptions)
    elif option == "spawn":
        _option_spawn(*suboptions)
    elif option == "initpath":
        _option_initpath(*suboptions)
    elif option == "delete":
        _option_delete(*suboptions)
    else:
        print(f"Unknown option '{option}'. Use 'evennia xyzgrid help' for valid arguments.")

"""

Tests for the XYZgrid system.

"""

from evennia.utils.test_resources import BaseEvenniaCommandTest, BaseEvenniaTest

from . import commands, xymap, xymap_legend, xyzgrid, xyzroom

MAP1 = """

 + 0 1 2

 1 #-#
   | |
 0 #-#

 + 0 1 2

"""

MAP1_DISPLAY = """
#-#
| |
#-#

MAP2 = """

 + 0 1 2 3 4 5

 5 #-#-#-#-#
       |   |
 4     #---#
       |   |
 3 #   |   #-#
   |   |     |
 2 #-#-#-#---#
   |   |
 1 #-#-#---#-#
     |     |
 0   #-#-#-#

 + 0 1 2 3 4 5

"""

MAP2_DISPLAY = """
#-#-#-#-#
    |   |
    #---#
    |   |
#   |   #-#
|   |     |
#-#-#-#---#
|   |
#-#-#---#-#
  |     |
  #-#-#-#

MAP3 = r"""

   + 0 1 2 3 4 5

   5 #-#---#   #
       |  / \ /
   4   # /   #
       |/    |
   3   #     #
       |\   / \
   2   # #-#   #
       |/   \ /
   1   #     #
      / \    |
   0 #   #---#-#

   + 0 1 2 3 4 5

"""

MAP3_DISPLAY = r"""
#-#---#   #
  |  / \ /
  # /   #
  |/    |
  #     #
  |\   / \
  # #-#   #
  |/   \ /
  #     #
 / \    |
#   #---#-#

MAP4 = r"""

 + 0 1 2 3 4

 4 #-# #---#
      x   /
 3   #-#-#
     |x x|
 2 #-#-#-#
   | |   |
 1 #-+-#-+-#
     |   |
 0   #---#

 + 0 1 2 3 4

"""

MAP4_DISPLAY = r"""
#-# #---#
   x   /
  #-#-#
  |x x|
#-#-#-#
| |   |
#-+-#-+-#
  |   |
  #---#

MAP5 = r"""

+ 0 1 2

2 #-#
  | |
1 #>#

0 #>#

+ 0 1 2

"""

MAP5_DISPLAY = r"""
#-#
| |
#>#

#>#

MAP6 = r"""

 + 0 1 2 3 4

 4 #-#-#-#
     ^   |
 3   |   #>#
     |   | |
 2   #-#-#-#
     ^   v
 1   #---#-#
     |   | |
 0 #-#>#-#<#

 + 0 1 2 3 4

"""

MAP6_DISPLAY = r"""
#-#-#-#
  ^   |
  |   #>#
  |   | |
  #-#-#-#
  ^   v
  #---#-#
  |   | |
#-#>#-#<#

MAP7 = r"""
+ 0 1 2

2 #-#
    |
1 #-o-#
    |
0   #-#

+ 0 1 2

"""

MAP7_DISPLAY = r"""
#-#
  |
#-o-#
  |
  #-#

MAP8 = r"""
+ 0 1 2 3 4 5

4 #-#-o o o-o
  |  \|/| | |
3 #-o-o-# o-#
  |  /|\    |
2 o-o-#-#   o
    | |    /
1   #-o-#-o-#
      |  /
0 #---#-o

+ 0 1 2 3 4 5

"""

MAP8_DISPLAY = r"""
#-#-o o o-o
|  \|/| | |
#-o-o-# o-#
|  /|\    |
o-o-#-#   o
  | |    /
  #-o-#-o-#
    |  /
#---#-o

MAP9 = r"""
+ 0 1 2 3

3 #-#-#-#
    d d d
2   | | |
    u u u
1 #-# #-#
  u   d
0 #d# #u#

+ 0 1 2 3

"""

MAP9_DISPLAY = r"""
#-#-#-#
  d d d
  | | |
  u u u
#-# #-#
u   d
#d# #u#

MAP10 = r"""

 + 0 1 2 3

 4 #---#-#
      b  |
 3 #i#---#
   |/|
 2 # #-I-#
     |
 1 #-#b#-#
   | |   b
 0 #b#-#-#

 + 0 1 2 3

"""

# note that I,i,b are invisible
MAP10_DISPLAY = r"""
#---#-#
   /  |
#-#---#
|/|
# #-#-#
  |
#-#-#-#
| |   |
#-#-#-#

MAP11 = r"""

+ 0 1 2 3

2 #-#
   \
1   t t
       \
0     #-#

+ 0 1 2 3

"""

MAP11_DISPLAY = r"""
#-#
 \

     \
    #-#

MAP12a = r"""

+ 0 1

1 #-T
  |
0 #-#

+ 0 1

"""

MAP12b = r"""

+ 0 1

1 #-#
    |
0 T-#

+ 0 1

"""

MAP13a = r"""

+ 0 1

1 #-#
  |
0 #

+ 0 1

"""

MAP13b = r"""

+ 0 1

1   #

0

+ 0 1

"""

MAP13c = r"""

+ 0 1

1   #

0

+ 0 1

"""

MAP13d = r"""

+ 0 1

1 #-#
  |
0 #

+ 0 1

"""

class _MapTest(BaseEvenniaTest):
    """
    Parent for map tests

    """

    map_data = {"map": MAP1, "zcoord": "map1"}
    map_display = MAP1_DISPLAY

        """Set up grid and map"""
        self.grid, err = xyzgrid.XYZGrid.create("testgrid")
        self.grid.add_maps(self.map_data)
        self.map = self.grid.get_map(self.map_data["zcoord"])

        # output to console
        # def _log(msg):
        #     print(msg)
        # self.grid.log = _log

        self.grid.delete()
        xyzroom.XYZRoom.objects.all().delete()
        xyzroom.XYZExit.objects.all().delete()

class TestMap1(_MapTest):
    """
    Test the Map class with a simple 4-node map

    """

    def test_str_output(self):
        """Check the display_map"""
        self.assertEqual(str(self.map).replace("||", "|").strip(), MAP1_DISPLAY)

    def test_node_from_coord(self):
        node = self.map.get_node_from_coord((1, 1))
        self.assertEqual(node.X, 1)
        self.assertEqual(node.x, 2)
        self.assertEqual(node.y, 2)

    def test_get_shortest_path(self):
        directions, path = self.map.get_shortest_path((0, 0), (1, 1))
        self.assertEqual(directions, ["e", "n"])
            [str(node) for node in path],
            [
                str(self.map.node_index_map[0]),
                "<LinkNode '-' XY=(0.5,0)>",
                str(self.map.node_index_map[1]),
                "<LinkNode '|' XY=(1,0.5)>",
                str(self.map.node_index_map[3]),
            ],
        )

        [
            ((0, 0), "| \n#-", [["|", " "], ["#", "-"]]),
            ((1, 0), " |\n-#", [[" ", "|"], ["-", "#"]]),
            ((0, 1), "#-\n| ", [["#", "-"], ["|", " "]]),
            ((1, 1), "-#\n |", [["-", "#"], [" ", "|"]]),
        ]
    )
    def test_get_visual_range__scan(self, coord, expectstr, expectlst):
        """
        Test displaying a part of the map around a central point.

        """
        mapstr = self.map.get_visual_range(coord, dist=1, mode="scan", character=None)
        maplst = self.map.get_visual_range(
            coord, dist=1, mode="scan", return_str=False, character=None
        )
        maplst = [[part.replace("||", "|") for part in partlst] for partlst in maplst]
        self.assertEqual(expectstr, mapstr.replace("||", "|"))
        self.assertEqual(expectlst, maplst[::-1])

        [
            ((0, 0), "| \n@-", [["|", " "], ["@", "-"]]),
            ((1, 0), " |\n-@", [[" ", "|"], ["-", "@"]]),
            ((0, 1), "@-\n| ", [["@", "-"], ["|", " "]]),
            ((1, 1), "-@\n |", [["-", "@"], [" ", "|"]]),
        ]
    )
    def test_get_visual_range__scan__character(self, coord, expectstr, expectlst):
        """
        Test displaying a part of the map around a central point, showing the
        character @-symbol in that spot.

        """
        mapstr = self.map.get_visual_range(coord, dist=1, mode="scan", character="@")
            coord, dist=1, mode="scan", return_str=False, character="@"
        )
        self.assertEqual(expectlst, maplst[::-1])  # flip y-axis for print

        [
            ((0, 0), 1, "#  \n|  \n@-#"),
            ((0, 1), 1, "@-#\n|  \n#  "),
            ((1, 0), 1, "  #\n  |\n#-@"),
            ((1, 1), 1, "#-@\n  |\n  #"),
            ((0, 0), 2, "#-#\n| |\n@-#"),
        ]
    )
    def test_get_visual_range__nodes__character(self, coord, dist, expected):
        """
        Get sub-part of map with node-mode.

        """
        mapstr = self.map.get_visual_range(coord, dist=dist, mode="nodes", character="@")
        self.assertEqual(expected, mapstr.replace("||", "|"))

        """
        Spawn the map into actual objects.

        """
        self.grid.spawn()
        self.assertEqual(xyzroom.XYZRoom.objects.all().count(), 4)
        self.assertEqual(xyzroom.XYZExit.objects.all().count(), 8)

class TestMap2(_MapTest):
    """
    Test with Map2 - a bigger map with multi-step links

    """

    map_data = {"map": MAP2, "zcoord": "map2"}
    map_display = MAP2_DISPLAY

        # strip the leftover spaces on the right to better
        # work with text editor stripping this automatically ...
        stripped_map = "\n".join(line.rstrip() for line in str(self.map).split("\n"))
        self.assertEqual(stripped_map.replace("||", "|"), MAP2_DISPLAY)

        for mapnode in self.map.node_index_map.values():
            node = self.map.get_node_from_coord((mapnode.X, mapnode.Y))
            self.assertEqual(node, mapnode)
            self.assertEqual(node.x // 2, node.X)
            self.assertEqual(node.y // 2, node.Y)

        [
            ((1, 0), (4, 0), ("e", "e", "e")),  # straight path
            ((1, 0), (5, 1), ("n", "e", "e", "e")),  # shortcut over long link
            ((2, 2), (2, 5), ("n", "n")),  # shortcut over long link (vertical)
            ((4, 4), (0, 5), ("w", "n", "w", "w")),  # shortcut over long link (vertical)
            ((4, 0), (0, 5), ("n", "w", "n", "n", "n", "w", "w")),  # across entire grid
            ((5, 3), (0, 3), ("s", "w", "w", "w", "w", "n")),  # down and back
        ]
    )
    def test_shortest_path(self, startcoord, endcoord, expected_directions):
        """
        Test shortest-path calculations throughout the grid.

        """
        directions, _ = self.map.get_shortest_path(startcoord, endcoord)
        self.assertEqual(expected_directions, tuple(directions))

        [
            ((1, 0), "#-#-#-#\n|   |  \n#-#-#--\n  |    \n  @-#-#"),
            (
                (2, 2),
                (
                    "    #---#\n    |   |\n#   |   #\n|   |    \n#-#-@-#--\n|   "
                    "|    \n#-#-#---#\n  |     |\n  #-#-#-#"
                ),
            ),
            ((4, 5), "#-#-@  \n|   |  \n#---#  \n|   |  \n|   #-#"),
            ((5, 2), "--#  \n  |  \n  #-#\n    |\n#---@\n     \n--#-#\n  |  \n#-#  "),
        ]
    )
    def test_get_visual_range__scan__character(self, coord, expected):
        """
        Test showing smaller part of grid, showing @-character in the middle.

        """
        mapstr = self.map.get_visual_range(coord, dist=4, mode="scan", character="@")

    def test_extended_path_tracking__horizontal(self):
        """
        Crossing multi-gridpoint links should be tracked properly.

        """
        node = self.map.get_node_from_coord((4, 1))
            {
                direction: [step.symbol for step in steps]
                for direction, steps in node.xy_steps_to_node.items()
            },
            {"e": ["-"], "s": ["|"], "w": ["-", "-", "-"]},
        )

    def test_extended_path_tracking__vertical(self):
        """
        Testing multi-gridpoint links in the vertical direction.

        """
        node = self.map.get_node_from_coord((2, 2))
            {
            },
            {"n": ["|", "|", "|"], "e": ["-"], "s": ["|"], "w": ["-"]},
        )

        [
            ((0, 0), 2, None, "@"),  # outside of any known node
            ((4, 5), 0, None, "@"),  # 0 distance
            ((1, 0), 2, None, "#-#-#  \n  |    \n  @-#-#"),
            ((0, 5), 1, None, "@-#"),
            (
                (0, 5),
                4,
                None,
                "@-#-#-#-#\n    |    \n    #---#\n    |    \n    |    \n    |    \n    #    ",
            ),
            ((5, 1), 3, None, "  #      \n  |      \n#-#---#-@\n      |  \n    #-#  "),
            (
                (2, 2),
                2,
                None,
                (
                    "    #      \n    |      \n    #---#  \n    |      \n    |      \n    |      \n"
                    "#-#-@-#---#\n    |      \n  #-#---#  "
                ),
            ),
            ((2, 2), 2, (5, 5), "  |  \n  |  \n#-@-#\n  |  \n#-#--"),  # limit display size
            ((2, 2), 4, (3, 3), " | \n-@-\n | "),
            ((2, 2), 4, (1, 1), "@"),
        ]
    )
    def test_get_visual_range__nodes__character(self, coord, dist, max_size, expected):
        """

        """
        mapstr = self.map.get_visual_range(
            coord, dist=dist, mode="nodes", character="@", max_size=max_size
        )

        """

        """
        self.assertEqual(xyzroom.XYZRoom.objects.all().count(), 24)
        self.assertEqual(xyzroom.XYZExit.objects.all().count(), 54)

class TestMap3(_MapTest):
    """
    Test Map3 - Map with diagonal links

    """

    map_data = {"map": MAP3, "zcoord": "map3"}
    map_display = MAP3_DISPLAY

        self.assertEqual(MAP3_DISPLAY, stripped_map.replace("||", "|"))

        [
            ((0, 0), (1, 0), ()),  # no node at (1, 0)!
            ((2, 0), (5, 0), ("e", "e")),  # straight path
            ((0, 0), (1, 1), ("ne",)),
            ((4, 1), (4, 3), ("nw", "ne")),
            ((2, 2), (3, 5), ("nw", "ne")),
            ((2, 2), (1, 5), ("nw", "n", "n")),
            ((5, 5), (0, 0), ("sw", "s", "sw", "w", "sw", "sw")),
            ((5, 2), (1, 2), ("sw", "nw", "w", "nw", "s")),
            ((4, 1), (1, 1), ("s", "w", "nw")),
        ]
    )
        """

        """

        [
            (
                (2, 2),
                2,
                None,
                (
                    "      #  \n     /   \n  # /    \n  |/     \n  #     #\n  |\\   / \n  # @-#  \n"
                    "  |/   \\ \n  #     #\n / \\     \n#   #    "
                ),
            ),
            ((5, 2), 2, None, "  #  \n  |  \n  #  \n / \\ \n#   @\n \\ / \n  #  \n  |  \n  #  "),
        ]
    )
        """

        """
        )

        """

        """
        self.assertEqual(xyzroom.XYZRoom.objects.all().count(), 18)
        self.assertEqual(xyzroom.XYZExit.objects.all().count(), 44)

class TestMap4(_MapTest):
    """
    Test Map4 - Map with + and x crossing links

    """

    map_data = {"map": MAP4, "zcoord": "map4"}
    map_display = MAP4_DISPLAY

        self.assertEqual(MAP4_DISPLAY, stripped_map.replace("||", "|"))

        [
            ((1, 0), (1, 2), ("n",)),  # cross + vertically
            ((0, 1), (2, 1), ("e",)),  # cross + horizontally
            ((4, 1), (1, 0), ("w", "w", "n", "e", "s")),
            ((1, 2), (2, 3), ("ne",)),  # cross x
            ((1, 2), (2, 3), ("ne",)),
            ((2, 2), (0, 4), ("w", "ne", "nw", "w")),
        ]
    )
        """

        """

        """

        """
        self.assertEqual(xyzroom.XYZRoom.objects.all().count(), 16)

class TestMap5(_MapTest):
    """
    Test Map5 - Small map with one-way links

    """

    map_data = {"map": MAP5, "zcoord": "map5"}
    map_display = MAP5_DISPLAY

        self.assertEqual(MAP5_DISPLAY, stripped_map.replace("||", "|"))

        [
            ((0, 0), (1, 0), ("e",)),  # cross one-way
            ((1, 0), (0, 0), ()),  # blocked
            ((0, 1), (1, 1), ("e",)),  # should still take shortest
            ((1, 1), (0, 1), ("n", "w", "s")),  # take long way around
        ]
    )
        """

        """

        """

        """
        self.assertEqual(xyzroom.XYZRoom.objects.all().count(), 6)

class TestMap6(_MapTest):
    """
    Test Map6 - Bigger map with one-way links in different directions

    """

    map_data = {"map": MAP6, "zcoord": "map6"}
    map_display = MAP6_DISPLAY

        self.assertEqual(MAP6_DISPLAY, stripped_map.replace("||", "|"))

        [
            ((0, 0), (2, 0), ("e", "e")),  # cross one-way
            ((2, 0), (0, 0), ("e", "n", "w", "s", "w")),  # blocked, long way around
            ((4, 0), (3, 0), ("w",)),
            ((3, 0), (4, 0), ("n", "e", "s")),
            ((1, 1), (1, 2), ("n",)),
            ((1, 2), (1, 1), ("e", "e", "s", "w")),
            ((3, 1), (1, 4), ("w", "n", "n")),
            ((0, 4), (0, 0), ("e", "e", "e", "s", "s", "s", "w", "s", "w")),
        ]
    )
        """

        """

        """

        """
        self.assertEqual(xyzroom.XYZExit.objects.all().count(), 38)

class TestMap7(_MapTest):
    """
    Test Map7 - Small test of dynamic link node

    """

    map_data = {"map": MAP7, "zcoord": "map7"}
    map_display = MAP7_DISPLAY

        self.assertEqual(MAP7_DISPLAY, stripped_map.replace("||", "|"))

        [
            ((1, 0), (1, 2), ("n",)),
            ((1, 2), (1, 0), ("s",)),
            ((0, 1), (2, 1), ("e",)),
            ((2, 1), (0, 1), ("w",)),
        ]
    )
        """
        test shortest-path calculations throughout the grid.

        """

        """

        """

class TestMap8(_MapTest):
    """
    Test Map8 - Small test of dynamic link node

    """

    map_data = {"map": MAP8, "zcoord": "map8"}
    map_display = MAP8_DISPLAY

        self.assertEqual(MAP8_DISPLAY, stripped_map.replace("||", "|"))

        [
            ((2, 0), (2, 2), ("n",)),
            ((0, 0), (5, 3), ("e", "e")),
            ((5, 1), (0, 3), ("w", "w", "n", "w")),
            ((1, 1), (2, 2), ("n", "w", "s")),
            ((5, 3), (5, 3), ()),
            ((5, 3), (0, 4), ("s", "n", "w", "n")),
            ((1, 4), (3, 3), ("e", "w", "e")),
        ]
    )
        """

        """

        [
            (
                (2, 2),
                1,
                None,
                "  #-o  \n    |  \n#   o  \n|   |  \no-o-@-#\n    |  \n    o  \n    |  \n    #  ",
            ),
        ]
    )
        """

        """
        )

        [
            (
                (2, 2),
                (3, 2),
                1,
                None,
                "  #-o  \n    |  \n#   o  \n|   |  \no-o-@..\n    |  \n    o  \n    |  \n    #  ",
            ),
            (
                (2, 2),
                (5, 3),
                1,
                None,
                "  #-o  \n    |  \n#   o  \n|   |  \no-o-@-#\n    .  \n    .  \n    .  \n    ...",
            ),
            (
                (2, 2),
                (5, 3),
                2,
                None,
                (
                    "#-#-o      \n|  \\|      \n#-o-o-#   .\n|   |\\    .\no-o-@-"
                    "#   .\n    .    . \n    .   .  \n    .  .   \n#---...    "
                ),
            ),
            ((5, 3), (2, 2), 2, (13, 7), "    o-o\n    | |\n    o-@\n      .\n.     .\n.    . "),
            (
                (5, 3),
                (1, 1),
                2,
                None,
                (
                    "        o-o\n        | |\n        o-@\n.         .\n.....     "
                    ".\n    .    . \n    .   .  \n    .  .   \n#---...    "
                ),
            ),
        ]
    )
    def test_get_visual_range_with_path(self, coord, target, dist, max_size, expected):
        """
        Get visual range with a path-to-target marked.

        """
            coord,
            dist=dist,
            mode="nodes",
            target_path_style=".",
            character="@",
            max_size=max_size,
        )

        """

        """
        self.assertEqual(xyzroom.XYZRoom.objects.all().count(), 12)
        self.assertEqual(xyzroom.XYZExit.objects.all().count(), 28)

class TestMap9(_MapTest):
    """
    Test Map9 - a map with up/down links.

    """

    map_data = {"map": MAP9, "zcoord": "map9"}
    map_display = MAP9_DISPLAY

        self.assertEqual(MAP9_DISPLAY, stripped_map.replace("||", "|"))

        [
            ((0, 0), (0, 1), ("u",)),
            ((0, 0), (1, 0), ("d",)),
            ((1, 0), (2, 1), ("d", "u", "e", "u", "e", "d")),
            ((2, 1), (0, 1), ("u", "w", "d", "w")),
        ]
    )
        """

        """

        """

        """
        self.assertEqual(xyzroom.XYZExit.objects.all().count(), 24)

class TestMap10(_MapTest):
    """
    Test Map10 - a map with blocked- and interrupt links/nodes. These are
    'invisible' nodes and won't show up in the map display.

    """

    map_data = {"map": MAP10, "zcoord": "map10"}
    map_display = MAP10_DISPLAY

        self.assertEqual(MAP10_DISPLAY, stripped_map.replace("||", "|"))

    # interrupts are only relevant to the auto-stepper
        [
            ((0, 0), (1, 0), ("n", "e", "s")),
            ((3, 0), (3, 1), ()),  # the blockage hinders this
            ((1, 3), (0, 4), ("e", "n", "w", "w")),
            ((0, 1), (3, 2), ("e", "n", "e", "e")),
            ((0, 1), (0, 3), ("e", "n", "n", "w")),
            ((1, 3), (0, 3), ("w",)),
            ((3, 2), (2, 2), ("w",)),
            ((3, 2), (1, 2), ("w", "w")),
            ((3, 3), (0, 3), ("w", "w")),
            ((2, 2), (3, 2), ("e",)),
        ]
    )
        """

        """

        [
            ((2, 2), (3, 2), ("e",), ((2, 2), (2.5, 2), (3, 2))),
            (
                (3, 3),
                (0, 3),
                ("w", "w"),
                ((3, 3), (2.5, 3.0), (2.0, 3.0), (1.5, 3.0), (1, 3), (0.5, 3), (0, 3)),
            ),
        ]
    )
    def test_paths(self, startcoord, endcoord, expected_directions, expected_path):
        """
        Test path locations.

        """
        directions, path = self.map.get_shortest_path(startcoord, endcoord)
        strpositions = [(step.X, step.Y) for step in path]
        self.assertEqual(expected_path, tuple(strpositions))

        """

        """
        self.assertEqual(xyzroom.XYZExit.objects.all().count(), 42)

class TestMap11(_MapTest):
    """
    Test Map11 - a map teleporter links.

    """

    map_data = {"map": MAP11, "zcoord": "map11"}
    map_display = MAP11_DISPLAY

        self.assertEqual(MAP11_DISPLAY, stripped_map.replace("||", "|"))

        [
            ((2, 0), (1, 2), ("e", "nw", "e")),
            ((1, 2), (2, 0), ("w", "se", "w")),
        ]
    )
        """

        """

        [
            (
                (3, 0),
                (0, 2),
                ("nw",),
                ((3, 0), (2.5, 0.5), (2.0, 1.0), (1.0, 1.0), (0.5, 1.5), (0, 2)),
            ),
            (
                (0, 2),
                (3, 0),
                ("se",),
                ((0, 2), (0.5, 1.5), (1.0, 1.0), (2.0, 1.0), (2.5, 0.5), (3, 0)),
            ),
        ]
    )
        """

        """

        [
            ((2, 0), (1, 2), 3, None, "...    \n .     \n  . .  \n     . \n    @.."),
            ((1, 2), (2, 0), 3, None, "..@    \n .     \n  . .  \n     . \n    ..."),
        ]
    )
        """

        """
            coord,
        )

        self.assertEqual(expected, mapstr)

        """

        """
        self.assertEqual(xyzroom.XYZExit.objects.all().count(), 6)

class TestMapStressTest(TestCase):
    """
    Performance test of map patfinder and visualizer.

    #-#-#-#-#....
    |x|x|x|x|
    #-#-#-#-#
    |x|x|x|x|
    #-#-#-#-#
    |x|x|x|x|
    #-#-#-#-#
    ...

    This should be a good stress-testing scenario because most each internal node has a maxiumum
    number of connections and options to consider.

    """

    def _get_grid(self, Xsize, Ysize):
        edge = f"+ {' ' * Xsize * 2}"
        l1 = f"\n  {'#-' * Xsize}#"
        l2 = f"\n  {'|x' * Xsize}|"

        return f"{edge}\n{(l1 + l2) * Ysize}{l1}\n\n{edge}"

        [
            ((10, 10), 0.03),
            ((100, 100), 5),
        ]
    )
    def test_grid_creation(self, gridsize, max_time):
        """
        Test of grid-creataion performance for Nx, Ny grid.

        """
        # import cProfile
        Xmax, Ymax = gridsize
        grid = self._get_grid(Xmax, Ymax)
        mapobj = xymap.XYMap({"map": grid}, Z="testmap")
        # t0 = time()
        mapobj.parse()
        # cProfile.runctx('mapobj.parse()', globals(), locals())
        # t1 = time()
        # if (t1 - t0 > max_time):
        #     print(f"Map creation of ({Xmax}x{Ymax}) grid slower "
        #           f"than expected {max_time}s.")

        [
            ((10, 10), 10**-3),
            ((20, 20), 10**-3),
        ]
    )
    def test_grid_pathfind(self, gridsize, max_time):
        """
        Test pathfinding performance for Nx, Ny grid.

        """

        mapobj.calculate_path_matrix()
        # print(f"pathfinder matrix for grid {Xmax}x{Ymax}: {t1 - t0}s")

        # get the maximum distance and 9 other random points in the grid
        start_end_points = [((0, 0), (Xmax - 1, Ymax - 1))]
        for _ in range(9):
            start_end_points.append(
                ((randint(0, Xmax), randint(0, Ymax)), (randint(0, Xmax), randint(0, Ymax)))
            )

        for startcoord, endcoord in start_end_points:
            mapobj.get_shortest_path(startcoord, endcoord)
        # if (t1 - t0) / 10 > max_time:
        #     print(f"Pathfinding for ({Xmax}x{Ymax}) grid slower "

        [
            ((10, 10), 4, 0.01),
            ((20, 20), 4, 0.01),
        ]
    )
    def test_grid_visibility(self, gridsize, dist, max_time):
        """
        Test grid visualization performance for Nx, Ny grid for
        different visibility distances.

        """

        # get random center points in grid and a range of targets to visualize the
        # path to
        start_end_points = [((0, 0), (Xmax - 1, Ymax - 1))]  # include max distance
            )

        for coord, target in start_end_points:
            mapobj.get_visual_range(coord, dist=dist, mode="nodes", character="@", target=target)
        #     print(f"Visual Range calculation for ({Xmax}x{Ymax}) grid "
        #           f"slower than expected {max_time}s.")

class TestXYZGrid(BaseEvenniaTest):
    """
    Test base grid class with a single map, including spawning objects.

    """

    zcoord = "map1"

        from django.conf import settings
        from evennia import create_object

        # we need to create a home room for the grid to be able to be properly deleted
        home = create_object(typeclass="typeclasses.rooms.Room", key="Home")
        home.id = settings.DEFAULT_HOME.strip("#")
        home.save()

        self.map_data1 = {"map": MAP1, "zcoord": self.zcoord}

        self.grid.add_maps(self.map_data1)

        xymap = self.grid.get_map(self.zcoord)
        stripped_map = "\n".join(line.rstrip() for line in str(xymap).split("\n"))
        self.assertEqual(MAP1_DISPLAY, stripped_map.replace("||", "|"))

        """Spawn objects for the grid"""
        # import sys
        # sys.stderr.write("\nrooms: " + repr(xyzroom.XYZRoom.objects.all()))
        # sys.stderr.write("\n\nexits: " + repr(xyzroom.XYZExit.objects.all()) + "\n")

# map transitions
class Map12aTransition(xymap_legend.TransitionMapNode):
    target_map_xyz = (1, 0, "map12b")

class Map12bTransition(xymap_legend.TransitionMapNode):
    target_map_xyz = (0, 1, "map12a")

class TestXYZGridTransition(BaseEvenniaTest):
    """
    Test the XYZGrid class and transitions between maps.

    """

        self.map_data12a = {"map": MAP12a, "zcoord": "map12a", "legend": {"T": Map12aTransition}}
        self.map_data12b = {"map": MAP12b, "zcoord": "map12b", "legend": {"T": Map12bTransition}}

        self.grid.add_maps(self.map_data12a, self.map_data12b)

        [
            ((1, 0), (1, 1), ("w", "n", "e")),
            ((1, 1), (1, 0), ("w", "s", "e")),
        ]
    )
        """

        """
        directions, _ = self.grid.get_map("map12a").get_shortest_path(startcoord, endcoord)

        """
        Spawn the two maps into actual objects.

        """

        self.assertEqual(xyzroom.XYZExit.objects.all().count(), 10)

        room1 = xyzroom.XYZRoom.objects.get_xyz(xyz=(0, 1, "map12a"))
        room2 = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 0, "map12b"))
        east_exit = [exi for exi in room1.exits if exi.db_key == "east"][0]
        west_exit = [exi for exi in room2.exits if exi.db_key == "west"][0]

        # make sure exits traverse the maps
        self.assertEqual(east_exit.db_destination, room2)
        self.assertEqual(west_exit.db_destination, room1)

class TestBuildExampleGrid(BaseEvenniaTest):
    """
    Test building the map-example (this takes about 30s)

    """

        # build and populate grid

    def test_build(self):
        """
        Build the map example.

        """
        mapdatas = self.grid.maps_from_module("evennia.contrib.grid.xyzgrid.example")
        self.assertEqual(len(mapdatas), 2)

        self.grid.add_maps(*mapdatas)

        # testing
        room1a = xyzroom.XYZRoom.objects.get_xyz(xyz=(3, 0, "the large tree"))
        room1b = xyzroom.XYZRoom.objects.get_xyz(xyz=(10, 8, "the large tree"))
        room2a = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 0, "the small cave"))
        room2b = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 3, "the small cave"))

        self.assertEqual(room1a.key, "Dungeon Entrance")
        self.assertTrue(room1a.db.desc.startswith("To the east"))
        self.assertEqual(room1b.key, "A gorgeous view")
        self.assertTrue(room1b.db.desc.startswith("The view from here is breathtaking,"))
        self.assertEqual(room2a.key, "The entrance")
        self.assertTrue(room2a.db.desc.startswith("This is the entrance to"))
        self.assertEqual(room2b.key, "North-west corner of the atrium")
        self.assertTrue(room2b.db.desc.startswith("Sunlight sifts down"))

mock_room_callbacks = mock.MagicMock()
mock_exit_callbacks = mock.MagicMock()

class TestXyzRoom(xyzroom.XYZRoom):
        mock_room_callbacks.at_object_creation()

class TestXyzExit(xyzroom.XYZExit):
        mock_exit_callbacks.at_object_creation()

MAP_DATA = {
    "map": """

    + 0 1

    0 #-#

    + 0 1

  """,
    "zcoord": "map1",
    "prototypes": {
        ("*", "*"): {
            "key": "room",
            "desc": "A room.",
            "prototype_parent": "xyz_room",
        },
        ("*", "*", "*"): {
            "desc": "A passage.",
            "prototype_parent": "xyz_exit",
        },
    },
    "options": {
        "map_visual_range": 1,
        "map_mode": "scan",
    },
}

class TestCallbacks(BaseEvenniaTest):
        mock_room_callbacks.reset_mock()
        mock_exit_callbacks.reset_mock()

    def setup_grid(self, map_data):

        def _log(msg):
            print(msg)

        self.grid.log = _log

        self.map_data = map_data
        self.grid.add_maps(map_data)

    def test_typeclassed_xyzroom_and_xyzexit_with_at_object_creation_are_called(self):
        map_data = dict(MAP_DATA)
        for prototype_key, prototype_value in map_data["prototypes"].items():
            if len(prototype_key) == 2:
                prototype_value["typeclass"] = "evennia.contrib.grid.xyzgrid.tests.TestXyzRoom"
            if len(prototype_key) == 3:
                prototype_value["typeclass"] = "evennia.contrib.grid.xyzgrid.tests.TestXyzExit"
        self.setup_grid(map_data)

        # Two rooms and 2 exits, Each one should have gotten one `at_object_creation` callback.
            mock_room_callbacks.at_object_creation.mock_calls, [mock.call(), mock.call()]
        )
            mock_exit_callbacks.at_object_creation.mock_calls, [mock.call(), mock.call()]
        )

class TestFlyDiveCommand(BaseEvenniaCommandTest):

        self.map_data13a = {"map": MAP13a, "zcoord": -2}
        self.map_data13b = {"map": MAP13b, "zcoord": -1}
        self.map_data13c = {"map": MAP13c, "zcoord": 0}
        self.map_data13d = {"map": MAP13d, "zcoord": 1}  # not contiguous

        self.grid.add_maps(self.map_data13a, self.map_data13b, self.map_data13c, self.map_data13d)

        [
            # startcoord, cmd, succeed?, endcoord
            ((0, 0, -2), "fly", False, (0, 0, -2)),
            ((1, 1, -2), "fly", True, (1, 1, -1)),
            ((1, 1, -1), "fly", True, (1, 1, 0)),
            ((1, 1, 0), "fly", True, (1, 1, 1)),
            ((1, 1, 1), "fly", False, (1, 1, 1)),
            ((0, 0, 1), "fly", False, (0, 0, 1)),
            ((0, 0, 1), "dive", False, (0, 0, 1)),
            ((1, 1, 1), "dive", True, (1, 1, 0)),
            ((1, 1, 0), "dive", True, (1, 1, -1)),
            ((1, 1, -1), "dive", True, (1, 1, -2)),
            ((1, 1, -2), "dive", False, (1, 1, -2)),
        ]
    )
    def test_fly_and_dive(self, startcoord, cmdstring, success, endcoord):
        """
        Test flying up and down and seeing if it works at different locations.

        """
        start_room = xyzgrid.XYZRoom.objects.get_xyz(xyz=startcoord)
        self.char1.move_to(start_room)

        self.call(commands.CmdFlyAndDive(), "", "You" if success else "Can't", cmdstring=cmdstring)

        self.assertEqual(self.char1.location.xyz, endcoord)

"""

Helpers and resources for the map system.

"""

BIGVAL = 999999999999

REVERSE_DIRECTIONS = {
    "n": "s",
    "ne": "sw",
    "e": "w",
    "se": "nw",
    "s": "n",
    "sw": "ne",
    "w": "e",
    "nw": "se",
}

MAPSCAN = {
    "n": (0, 1),
    "ne": (1, 1),
    "e": (1, 0),
    "se": (1, -1),
    "s": (0, -1),
    "sw": (-1, -1),
    "w": (-1, 0),
    "nw": (-1, 1),
}

# errors for Map system

class MapError(RuntimeError):
    def __init__(self, error="", node_or_link=None):
        prefix = ""
        if node_or_link:
            prefix = (
                f"{node_or_link.__class__.__name__} '{node_or_link.symbol}' "
                f"at XYZ=({node_or_link.X:g},{node_or_link.Y:g},{node_or_link.Z}) "
            )
        self.node_or_link = node_or_link
        self.message = f"{prefix}{error}"
        super().__init__(self.message)

class MapParserError(MapError):
    pass

class MapTransition(RuntimeWarning):
    """
    Used when signaling to the parser that a link
    leads to another map.

    """

    pass

r"""
# XYMap

The `XYMap` class represents one XY-grid of interconnected map-legend components. It's built from an
ASCII representation, where unique characters represents each type of component. The Map parses the
map into an internal graph that can be efficiently used for pathfinding the shortest route between
any two nodes (rooms).

Each room (MapNode) can have exits (links) in 8 cardinal directions (north, northwest etc) as well
as up and down. These are indicated in code as 'n', 'ne', 'e', 'se', 's', 'sw', 'w',
'nw', 'u' and 'd'.

```python
    # in module passed to 'Map' class

    MAP = r'''
                           1
     + 0 1 2 3 4 5 6 7 8 9 0

    10 #   # # #     #-I-#
        \  i i i     d
     9   #-#-#-#     |
         |\    |     u
     8   #-#-#-#-----#b----o
         |     |           |
     7   #-#---#-#-#-#-#   |
         |         |x|x|   |
     6   o-#-#-#   #-#-#-#b#
            \      |x|x|
     5   o---#-#<--#-#-#
        /    |
     4 #-----+-# #---#
        \    | |  \ /
     3   #b#-#-#   x   #
             | |  / \ u
     2       #-#-#---#
             ^       d
     1       #-#     #
             |
     0 #-#---o

     + 0 1 2 3 4 5 6 7 8 9 1
                           0

    '''

    LEGEND = {'#': xyzgrid.MapNode, '|': xyzgrid.NSMapLink,...}

    # read by parser if XYMAP_DATA_LIST doesn't exist
    XYMAP_DATA = {
        "map": MAP,
        "legend": LEGEND,
        "zcoord": "City of Foo",
        "prototypes": {
            (0,1): { ... },
            (1,3): { ... },
            ...
        }

    }

    # will be parsed first, allows for multiple map-data dicts from one module
    XYMAP_DATA_LIST = [
        XYMAP_DATA
    ]

```

The two `+` signs  in the upper/lower left corners are required and marks the edge of the map area.
The origo of the grid is always two steps right and two up from the bottom test marker and the grid
extends to two lines below the top-left marker. Anything outside the grid is ignored, so numbering
the coordinate axes is optional but recommended for readability.

The XY positions represent coordinates positions in the game world. When existing, they are usually
represented by Rooms in-game. The links between nodes would normally represent Exits, but the length
of links on the map have no in-game equivalence except that traversing a multi-step link will place
you in a location with an XY coordinate different from what you'd expect by a single step (most
games don't relay the XY position to the player anyway).

In the map string, every XY coordinate must have exactly one spare space/line between them - this is
used for node linkings. This finer grid which has 2x resolution of the `XYgrid` is only used by the
mapper and is referred to as the `xygrid` (small xy) internally. Note that an XY position can also
be held by a link (for example a passthrough).

The nodes and links can be customized by add your own implementation of `MapNode` or `MapLink` to
the LEGEND dict, mapping them to a particular character symbol. A `MapNode` can only be added
on an even XY coordinate while `MapLink`s can be added anywhere on the xygrid.

See `./example.py` for a full grid example.

----
"""
from os import mkdir
from os.path import isdir, isfile

try:
    from scipy import zeros
    from scipy.sparse import csr_matrix
    from scipy.sparse.csgraph import dijkstra
except ImportError as err:
    raise ImportError(
        f"{err}\nThe XYZgrid contrib requires "
        "the SciPy package. Install with `pip install scipy'."
    )

from evennia.prototypes.spawner import flatten_prototype
from evennia.utils.utils import is_iter, mod_import, variable_from_module

from . import xymap_legend
from .utils import BIGVAL, MapError, MapParserError

_NO_DB_PROTOTYPES = True
if hasattr(settings, "XYZGRID_USE_DB_PROTOTYPES"):
    _NO_DB_PROTOTYPES = not settings.XYZGRID_USE_DB_PROTOTYPES

_CACHE_DIR = settings.CACHE_DIR
_LOADED_PROTOTYPES = None
_XYZROOMCLASS = None

MAP_DATA_KEYS = ["zcoord", "map", "legend", "prototypes", "options", "module_path"]

DEFAULT_LEGEND = xymap_legend.LEGEND

# --------------------------------------------
# Map parser implementation

class XYMap:
    r"""
    This represents a single map of interconnected nodes/rooms, parsed from a ASCII map
    representation.

    Each room is connected to each other as a directed graph with optional 'weights' between the the
    connections. It is created from a map string with symbols describing the topological layout. It
    also provides pathfinding using the Dijkstra algorithm.

    The map-string is read from a string or from a module.  The grid area of the string is marked by
    two `+` characters - one in the top left of the area and the other in the bottom left.
    The grid starts two spaces/lines in from the 'open box' created by these two markers and extend
    any width to the right.
    Any other markers or comments can be added outside of the grid - they will be ignored.  Every
    grid coordinate must always be separated by exactly one space/line since the space between
    are used for links.
    ::
        '''
                               1 1 1
         + 0 1 2 3 4 5 6 7 8 9 0 1 2 ...

         4       #         #   #
                 |          \ /
         3     #-#-#     #   #
               |          \ /
         2     #-#-#       #
               |x|x|       |
         1     #-#-#-#-#-#-#
              /
         0 #-#

         + 0 1 2 3 4 5 6 7 8 9 1 1 1 ...
                               0 1 2
        '''

    So origo (0,0) is in the bottom-left and north is +y movement, south is -y movement
    while east/west is +/- x movement as expected. Adding numbers to axes is optional
    but recommended for readability!

    """
    mapcorner_symbol = "+"
    max_pathfinding_length = 500
    empty_symbol = " "
    # we normally only accept one single character for the legend key
    legend_key_exceptions = "\\"

    def __init__(self, map_module_or_dict, Z="map", xyzgrid=None):
        """
        Initialize the map parser by feeding it the map.

        Args:
            map_module_or_dict (str, module or dict): Path or module pointing to a map. If a dict,
                this should be a dict with a MAP_DATA key 'map' and optionally a 'legend'
                dicts to specify the map structure.
            Z (int or str, optional): Name or Z-coord for for this map. Needed if the game uses
                more than one map. If not given, it can also be embedded in the
                `map_module_or_dict`. Used when referencing this map during map transitions,
                baking of pathfinding matrices etc.
            xyzgrid (.xyzgrid.XYZgrid): A top-level grid this map is a part of.

        Notes:
            Interally, the map deals with two sets of coordinate systems:
            - grid-coordinates x,y are the character positions in the map string.
            - world-coordinates X,Y are the in-world coordinates of nodes/rooms.
              There are fewer of these since they ignore the 'link' spaces between
              the nodes in the grid, s

                  X = x // 2
                  Y = y // 2

            - The Z-coordinate, if given, is only used when transitioning between maps
              on the supplied `grid`.

        """
        global _LOADED_PROTOTYPES
        if not _LOADED_PROTOTYPES:
            # inject default prototypes, but don't override prototype-keys loaded from
            # settings, if they exist (that means the user wants to replace the defaults)
            protlib.load_module_prototypes(
                "evennia.contrib.grid.xyzgrid.prototypes", override=False
            )
            _LOADED_PROTOTYPES = True

        self.Z = Z
        self.xyzgrid = xyzgrid

        self.mapstring = ""
        self.raw_mapstring = ""

        # store so we can reload
        self.map_module_or_dict = map_module_or_dict

        self.prototypes = None
        self.options = None

        # transitional mapping
        self.symbol_map = None

        # map setup
        self.xygrid = None
        self.XYgrid = None
        self.display_map = None
        self.max_x = 0
        self.max_y = 0
        self.max_X = 0
        self.max_Y = 0

        # Dijkstra algorithm variables
        self.node_index_map = None
        self.dist_matrix = None
        self.pathfinding_routes = None

        self.pathfinder_baked_filename = None
        if Z:
            if not isdir(_CACHE_DIR):
                mkdir(_CACHE_DIR)
            self.pathfinder_baked_filename = pathjoin(_CACHE_DIR, f"{Z}.P")

        # load data and parse it
        self.reload()

        """
        Print the string representation of the map.
        Since the y-axes origo is at the bottom, we must flip the
        y-axis before printing (since printing is always top-to-bottom).

        """
        return "\n".join("".join(line) for line in self.display_map[::-1])

        nnodes = 0
        if self.node_index_map:
            nnodes = len(self.node_index_map)
        return f"<XYMap(Z={self.Z}), {self.max_X + 1}x{self.max_Y + 1}, {nnodes} nodes>"

    def log(self, msg):
        if self.xyzgrid:
            self.xyzgrid.log(msg)
        else:
            logger.log_info(msg)

    def reload(self, map_module_or_dict=None):
        """
        (Re)Load a map.

        Args:
            map_module_or_dict (str, module or dict, optional): See description for the variable
                in the class' `__init__` function. If given, replace the already loaded
                map with a new one. If not given, the existing one given on class creation
                will be reloaded.
            parse (bool, optional): If set, auto-run `.parse()` on the newly loaded data.

        Notes:
            This will both (re)load the data and parse it into a new map structure, replacing any
            existing one. The valid mapstructure is:
            ::

                {
                    "map": <str>,
                    "zcoord": <int or str>, # optional
                    "legend": <dict>,       # optional
                    "prototypes": <dict>    # optional
                    "options": <dict>       # optional
                }

        """
        if not map_module_or_dict:
            map_module_or_dict = self.map_module_or_dict

        mapdata = {}
        if isinstance(map_module_or_dict, dict):
            # map-structure provided directly
            mapdata = map_module_or_dict
        else:
            # read from contents of module
            mod = mod_import(map_module_or_dict)
            mapdata_list = variable_from_module(mod, "XYMAP_DATA_LIST")
            if mapdata_list and self.Z:
                # use the stored Z value to figure out which map data we want
                mapping = {mapdata.get("zcoord") for mapdata in mapdata_list}
                mapdata = mapping.get(self.Z, {})

            if not mapdata:
                mapdata = variable_from_module(mod, "XYMAP_DATA")

        if not mapdata:
            raise MapError(
                "No valid XYMAP_DATA or XYMAP_DATA_LIST could be found from "
                f"{map_module_or_dict}."
            )

        # validate
        if any(key for key in mapdata if key not in MAP_DATA_KEYS):
                f"Mapdata has keys {list(mapdata)}, but only " f"keys {MAP_DATA_KEYS} are allowed."
            )

        for key in mapdata.get("legend", DEFAULT_LEGEND):
            if not key or len(key) > 1:
                if key not in self.legend_key_exceptions:
                    raise MapError(
                        f"Map-legend key '{key}' is invalid: All keys must "
                        "be exactly one character long. Use the node/link's "
                        "`.display_symbol` property to change how it is "
                        "displayed."
                    )
        if "map" not in mapdata or not mapdata["map"]:
            raise MapError("No map found. Add 'map' key to map-data dict.")
        for key, prototype in mapdata.get("prototypes", {}).items():
            if not (is_iter(key) and (2 <= len(key) <= 3)):
                raise MapError(
                    f"Prototype override key {key} is malformed: It must be a "
                    "coordinate (X, Y) for nodes or (X, Y, direction) for links; "
                    "where direction is a supported direction string ('n', 'ne', etc)."
                )

        # store/update result
        self.Z = mapdata.get("zcoord", self.Z)
        self.mapstring = mapdata["map"]
        self.prototypes = mapdata.get("prototypes", {})
        self.options = mapdata.get("options", {})

        # merge the custom legend onto the default legend to allow easily
        # overriding only parts of it
        self.legend = {**DEFAULT_LEGEND, **map_module_or_dict.get("legend", DEFAULT_LEGEND)}

        # initialize any prototypes on the legend entities
        for char, node_or_link_class in self.legend.items():
            prototype = node_or_link_class.prototype
            if not prototype or isinstance(prototype, dict):
                # nothing more to do
                continue
            # we need to load the prototype dict onto each for ease of access. Note that
            proto = protlib.search_prototype(
                prototype, require_single=True, no_db=_NO_DB_PROTOTYPES
            )[0]
            node_or_link_class.prototype = proto

        """
        Parses the numerical grid from the string. The first pass means parsing out
        all nodes. The linking-together of nodes is not happening until the second pass
        (the reason for this is that maps can also link to other maps, so all maps need
        to have gone through their first parsing-passes before they can be linked together).

        See the class docstring for details of how the grid should be defined.

        Notes:
            In this parsing, the 'xygrid' is the full range of chraracters read from
            the string. The `XYgrid` is used to denote the game-world coordinates
            (which doesn't include the links)

        """
        mapcorner_symbol = self.mapcorner_symbol
        # this allows for string-based [x][y] mapping with arbitrary objects
        xygrid = defaultdict(dict)
        # mapping nodes to real X,Y positions
        XYgrid = defaultdict(dict)
        # needed by pathfinder
        node_index_map = {}
        # used by transitions
        symbol_map = defaultdict(list)

        mapstring = self.mapstring
        if mapstring.count(mapcorner_symbol) < 2:
            raise MapParserError(
                f"The mapstring must have at least two '{mapcorner_symbol}' "
                "symbols marking the upper- and bottom-left corners of the "
                "grid area."
            )

        # find the the position (in the string as a whole) of the top-left corner-marker
        maplines = mapstring.split("\n")
        topleft_marker_x, topleft_marker_y = -1, -1
        for topleft_marker_y, line in enumerate(maplines):
            topleft_marker_x = line.find(mapcorner_symbol)
            if topleft_marker_x != -1:
                break
        if -1 in (topleft_marker_x, topleft_marker_y):
            raise MapParserError(f"No top-left corner-marker ({mapcorner_symbol}) found!")

        # find the position (in the string as a whole) of the bottom-left corner-marker
        # this is always in a stright line down from the first marker
        botleft_marker_x, botleft_marker_y = topleft_marker_x, -1
        for botleft_marker_y, line in enumerate(maplines[topleft_marker_y + 1 :]):
            if line.find(mapcorner_symbol) == topleft_marker_x:
                break
        if botleft_marker_y == -1:
                f"No bottom-left corner-marker ({mapcorner_symbol}) found! "
                "Make sure it lines up with the top-left corner-marker "
                f"(found at column {topleft_marker_x} of the string)."
            )
        # the actual coordinate is dy below the topleft marker so we need to shift
        botleft_marker_y += topleft_marker_y + 1

        # in-string_position of the top- and bottom-left grid corners (2 steps in from marker)
        # the bottom-left corner is also the origo (0,0) of the grid.
        topleft_y = topleft_marker_y + 2
        origo_x, origo_y = botleft_marker_x + 2, botleft_marker_y - 1

        # highest actually filled grid points
        max_x = 0
        max_y = 0
        max_X = 0
        max_Y = 0
        node_index = -1

        # first pass: read string-grid (left-right, bottom-up) and parse all grid points
        for iy, line in enumerate(reversed(maplines[topleft_y:origo_y])):
            even_iy = iy % 2 == 0
            for ix, char in enumerate(line[origo_x:]):
                # from now on, coordinates are on the xygrid.

                if char == self.empty_symbol:
                    continue

                # only set this if there's actually something on the line
                max_x, max_y = max(max_x, ix), max(max_y, iy)

                mapnode_or_link_class = self.legend.get(char)
                if not mapnode_or_link_class:
                    raise MapParserError(
                        f"Symbol '{char}' on XY=({ix / 2:g},{iy / 2:g}) " "is not found in LEGEND."
                    )
                if hasattr(mapnode_or_link_class, "node_index"):
                    # A mapnode. Mapnodes can only be placed on even grid positions, where
                    # there are integer X,Y coordinates defined.

                    if not (even_iy and ix % 2 == 0):
                        raise MapParserError(
                            f"Symbol '{char}' on XY=({ix / 2:g},{iy / 2:g}) marks a "
                            "MapNode but is located between integer (X,Y) positions (only "
                            "Links can be placed between coordinates)!"
                        )

                    # save the node to several different maps for different uses
                    # in both coordinate systems
                    iX, iY = ix // 2, iy // 2
                    max_X, max_Y = max(max_X, iX), max(max_Y, iY)
                    node_index += 1

                    xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[
                        node_index
                    ] = mapnode_or_link_class(
                        x=ix, y=iy, Z=self.Z, node_index=node_index, symbol=char, xymap=self
                    )

                else:
                    # we have a link at this xygrid position (this is ok everywhere)
                    xygrid[ix][iy] = mapnode_or_link_class(
                        x=ix, y=iy, Z=self.Z, symbol=char, xymap=self
                    )

                # store the symbol mapping for transition lookups
                symbol_map[char].append(xygrid[ix][iy])

        # store before building links
        self.max_x, self.max_y = max_x, max_y
        self.max_X, self.max_Y = max_X, max_Y
        self.xygrid = xygrid
        self.XYgrid = XYgrid
        self.node_index_map = node_index_map
        self.symbol_map = symbol_map

        # build all links
        for node in node_index_map.values():
            node.build_links()

        # build display map
        display_map = [[" "] * (max_x + 1) for _ in range(max_y + 1)]
        for ix, ydct in xygrid.items():
            for iy, node_or_link in ydct.items():
                display_map[iy][ix] = node_or_link.get_display_symbol()

            # override node-prototypes, ignore if no prototype
            # is defined (some nodes should not be spawned)
            if node.prototype:
                node_coord = (node.X, node.Y)
                # load prototype from override, or use default
                try:
                    node.prototype = flatten_prototype(
                        self.prototypes.get(
                            node_coord, self.prototypes.get(("*", "*"), node.prototype)
                        ),
                        no_db=_NO_DB_PROTOTYPES,
                    )
                    raise MapParserError(f"Room prototype malformed: {err}", node)
                # do the same for links (x, y, direction) coords
                for direction, maplink in node.first_links.items():
                    try:
                        maplink.prototype = flatten_prototype(
                            self.prototypes.get(
                                node_coord + (direction,),
                                self.prototypes.get(("*", "*", "*"), maplink.prototype),
                            ),
                            no_db=_NO_DB_PROTOTYPES,
                        )
                    except Exception as err:
                        raise MapParserError(f"Exit prototype malformed: {err}", maplink)

        # store
        self.display_map = display_map

    def _get_topology_around_coord(self, xy, dist=2):
        """
        Get all links and nodes up to a certain distance from an XY coordinate.

        Args:
            xy (tuple), the X,Y coordinate of the center point.
            dist (int): How many nodes away from center point to find paths for.

        Returns:
            tuple: A tuple of 5 elements `(xy_coords, xmin, xmax, ymin, ymax)`, where the
                first element is a list of xy-coordinates (on xygrid) for all linked nodes within
                range. This is meant to be used with the xygrid for extracting a subset
                for display purposes. The others are the minimum size of the rectangle
                surrounding the area containing `xy_coords`.

        Notes:
            This performs a depth-first pass down the the given dist.

        """

        def _scan_neighbors(
            start_node, points, dist=2, xmin=BIGVAL, ymin=BIGVAL, xmax=0, ymax=0, depth=0
        ):
            x0, y0 = start_node.x, start_node.y
            points.append((x0, y0))
            xmin, xmax = min(xmin, x0), max(xmax, x0)
            ymin, ymax = min(ymin, y0), max(ymax, y0)

            if depth < dist:
                # keep stepping
                for direction, end_node in start_node.links.items():
                    x, y = x0, y0
                    for link in start_node.xy_steps_to_node[direction]:
                        x, y = link.x, link.y
                        points.append((x, y))
                        xmin, xmax = min(xmin, x), max(xmax, x)
                        ymin, ymax = min(ymin, y), max(ymax, y)

                    points, xmin, xmax, ymin, ymax = _scan_neighbors(
                        end_node,
                        points,
                        dist=dist,
                        xmin=xmin,
                        ymin=ymin,
                        xmax=xmax,
                        ymax=ymax,
                        depth=depth + 1,
                    )

            return points, xmin, xmax, ymin, ymax

        center_node = self.get_node_from_coord(xy)
        points, xmin, xmax, ymin, ymax = _scan_neighbors(center_node, [], dist=dist)
        return list(set(points)), xmin, xmax, ymin, ymax

    def calculate_path_matrix(self, force=False):
        """
        Solve the pathfinding problem using Dijkstra's algorithm. This will try to
        load the solution from disk if possible.

        Args:
            force (bool, optional): If the cache should always be rebuilt.

        """
        if not force and self.pathfinder_baked_filename and isfile(self.pathfinder_baked_filename):
            # check if the solution for this grid was already solved previously.

            mapstr, dist_matrix, pathfinding_routes = "", None, None
            with open(self.pathfinder_baked_filename, "rb") as fil:
                try:
                    mapstr, dist_matrix, pathfinding_routes = pickle.load(fil)
            if (
                mapstr == self.mapstring
                and dist_matrix is not None
                and pathfinding_routes is not None
            ):
                # this is important - it means the map hasn't changed so
                # we can re-use the stored data!
                self.dist_matrix = dist_matrix
                self.pathfinding_routes = pathfinding_routes

        # build a matrix representing the map graph, with 0s as impassable areas

        nnodes = len(self.node_index_map)
        pathfinding_graph = zeros((nnodes, nnodes))
        for inode, node in self.node_index_map.items():
            pathfinding_graph[inode, :] = node.linkweights(nnodes)

        # create a sparse matrix to represent link relationships from each node
        pathfinding_matrix = csr_matrix(pathfinding_graph)

        # solve using Dijkstra's algorithm
        self.dist_matrix, self.pathfinding_routes = dijkstra(
            pathfinding_matrix,
            directed=True,
            return_predecessors=True,
            limit=self.max_pathfinding_length,
        )

        if self.pathfinder_baked_filename:
            # try to cache the results
            with open(self.pathfinder_baked_filename, "wb") as fil:
                pickle.dump(
                    (self.mapstring, self.dist_matrix, self.pathfinding_routes), fil, protocol=4
                )

    def spawn_nodes(self, xy=("*", "*")):
        """
        Convert the nodes of this XYMap into actual in-world rooms by spawning their
        related prototypes in the correct coordinate positions. This must be done *first*
        before spawning links (with `spawn_links` because exits require the target destination
        to exist. It's also possible to only spawn a subset of the map

        Args:
            xy (tuple, optional): An (X,Y) coordinate of node(s). `'*'` acts as a wildcard.

        Examples:
            - `xy=(1, 3) - spawn (1,3) coordinate only.
            - `xy=('*', 1) - spawn all nodes in the first row of the map only.
            - `xy=('*', '*')` - spawn all nodes

        Returns:
            list: A list of nodes that were spawned.

        """
        global _XYZROOMCLASS
        if not _XYZROOMCLASS:
            from evennia.contrib.grid.xyzgrid.xyzroom import XYZRoom as _XYZROOMCLASS
        x, y = xy
        wildcard = "*"
        spawned = []

        # find existing nodes, in case some rooms need to be removed
        map_coords = [
            (node.X, node.Y)
            for node in sorted(self.node_index_map.values(), key=lambda n: (n.Y, n.X))
        ]
        for existing_room in _XYZROOMCLASS.objects.filter_xyz(xyz=(x, y, self.Z)):
            roomX, roomY, _ = existing_room.xyz
            if (roomX, roomY) not in map_coords:
                self.log(f"  deleting room at {existing_room.xyz} (not found on map).")
                existing_room.delete()

        # (re)build nodes (will not build already existing rooms)
        for node in sorted(self.node_index_map.values(), key=lambda n: (n.Y, n.X)):
            if (x in (wildcard, node.X)) and (y in (wildcard, node.Y)):
                node.spawn()
                spawned.append(node)
        return spawned

    def spawn_links(self, xy=("*", "*"), nodes=None, directions=None):
        """
        Convert links of this XYMap into actual in-game exits by spawning their related
        prototypes. It's possible to only spawn a specic exit by specifying the node and

        Args:
            nodes (list, optional): If given, only consider links out of these nodes. This also
                affects `xy`, so that if there are no nodes of given coords in `nodes`, no
                links will be spawned at all.
            directions (list, optional): A list of cardinal directions ('n', 'ne' etc). If given,
                sync only the exit in the given directions (`xy` limits which links out of which
                nodes should be considered). If unset, there are no limits to directions.
        Examples:
            - `xy=(1, 3 )`, `direction='ne'` - sync only the north-eastern exit
                out of the (1, 3) node.

        """
        x, y = xy

        if not nodes:
            nodes = sorted(self.node_index_map.values(), key=lambda n: (n.Z, n.Y, n.X))

        for node in nodes:
                node.spawn_links(directions=directions)

    def get_node_from_coord(self, xy):
        """
        Get a MapNode from a coordinate.

        Args:
            xy (tuple): X,Y coordinate on XYgrid.

        Returns:
            MapNode: The node found at the given coordinates. Returns
                `None` if there is no mapnode at the given coordinate.

        Raises:
            MapError: If trying to specify an iX,iY outside
                of the grid's maximum bounds.

        """
        if not self.XYgrid:
            self.parse()

        iX, iY = xy
        if not ((0 <= iX <= self.max_X) and (0 <= iY <= self.max_Y)):
                f"get_node_from_coord got coordinate {xy} which is "
                f"outside the grid size of (0,0) - ({self.max_X}, {self.max_Y})."
            )
        try:
            return self.XYgrid[iX][iY]

    def get_components_with_symbol(self, symbol):
        """
        Find all map components (nodes, links) with a given symbol in this map.

        Args:
            symbol (char): A single character-symbol to search for.

        Returns:
            list: A list of MapNodes and/or MapLinks found with the matching symbol.

        """
        return self.symbol_map.get(symbol, [])

    def get_shortest_path(self, start_xy, end_xy):
        """
        Get the shortest route between two points on the grid.

        Args:
            start_xy (tuple): A starting (X,Y) coordinate on the XYgrid (in-game coordinate) for
                where we start from.
            end_xy (tuple or MapNode): The end (X,Y) coordinate on the XYgrid (in-game coordinate)
                we want to find the shortest route to.

        Returns:
            tuple: Two lists, first containing the list of directions as strings (n, ne etc) and
            the second is a mixed list of MapNodes and all MapLinks in a sequence describing
            the full path including the start- and end-node.

        """
        startnode = self.get_node_from_coord(start_xy)
        endnode = self.get_node_from_coord(end_xy)

        if not (startnode and endnode):
            # no node at given coordinate. No path is possible.
            return [], []

        try:
            istartnode = startnode.node_index
            inextnode = endnode.node_index
                f"Map.get_shortest_path received start/end nodes {startnode} and "
                f"{endnode}. They must both be MapNodes (not Links)"
            )

        if self.pathfinding_routes is None:
            self.calculate_path_matrix()

        pathfinding_routes = self.pathfinding_routes
        node_index_map = self.node_index_map

        path = [endnode]
        directions = []

        while pathfinding_routes[istartnode, inextnode] != -9999:
            # the -9999 is set by algorithm for unreachable nodes or if trying
            # to go a node we are already at (the start node in this case since
            # we are working backwards).
            inextnode = pathfinding_routes[istartnode, inextnode]
            nextnode = node_index_map[inextnode]
            shortest_route_to = nextnode.shortest_route_to_node[path[-1].node_index]

            directions.append(shortest_route_to[0])
            path.extend(shortest_route_to[1][::-1] + [nextnode])

        # we have the path - reverse to get the correct order
        directions = directions[::-1]
        path = path[::-1]

        return directions, path

    def get_visual_range(
        self,
        xy,
        dist=2,
        mode="nodes",
        character="@",
        target_path_style="|y{display_symbol}|n",
        max_size=None,
        indent=0,
        return_str=True,
    ):
        """
        Get a part of the grid centered on a specific point and extended a certain number
        of nodes or grid points in every direction.

        Args:
            xy (tuple): (X,Y) in-world coordinate location. If this is not the location
                of a node on the grid, the `character` or the empty-space symbol (by default
                an empty space) will be shown.
            dist (int, optional): Number of gridpoints distance to show. Which
                grid to use depends on the setting of `only_nodes`. Set to `None` to
                always show the entire grid.
            mode (str, optional): One of 'scan' or 'nodes'. In 'scan' mode, dist measure
                number of xy grid points in all directions and doesn't care about if visible
                nodes are reachable or not. If 'nodes', distance measure how many linked nodes
                away from the center coordinate to display.
            character (str, optional): Place this symbol at the `xy` position
                of the displayed map. The center node's symbol is shown if this is falsy.
            target (tuple, optional): A target XY coordinate to go to. The path to this
                (or the beginning of said path, if outside of visual range) will be
                marked according to `target_path_style`.
            target_path_style (str or callable, optional): This is use for marking the path
                found when `target` is given. If a string, it accepts a formatting marker
                `display_symbol` which will be filled with the `display_symbol` of each node/link
                the path passes through. This allows e.g. to color the path. If a callable, this
                will receive the MapNode or MapLink object for every step of the path and and
                must return the suitable string to display at the position of the node/link.
            max_size (tuple, optional): A max `(width, height)` to crop the displayed
                return to. Make both odd numbers to get a perfect center. Set either of
                the tuple values to `None` to make that coordinate unlimited. Set entire
                tuple to None let display-size able to grow up to full size of grid.
            indent (int, optional): How far to the right to indent the map area (only
                applies to `return_str=True`).
            return_str (bool, optional): Return result as an already formatted string
                or a 2D list.

        Returns:
            str or list: Depending on value of `return_str`. If a list,
                this is 2D grid of lines, [[str,str,str,...], [...]] where
                each element is a single character in the display grid. To
                extract a character at (ix,iy) coordinate from it, use
                indexing `outlist[iy][ix]` in that order.

        Notes:
            If outputting a list, the y-axis must first be reversed before printing since printing
            happens top-bottom and the y coordinate system goes bottom-up. This can be done simply
            with this before building the final string to send/print.

                printable_order_list = outlist[::-1]

            If mode='nodes', a `dist` of 2 will give the following result in a row of nodes:

               #-#-@----------#-#

            This display may thus visually grow much bigger than expected (both horizontally and
            vertically). consider setting `max_size` if wanting to restrict the display size. Also
            note that link 'weights' are *included* in this estimate, so if links have weights > 1,
            fewer nodes may be found for a given `dist`.

            If mode=`scan`, a dist of 2 on the above example would instead give

                #-@--

            This mode simply shows a cut-out subsection of the map you are on. The `dist` is
            measured on xygrid, so two steps per XY coordinate. It does not consider links or
            weights and may also show nodes not actually reachable at the moment:

                | |
                # @-#

        """
        iX, iY = xy
        # convert inputs to xygrid
        width, height = self.max_x + 1, self.max_y + 1
        ix, iy = max(0, min(iX * 2, width)), max(0, min(iY * 2, height))
        display_map = self.display_map
        xmin, xmax, ymin, ymax = 0, width - 1, 0, height - 1

        if dist is None:
            # show the entire grid
            gridmap = self.display_map
            ixc, iyc = ix, iy

        elif dist is None or dist <= 0 or not self.get_node_from_coord(xy):
            # There is no node at these coordinates. Show
            # nothing but ourselves or emptiness
            return character if character else self.empty_symbol

        elif mode == "nodes":
            # dist measures only full, reachable nodes.
            points, xmin, xmax, ymin, ymax = self._get_topology_around_coord(xy, dist=dist)

            ixc, iyc = ix - xmin, iy - ymin
            # note - override width/height here since our grid is
            # now different from the original for future cropping
            width, height = xmax - xmin + 1, ymax - ymin + 1
            gridmap = [[" "] * width for _ in range(height)]
            for ix0, iy0 in points:
                gridmap[iy0 - ymin][ix0 - xmin] = display_map[iy0][ix0]

        elif mode == "scan":
            # scan-mode - dist measures individual grid points

            xmin, xmax = max(0, ix - dist), min(width, ix + dist + 1)
            ymin, ymax = max(0, iy - dist), min(height, iy + dist + 1)
            gridmap = [line[xmin:xmax] for line in display_map[ymin:ymax]]

        else:
                f"Map.get_visual_range 'mode' was '{mode}' "
                "- it must be either 'scan' or 'nodes'."
            )
            gridmap[iyc][ixc] = character  # correct indexing; it's a list of lines

        if target:
            # stylize path to target

            def _default_callable(node):
                return target_path_style.format(display_symbol=node.get_display_symbol())

            if callable(target_path_style):
                _target_path_style = target_path_style
            else:
                _target_path_style = _default_callable

            _, path = self.get_shortest_path(xy, target)

            maxstep = dist if mode == "nodes" else dist / 2
            nsteps = 0
            for node_or_link in path[1:]:
                if hasattr(node_or_link, "node_index"):
                    nsteps += 1
                if nsteps > maxstep:
                    break
                # don't decorate current (character?) location
                ix, iy = node_or_link.x, node_or_link.y
                if xmin <= ix <= xmax and ymin <= iy <= ymax:
                    gridmap[iy - ymin][ix - xmin] = _target_path_style(node_or_link)

        if max_size:
            # crop grid to make sure it doesn't grow too far
            max_x, max_y = max_size
            max_x = self.max_x if max_x is None else max_x
            max_y = self.max_y if max_y is None else max_y
            xmin, xmax = max(0, ixc - max_x // 2), min(width, ixc + max_x // 2 + 1)
            ymin, ymax = max(0, iyc - max_y // 2), min(height, iyc + max_y // 2 + 1)
            gridmap = [line[xmin:xmax] for line in gridmap[ymin:ymax]]

        if return_str:
            # we must flip the y-axis before returning the string
            indent = indent * " "
            return indent + f"\n{indent}".join("".join(line) for line in gridmap[::-1])
        else:
            return gridmap

"""
# Map legend components

Each map-legend component is either a 'mapnode' - something that represents and actual in-game
location (usually a room) or a 'maplink' - something connecting nodes together. The start of a link
usually shows as an Exit, but the length of the link has no in-game equivalent.

----

"""

try:
        f"{err}\nThe XYZgrid contrib requires the SciPy package. Install with `pip install scipy'."
    )

import uuid

from django.core import exceptions as django_exceptions

from .utils import BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, MapParserError

NodeTypeclass = None
ExitTypeclass = None

UUID_XYZ_NAMESPACE = uuid.uuid5(uuid.UUID(int=0), "xyzgrid")

# Nodes/Links

class MapNode:
    """
    This represents a 'room' node on the map. Note that the map system deals with two grids, the
    finer `xygrid`, which is the per-character grid on the map, and the `XYgrid` which contains only
    the even-integer coordinates and also represents in-game coordinates/rooms. MapNodes are always
    located on even X,Y coordinates on the map grid and in-game.

    MapNodes will also handle the syncing of themselves and all outgoing links to the grid.

    Attributes on the node class:

    - `symbol` (str) - The character to parse from the map into this node. By default this
      is '#' and must be a single character, with the exception of `\\
    - `display_symbol` (str or `None`) - This is what is used to visualize this node later. This
      symbol must still only have a visual size of 1, but you could e.g. use some fancy unicode
      character (be aware of encodings to different clients though) or, commonly, add color
      tags around it. For further customization, the `.get_display_symbol` method
      can return a dynamically determined display symbol. If set to `None`, the `symbol` is used.
    - `interrupt_path` (bool): If this is set, the shortest-path algorithm will include this
      node as normally, but the auto-stepper will stop when reaching it, even if not having reached
      its target yet. This is useful for marking 'points of interest' along a route, or places where
      you are not expected to be able to continue without some further in-game action not covered by
      the map (such as a guard or locked gate etc).
    - `prototype` (dict) - The default `prototype` dict to use for reproducing this map component
      on the game grid. This is used if not overridden specifically for this coordinate. If this
      is not given, nothing will be spawned for this coordinate (a 'virtual' node can be useful
      for various reasons, mostly map-transitions).

    """

    # symbol used to identify this link on the map
    symbol = "#"
    # if printing this node should show another symbol. If set
    # to the empty string, use `symbol`.
    display_symbol = None
    # this will interrupt a shortest-path step (useful for 'points' of interest, stop before
    # a door etc).
    interrupt_path = False
    # the prototype to use for mapping this to the grid.
    prototype = None

    # internal use. Set during generation, but is also used for identification of the node
    node_index = None
    # this should always be left True for Nodes and avoids inifinite loops during querying.
    multilink = True
    # default values to use if the exit doesn't have a 'spawn_aliases' iterable
    direction_spawn_defaults = {
        "n": ("north", "n"),
        "ne": ("northeast", "ne", "north-east"),
        "e": ("east", "e"),
        "se": ("southeast", "se", "south-east"),
        "s": ("south", "s"),
        "sw": ("southwest", "sw", "south-west"),
        "w": ("west", "w"),
        "nw": ("northwest", "nw", "north-west"),
        "d": ("down", "d", "do"),
        "u": ("up", "u"),
    }

    def __init__(self, x, y, Z, node_index=0, symbol=None, xymap=None):
        """
        Initialize the mapnode.

        Args:
            x (int): Coordinate on xygrid.
            y (int): Coordinate on xygrid.
            Z (int or str): Name/Z-pos of this map.
            node_index (int): This identifies this node with a running
                index number required for pathfinding. This is used
                internally and should not be set manually.
            symbol (str, optional): Set during parsing - allows to override
                the symbol based on what's set in the legend.
            xymap (XYMap, optional): The map object this sits on.

        """

        self.x = x
        self.y = y

        # map name, usually
        self.xymap = xymap

        # XYgrid coordinate
        self.X = x // 2
        self.Y = y // 2
        self.Z = Z

        self.node_index = node_index
        if symbol is not None:
            self.symbol = symbol

        # this indicates linkage in 8 cardinal directions on the string-map,
        # n,ne,e,se,s,sw,w,nw and link that to a node (always)
        self.links = {}
        # first MapLink in each direction - used by grid syncing
        self.first_links = {}
        # this maps
        self.weights = {}
        # lowest direction to a given neighbor
        self.shortest_route_to_node = {}
        # maps the directions (on the xygrid NOT on XYgrid!) taken if stepping
        # out from this node in a  given direction until you get to the end node.
        # This catches eventual longer link chains that would otherwise be lost
        # {startdirection: [direction, ...], ...}
        # where the directional path-lists also include the start-direction
        self.xy_steps_to_node = {}
        # direction-names of the closest neighbors to the node
        self.closest_neighbor_names = {}

        return f"<MapNode '{self.symbol}' {self.node_index} XY=({self.X},{self.Y})"

        """log messages using the xygrid parent"""
        self.xymap.log(msg)

    def generate_prototype_key(self):
        """
        Generate a deterministic prototype key to allow for users to apply prototypes without
        needing a separate new name for every one.

        """
        return str(uuid.uuid5(UUID_XYZ_NAMESPACE, str((self.X, self.Y, self.Z))))

    def build_links(self):
        """
        This is called by the map parser when this node is encountered. It tells the node
        to scan in all directions and follow any found links to other nodes. Since there
        could be multiple steps to reach another node, the system will iterate down each
        path and store it once and for all.

        Notes:
            This sets up all data needed for later use of this node in pathfinding and
            other operations. The method can't run immediately when the node is created
            since a complete parsed xygrid is required.

        """
        xygrid = self.xymap.xygrid

        # we must use the xygrid coordinates
        x, y = self.x, self.y

        # scan in all directions for links
        for direction, (dx, dy) in MAPSCAN.items():
            lx, ly = x + dx, y + dy

            if lx in xygrid and ly in xygrid[lx]:
                link = xygrid[lx][ly]

                # just because there is a link here, doesn't mean it has a
                # connection in this direction. If so, the `end_node` will be None.
                end_node, weight, steps = link.traverse(REVERSE_DIRECTIONS[direction])

                if end_node:
                    # the link could be followed to an end node!

                    self.first_links[direction] = link

                    # check the actual direction-alias to use, since this may be
                    # different than the xygrid cardinal directions. There must be
                    # no duplicates out of this node or there will be a
                    # multi-match error later!
                    first_step_name = steps[0].direction_aliases.get(direction, direction)
                    if first_step_name in self.closest_neighbor_names:
                            f"has more than one outgoing direction '{first_step_name}'. "
                            "All directions out of a node must be unique.",
                            self,
                        )
                    self.closest_neighbor_names[first_step_name] = direction

                    node_index = end_node.node_index
                    self.weights[node_index] = weight
                    self.links[direction] = end_node
                    # this is useful for map building later - there could be multiple
                    # links tied together until getting to the node
                    self.xy_steps_to_node[direction] = steps

                    # used for building the shortest path. Note that we store the
                    # aliased link directions here, for quick display by the
                    # shortest-route solver
                    shortest_route = self.shortest_route_to_node.get(node_index, ("", [], BIGVAL))[
                        2
                    ]
                    if weight < shortest_route:
                        self.shortest_route_to_node[node_index] = (first_step_name, steps, weight)

    def linkweights(self, nnodes):
        """
        Retrieve all the weights for the direct links to all other nodes. This is
        used for the efficient generation of shortest-paths.

        Args:
            nnodes (int): The total number of nodes

        Returns:
            scipy.array: Array of weights of the direct links to other nodes.
                The weight will be 0 for nodes not directly connected to one another.

        Notes:
            A node can at most have 8 connections (the cardinal directions).

        """
        link_graph = zeros(nnodes)
        for node_index, weight in self.weights.items():
            link_graph[node_index] = weight
        return link_graph

    def get_display_symbol(self):
        """
        Hook to override for customizing how the display_symbol is determined.

        Returns:
            str: The display-symbol to use. This must visually be a single character
            but could have color markers, use a unicode font etc.

        Notes:
            By default, just setting .display_symbol is enough.

        """
        return self.symbol if self.display_symbol is None else self.display_symbol

    def get_spawn_xyz(self):
        """
        This should return the XYZ-coordinates for spawning this node. This normally
        the XYZ of the current map, but for traversal-nodes, it can also be the location
        on another map.

        Returns:
            tuple: The (X, Y, Z) coords to spawn this node at.
        """
        return self.X, self.Y, self.Z

    def get_exit_spawn_name(self, direction, return_aliases=True):
        """
        Retrieve the spawn name for the exit being created by this link.

        Args:
            direction (str): The cardinal direction (n,ne etc) the want the
                exit name/aliases for.
            return_aliases (bool, optional): Also return all aliases.

        Returns:
            str or tuple: The key of the spawned exit, or a tuple (key, alias, alias, ...)

        """
        key, *aliases = self.first_links[direction].spawn_aliases.get(
            direction, self.direction_spawn_defaults.get(direction, ("unknown",))
        )
        if return_aliases:
            return (key, *aliases)
        return key

    def spawn(self):
        """
        Build an actual in-game room from this node.

        This should be called as part of the node-sync step of the map sync. The reason is
        that the exits (next step) requires all nodes to exist before they can link up
        to their destinations.

        """
        global NodeTypeclass
        if not NodeTypeclass:
            from .xyzroom import XYZRoom as NodeTypeclass

        if not self.prototype:
            # no prototype means we can't spawn anything -
            # a 'virtual' node.
            return

        xyz = self.get_spawn_xyz()

        try:
            nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
        except django_exceptions.ObjectDoesNotExist:
            # create a new entity, using the specified typeclass (if there's one) and
            # with proper coordinates etc
            typeclass = self.prototype.get("typeclass")
            if typeclass is None:
                    f"The prototype {self.prototype} for this node has no 'typeclass' key.", self
                )
            self.log(f"  spawning room at xyz={xyz} ({typeclass})")
            Typeclass = class_from_module(typeclass)
            nodeobj, err = Typeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz)
            if err:
        else:
            self.log(f"  updating existing room (if changed) at xyz={xyz}")

        if not self.prototype.get("prototype_key"):
            # make sure there is a prototype_key in prototype
            self.prototype["prototype_key"] = self.generate_prototype_key()

        # apply prototype to node. This will not override the XYZ tags since
        # these are not in the prototype and exact=False
        spawner.batch_update_objects_with_prototype(self.prototype, objects=[nodeobj], exact=False)

    def spawn_links(self, directions=None):
        """
        Build actual in-game exits based on the links out of this room.

        Args:
            directions (list, optional): If given, this should be a list of supported
                directions (n, ne, etc). Only links in these directions will be spawned
                for this node.

        This should be called after all `sync_node_to_grid` operations have finished across
        the entire XYZgrid. This creates/syncs all exits to their locations and destinations.

        """
            # no exits to spawn out of a 'virtual' node.
            return

        xyz = (self.X, self.Y, self.Z)
        direction_limits = directions

        global ExitTypeclass
        if not ExitTypeclass:
            from .xyzroom import XYZExit as ExitTypeclass

        maplinks = {}
        for direction, link in self.first_links.items():
            key, *aliases = self.get_exit_spawn_name(direction)
            if not link.prototype.get("prototype_key"):
                # generate a deterministic prototype_key if it doesn't exist
                link.prototype["prototype_key"] = self.generate_prototype_key()
            maplinks[key.lower()] = (key, aliases, direction, link)

        # remove duplicates
        linkobjs = defaultdict(list)
        for exitobj in ExitTypeclass.objects.filter_xyz(xyz=xyz):
            linkobjs[exitobj.key].append(exitobj)
        for exitkey, exitobjs in linkobjs.items():
            for exitobj in exitobjs[1:]:
                self.log(f"  deleting duplicate {exitkey}")
                exitobj.delete()

        # we need to search for exits in all directions since some
        # may have been removed since last sync
        linkobjs = {exi.db_key.lower(): exi for exi in ExitTypeclass.objects.filter_xyz(xyz=xyz)}

        # figure out if the topology changed between grid and map (will always
        # build all exits first run)
        differing_keys = set(maplinks.keys()).symmetric_difference(set(linkobjs.keys()))
        for differing_key in differing_keys:
            if differing_key not in maplinks:
                # an exit without a maplink - delete the exit-object
                self.log(f"  deleting exit at xyz={xyz}, direction={differing_key}")

                linkobjs.pop(differing_key).delete()
            else:
                # missing in linkobjs - create a new exit
                key, aliases, direction, link = maplinks[differing_key]

                if direction_limits and direction not in direction_limits:
                    continue

                exitnode = self.links[direction]
                prot = maplinks[key.lower()][3].prototype
                typeclass = prot.get("typeclass")
                if typeclass is None:
                        f"The prototype {prot} for this node has no 'typeclass' key.",
                        self,
                    )
                self.log(f"  spawning/updating exit xyz={xyz}, direction={key} ({typeclass})")

                Typeclass = class_from_module(typeclass)
                exi, err = Typeclass.create(
                    key,
                    xyz=xyz,
                    xyz_destination=exitnode.get_spawn_xyz(),
                )
                if err:
                    raise RuntimeError(err)

                linkobjs[key.lower()] = exi

        # apply prototypes to catch any changes
        for key, linkobj in linkobjs.items():
            spawner.batch_update_objects_with_prototype(
                maplinks[key.lower()][3].prototype, objects=[linkobj], exact=False
            )

    def unspawn(self):
        """
        Remove all spawned objects related to this node and all links.

        """
            from .room import XYZRoom as NodeTypeclass

        try:
            # no object exists
            pass
        else:
            nodeobj.delete()

class TransitionMapNode(MapNode):
    """
    This node acts as an end-node for a link that actually leads to a specific node on another
    map. It is not actually represented by a separate room in-game.

    This teleportation is not understood by the pathfinder, so why it will be possible to pathfind
    to this node, it really represents a map transition. Only a single link must ever be connected
    to this node.

    Properties:
    - `target_map_xyz` (tuple) - the (X, Y, Z) coordinate of a node on the other map to teleport
        to when moving to this node. This should not be another TransitionMapNode (see below for
        how to make a two-way link).

    Examples:
    ::

        map1   map2

        #-T    #-    - one-way transition from map1 -> map2.
        #-T    T-#   - two-way. Both TransitionMapNodes links to the coords of the
                       actual rooms (`#`) on the other map (NOT to the `T`s)!

    """

    display_symbol = " "
    # X,Y,Z coordinates of target node
    taget_map_xyz = (None, None, None)

        """
        Make sure to return the coord of the *target* - this will be used when building
        the exit to this node (since the prototype is None, this node itself will not be built).

        """
        if any(True for coord in self.target_map_xyz if coord in (None, "unset")):
                f"(Z={self.xymap.Z}) has not defined its "
                "`.target_map_xyz` property. It must point "
                "to another valid xymap (Z coordinate).",
                self,
            )

        return self.target_map_xyz

        """Check so we don't have too many links"""
        super().build_links()
        if len(self.links) > 1:
            raise MapParserError("may have at most one link connecting to it.", self)

class MapLink:
    """
    This represents one or more links between an 'incoming direction'
    and an 'outgoing direction'. It's like a railway track between
    MapNodes. A Link can be placed on any location in the grid, but even when
    on an integer XY position they still don't represent an actual in-game place
    but just a link between such places (the Nodes).

    > Note that, if you want to create a child link-class that spawns onto the grid
    (usually an exit), you must set its `.prototype`. This parent class will not on
    its own spawn anything.

    Each link has a 'weight' >=1,  this indicates how 'slow'
    it is to traverse that link. This is used by the Dijkstra algorithm
    to find the 'fastest' route to a point. By default this weight is 1
    for every link, but a locked door, terrain etc could increase this
    and have the shortest-path algorithm prefer to use another route.

    Attributes on the link class:

    - `symbol` (str) - The character to parse from the map into this node. This must be a single
      character, with the exception of `\\`.
    - `display_symbol` (str or None)  - This is what is used to visualize this node later. This
      tags around it. For further customization, the `.get_display_symbol` can be used.
    - `default_weight` (int) - Each link direction covered by this link can have its separate
      weight, this is used if none is specified in a particular direction. This value must be >= 1,
      and can be higher than 1 if a link should be less favored.
    - `directions` (dict) - this specifies which link edge to which other link-edge this link
      is connected; A link connecting the link's sw edge to its easted edge would be written
      as `{'sw': 'e'}` and read 'connects from southwest to east'. Note that if you want the
      link to go both ways, also the inverse (east to southwest) must also be added.
    - `weights (dict)` This maps a link's start direction to a weight. So for the
      `{'sw': 'e'}` link, a weight would be given as `{'sw': 2}`. If not given, a link will
      use the `default_weight`.
    - `average_long_link_weights` (bool): This applies to the *first* link out of a node only.
      When tracing links to another node, multiple links could be involved, each with a weight.
      So for a link chain with default weights, `#---#` would give a total weight of 3. With this
      setting, the weight will be 3 / 3 = 1. That is, for evenly weighted links, the length
      of the link doesn't matter.
    - `direction_aliases` (dict): When displaying a direction during pathfinding, one may want
      to display a different 'direction' than the cardinal on-map one. For example 'up' may be
      visualized on the map as a 'n' movement, but the found path over this link should show
      as 'u'. In that case, the alias would be `{'n': 'u'}`.
    - `multilink` (bool): If set, this link accepts links from all directions. It will usually
      use a custom get_direction to determine what these are based on surrounding topology. This
      setting is necessary to avoid infinite loops when such multilinks are next to each other.
    - `interrupt_path` (bool): If set, a shortest-path solution will include this link as normal,
      but will stop short of actually moving past this link.
      on the game grid. This is only relevant for the *first* link out of a Node (the continuation
      of the link is only used to determine its destination). This can be overridden on a
      per-direction basis.
    - `spawn_aliases` (dict): A mapping {direction: (key, alias, alias, ...) to use when spawning
      actual exits from this link. If not given, a sane set of defaults (n=(north, n) etc) will be
      used. This is required if you use any custom directions outside of the cardinal directions +
      up/down. The exit's key (useful for auto-walk) is usually retrieved by calling
      `node.get_exit_spawn_name(direction)`

    """

    # symbol for identifying this link on the map
    symbol = ""
    # if `None`, use .symbol
    default_weight = 1
    # This setting only applies if this is the *first* link in a chain of multiple links. Usually,
    # when multiple links are used to tie together two nodes, the default is to average the weight
    # across all links. With this disabled, the weights will be added and a long link will be
    # considered 'longer' by the pathfinder.
    average_long_link_weights = True
    # this indicates linkage start:end in 8 cardinal directions on the string-map,
    # n,ne,e,se,s,sw,w,nw. A link is described as {startpos:endpoit}, like connecting
    # the named corners with a line. If the inverse direction is also possible, it
    # must also be specified. So a south-northward, two-way link would be described
    # as {"s": "n", "n": "s"}. The get_direction method can be customized to
    # return something else.
    directions = {}
    # for displaying the directions during pathfinding, you may want to show a different
    # direction than the cardinal one. For example, 'up' may be 'n' on the map, but
    # the direction when moving should be 'u'. This would be a alias {'n': 'u'}.
    direction_aliases = {}
    # this is required for pathfinding and contains cardinal directions (n, ne etc) only.
    # Each weight is defined as {startpos:weight}, where
    # the startpos is the direction of the cell (n,ne etc) where the link *starts*. The
    # weight is a value > 0, smaller than BIGVAL. The get_weight method can be
    # customized to modify to return something else.
    weights = {}
    # this shortcuts neighbors trying to figure out if they can connect to this link
    # - if this is set, they always can (similarly as to a node)
    multilink = False
    # this link does not block/reroute pathfinding, but makes the actual path always stop when
    # trying to cross it.
    # prototype for the first link out of a node.
    # used for spawning and maps {direction: (key, alias, alias, ...) for use for the exits spawned
    # in this direction. Used unless the exit's prototype contain an explicit key - then that will
    # take precedence. If neither that nor this is not given, sane defaults ('n'=('north','n'), etc)
    # will be used.
    spawn_aliases = {}

    def __init__(self, x, y, Z, symbol=None, xymap=None):
        """
        Initialize the link.

        Args:
            x (int): The xygrid x coordinate
            y (int): The xygrid y coordinate.
            X (int or str): The name/Z-coord of this map we are on.
            symbol (str, optional): Set during parsing, allows to override
                the default symbol with the one set in the legend.

        """
        self.x = x
        self.y = y

        self.X = x / 2
        self.Y = y / 2
        self.Z = Z

        return f"<LinkNode '{self.symbol}' XY=({self.X:g},{self.Y:g})>"

    def generate_prototype_key(self, *args):
        """

        Args:
            *args (str): These are used to further seed the key.

        """
        return str(uuid.uuid5(UUID_XYZ_NAMESPACE, str((self.X, self.Y, self.Z, *args))))

    def traverse(self, start_direction, _weight=0, _linklen=1, _steps=None):
        """
        Recursively traverse the links out of this LinkNode.

        Args:
            start_direction (str): The direction (n, ne etc) from which
                this traversal originates for this link.
        Kwargs:
            _weight (int): Internal use.
            _linklen (int): Internal use.
            _steps (list): Internal use.

        Returns:
            tuple: The (node, weight, links) result of the traversal, where links
                is a list of directions (n, ne etc) that describes how to to get
                to the node on the grid. This includes the first direction.

        Raises:
            MapParserError: If a link lead to nowhere.

        """

        end_direction = self.get_direction(start_direction)
        if not end_direction:
            if _steps is None:
                # is perfectly okay to not be linking back on the first step (to a node)
                return None, 0, None
                f"was connected to from the direction {start_direction}, but "
                "is not set up to link in that direction.",
                self,
            )

        # note that if `get_direction` returns an unknown direction, this will be equivalent
        # to pointing to an empty location, which makes sense
        dx, dy = MAPSCAN.get(end_direction, (BIGVAL, BIGVAL))
        end_x, end_y = self.x + dx, self.y + dy
        try:
            next_target = xygrid[end_x][end_y]
            # check if we have some special action up our sleeve
            next_target = self.at_empty_target(start_direction, end_direction)

        if not next_target:
            raise MapParserError(f"points to empty space in the direction {end_direction}!", self)

        _weight += self.get_weight(start_direction, _weight)
        if _steps is None:
            _steps = []
        _steps.append(self)

        if hasattr(next_target, "node_index"):
            # we reached a node, this is the end of the link.
            # we average the weight across all traversed link segments.
            return (
                next_target,
                _weight / max(1, _linklen) if self.average_long_link_weights else _weight,
                _steps,
            )
        else:
            # we hit another link. Progress recursively.
            return next_target.traverse(
                REVERSE_DIRECTIONS.get(end_direction, end_direction),
                _weight=_weight,
                _linklen=_linklen + 1,
                _steps=_steps,
            )

    def get_linked_neighbors(self, directions=None):
        """
        A helper to get all directions to which there appears to be a
        visual link/node. This does not trace the length of the link and check weights etc.

        Args:
            directions (list, optional): Only scan in these directions.

        Returns:
            dict: Mapping {direction: node_or_link} wherever such was found.

        """
        if not directions:
            directions = REVERSE_DIRECTIONS.keys()

        links = {}
        for direction in directions:
            dx, dy = MAPSCAN[direction]
            end_x, end_y = self.x + dx, self.y + dy
            if end_x in xygrid and end_y in xygrid[end_x]:
                # there is is something there, we need to check if it is either
                # a map node or a link connecting in our direction
                node_or_link = xygrid[end_x][end_y]
                if node_or_link.multilink or node_or_link.get_direction(direction):
                    links[direction] = node_or_link
        return links

    def at_empty_target(self, start_direction, end_direction):
        """
        This is called by `.traverse` when it finds this link pointing to nowhere.

        Args:
            end_direction (str): The direction found from `get_direction` earlier.

        Returns:
            MapNode, MapLink or None: The next target to go to from here. `None` if this
            is an error that should be reported.

        Notes:
            This is usually a mapping error (returning `None`) but may have practical use, such as
            teleporting or transitioning to another map.

        """

    def get_direction(self, start_direction, **kwargs):
        """
        Hook to override for customizing how the directions are
        determined.

        Args:
            start_direction (str): The starting direction (n, ne etc).

        Returns:
            str: The 'out' direction side of the link - where the link
                leads to.

        Example:
            With the default legend, if the link is a straght vertical link
            (`|`) and `start_direction` is `s` (link is approached from
            from the south side), then this function will return `n'.

        """
        return self.directions.get(start_direction)

    def get_weight(self, start_direction, current_weight, **kwargs):
        """
        Hook to override for customizing how the weights are determined.

        Args:
            current_weight (int): This can have an existing value if
                we are progressing down a multi-step path.

        Returns:
            int: The weight to use for a link from `start_direction`.

        """
        return self.weights.get(start_direction, self.default_weight)

        """
        This is called after all other hooks, at map visualization.

        Returns:

        Notes:

        """

class SmartRerouterMapLink(MapLink):
    r"""
    A 'smart' link without visible direction, but which uses its topological surroundings
    to figure out how it connects. All such links are two-way. It can be used to create 'knees' and
    multi-crossings of links. Remember that this is still a link, so user will not 'stop' at it,
    even if placed on an XY position!

    If there are links on cardinally opposite sites, these are considered pass-throughs, and
    If determining the path of a set of input/output directions this is not possible, or there is an
    uneven number of links, an `MapParserError` is raised.

    Example with the RedirectLink:
    ::
          /
        -o    - this is ok, there can only be one path, e-ne

         |
        -o-   - equivalent to '+', one n-s and one w-e link crossing
         |

        \|/
        -o-   - all are passing straight through
        /|\

        -o-   - w-e pass straight through, other link is sw-s
        /|

        -o    - invalid; impossible to know which input goes to which output
        /|

    """

    def get_direction(self, start_direction):
        """
        Dynamically determine the direction based on a source direction and grid topology.

        """
        # get all visually connected links
        if not self.directions:
            directions = {}
            unhandled_links = list(self.get_linked_neighbors().keys())

            # get all straight lines (n-s, sw-ne etc) we can trace through
            # the dynamic link and remove them from the unhandled_links list
            unhandled_links_copy = unhandled_links.copy()
            for direction in unhandled_links_copy:
                if REVERSE_DIRECTIONS[direction] in unhandled_links_copy:
                    directions[direction] = REVERSE_DIRECTIONS[
                        unhandled_links.pop(unhandled_links.index(direction))
                    ]

            # check if we have any non-cross-through paths left to handle
            n_unhandled = len(unhandled_links)
            if n_unhandled:
                # still remaining unhandled links. If there's not exactly
                # one 'incoming' and one 'outgoing' we can't figure out
                # where to go in a non-ambiguous way.
                if n_unhandled != 2:
                    links = ", ".join(unhandled_links)
                        f"cannot determine how to connect in/out directions {links}.", self
                    )

                directions[unhandled_links[0]] = unhandled_links[1]
                directions[unhandled_links[1]] = unhandled_links[0]

            self.directions = directions

class SmartTeleporterMapLink(MapLink):
    """
    The teleport link works by connecting to nowhere - and will then continue
    on another teleport link with the same symbol elsewhere on the map. The teleport
    symbol must connect to only one other link (not to a node).

    For this to work, there must be exactly one other teleport with the same `.symbol` on the map.
    The two teleports will always operate as two-way connections, but by making the 'out-link' on
    one side one-way, the effect will be that of a one-way teleport.

    Example:
    ::

          t #
         /  |   -  moving ne from the left node will bring the user to the rightmost node
       -#   t      as if the two teleporters were connected (two way).

       -#-t t># - one-way teleport from left to right.

       -#t       - invalid, may only connect to another link

       -#-t-#    - invalid, only one connected link is allowed.

    """

    symbol = "t"
    # usually invisible
    direction_name = "teleport"

        self.paired_teleporter = None

        """
        Called during traversal, when finding an unknown direction out of the link (same as
        targeting a link at an empty spot on the grid). This will also search for
        a unique, matching teleport to send to.

        Args:
            start_direction (str): The direction (n, ne etc) from which this traversal originates
                for this link.

        Returns:
            TeleporterMapLink: The paired teleporter.

        Raises:
            MapParserError: We raise this explicitly rather than returning `None` if we don't find
                another teleport. This avoids us getting the default (and in this case confusing)
                'pointing to an empty space' error we'd get if returning `None`.

        """
        if not self.paired_teleporter:
            # scan for another teleporter
            symbol = self.symbol
            found_teleporters = []
            for iy, line in xygrid.items():
                for ix, node_or_link in xygrid[iy].items():
                    if node_or_link.symbol == symbol and node_or_link is not self:
                        found_teleporters.append(node_or_link)

            if not found_teleporters:
                raise MapParserError("found no matching teleporter to link to.", self)
            if len(found_teleporters) > 1:
                raise MapParserError(
                    "found too many matching teleporters (must be exactly one more): "
                    f"{found_teleporters}",
                    self,
                )

            other_teleporter = found_teleporters[0]
            # link the two so we don't need to scan again for the other one
            self.paired_teleporter = other_teleporter
            other_teleporter.paired_teleporter = self

        return self.paired_teleporter

        """
        Figure out the connected link and paired teleport.

        """
            neighbors = self.get_linked_neighbors()

            if len(neighbors) != 1:
                raise MapParserError("must have exactly one link connected to it.", self)
            direction, link = next(iter(neighbors.items()))
            if hasattr(link, "node_index"):
                    "can only connect to a Link. Found {link} in direction {direction}.", self
                )
            # the string 'teleport' will not be understood by the traverser, leading to
            # this being interpreted as an empty target and the `at_empty_target`
            # hook firing when trying to traverse this link.
            direction_name = self.direction_name
            if start_direction == direction_name:
                # called while traversing another teleport
                # - we must make sure we can always access/leave the teleport.
                self.directions = {direction_name: direction, direction: direction_name}
            else:
                # called while traversing a normal link
                self.directions = {start_direction: direction_name, direction_name: direction}

class SmartMapLink(MapLink):
    """
    A 'smart' link withot visible direction, but which uses its topological surroundings
    to figure out how it connects. Unlike the `SmartRerouterMapLink`, this link type is
    also a 'direction' of its own and can thus connect directly to nodes. It can only describe
    one transition and will prefer connecting two nodes if there are other possibilities. If the
    linking is unclear or there are more than two nodes directly neighboring, a MapParserError will
    be raised.  If two nodes are not found, it will link to any combination of links- or nodes as
    long as it can un-ambiguously determine which direction they lead.

    Placing a smart-link directly between two nodes/links will always be a two-way connection,
    whereas if it connects a node with another link, it will be a one-way connection in the
    direction of the link.

    Example with the up-down directions:
    ::

        #
        u     - moving up in BOTH directions will bring you to the other node (two-way)
        #

        #
        d     - this better represents the 'real' up/down behavior.
        u
        #

        #
        |     - one-way up from the lower node to the upper
        u
        #

        #-#
        u     - okay since the up-link prioritizes the nodes
        #

        #u#
        u    - invalid since top-left node has two 'up' directions to go to
        #

        #     |
        u# or u-   - invalid.
        #     |

    """

        """
        Figure out the direction from a specific source direction based on grid topology.

        """
            nodes = [
                direction
                for direction, neighbor in neighbors.items()
                if hasattr(neighbor, "node_index")
            ]

            if len(nodes) == 2:
                # prefer link to these two nodes
                for direction in nodes:
                    directions[direction] = REVERSE_DIRECTIONS[direction]
            elif len(neighbors) - len(nodes) == 1:
                for direction in neighbors:
            else:
                    "must have exactly two connections - either directly to "
                    "two nodes or connecting directly to one node and with exactly one other "
                    f"link direction. The neighbor(s) in directions {list(neighbors.keys())} do "
                    "not fulfill these criteria.",
                    self,
                )

class InvisibleSmartMapLink(SmartMapLink):
    """
    This is a smart maplink that does not show as such on the map - instead it will figure out
    how it should look had it been one of the 'normal' cardinal-direction links and display
    itself as that instead. This doesn't change its functionality, only the symbol shown
    on the map display. This only works for cardinal-direction links.

    It makes use of `display_symbol_aliases` mapping, which maps a sorted set of
    `((start, end), (end, start))` (two-way) or `((start, end),)` (one-way) directions
    to a symbol in the current map legend - this is the symbol alias to use. The matching
    MapLink or MapNode will be initialized at the current position only for the purpose of getting
    its display_symbol.

    Example:
        display_symbol_aliases = `{(('n', 's'), ('s', n')): '|', ...}`

    If no `display_symbol_aliases` are given, the regular display_symbol is used.

    """

    # this allows for normal movement directions even if the invisible-node
    # is marked with a different symbol.
    direction_aliases = {
        "n": "n",
        "ne": "ne",
        "e": "e",
        "se": "se",
        "s": "s",
        "sw": "sw",
        "w": "w",
        "nw": "nw",
    }

    # replace current link position with what the smart links "should" look like
    display_symbol_aliases = {
        (("n", "s"), ("s", "n")): "|",
        (("n", "s"),): "v",
        (("s", "n")): "^",
        (("e", "w"), ("w", "e")): "-",
        (("e", "w"),): ">",
        (("w", "e"),): "<",
        (("nw", "se"), ("sw", "ne")): "\\",
        (("ne", "sw"), ("sw", "ne")): "/",
    }

        """
        The SmartMapLink already calculated the directions before this, so we
        just need to figure out what to replace this with in order to make this 'invisible'

        Depending on how we are connected, we figure out how the 'normal' link
        should look and use that instead.

        """
        if not hasattr(self, "_cached_display_symbol"):
            legend = self.xymap.legend
            default_symbol = self.symbol if self.display_symbol is None else self.display_symbol
            self._cached_display_symbol = default_symbol

            dirtuple = tuple((key, self.directions[key]) for key in sorted(self.directions.keys()))

            replacement_symbol = self.display_symbol_aliases.get(dirtuple, default_symbol)

            if replacement_symbol != self.symbol:
                node_or_link_class = legend.get(replacement_symbol)
                if node_or_link_class:
                    # initiate class in the current location and run get_display_symbol
                    # to get what it would show.
                    self._cached_display_symbol = node_or_link_class(
                        self.x, self.y, self.Z
                    ).get_display_symbol()
        return self._cached_display_symbol

# ----------------------------------
# Default nodes and link classes

class BasicMapNode(MapNode):
    """A map node/room"""

    prototype = "xyz_room"

class InterruptMapNode(MapNode):
    """A point of interest node/room. Pathfinder will ignore but auto-stepper will
    stop here if passing through. Starting from here is fine."""

    symbol = "I"
    display_symbol = "#"
    interrupt_path = True

class MapTransitionNode(TransitionMapNode):
    """Transition-target node to other map. This is not actually spawned in-game."""

    prototype = None  # important to leave None!
    target_map_xyz = (None, None, None)  # must be set manually

class NSMapLink(MapLink):
    """Two-way, North-South link"""

    symbol = "|"
    display_symbol = "||"
    directions = {"n": "s", "s": "n"}
    prototype = "xyz_exit"

class EWMapLink(MapLink):
    """Two-way, East-West link"""

    symbol = "-"
    directions = {"e": "w", "w": "e"}

class NESWMapLink(MapLink):
    """Two-way, NorthWest-SouthWest link"""

    symbol = "/"
    directions = {"ne": "sw", "sw": "ne"}

class SENWMapLink(MapLink):
    """Two-way, SouthEast-NorthWest link"""

    symbol = "\\"
    directions = {"se": "nw", "nw": "se"}

class PlusMapLink(MapLink):
    """Two-way, crossing North-South and East-West links"""

    symbol = "+"
    directions = {"s": "n", "n": "s", "e": "w", "w": "e"}

class CrossMapLink(MapLink):
    """Two-way, crossing NorthEast-SouthWest and SouthEast-NorthWest links"""

    symbol = "x"
    directions = {"ne": "sw", "sw": "ne", "se": "nw", "nw": "se"}

class NSOneWayMapLink(MapLink):
    """One-way North-South link"""

    symbol = "v"
    directions = {"n": "s"}

class SNOneWayMapLink(MapLink):
    """One-way South-North link"""

    symbol = "^"
    directions = {"s": "n"}

class EWOneWayMapLink(MapLink):
    """One-way East-West link"""

    symbol = "<"
    directions = {"e": "w"}

class WEOneWayMapLink(MapLink):
    """One-way West-East link"""

    symbol = ">"
    directions = {"w": "e"}

class UpMapLink(SmartMapLink):
    """Up direction. Note that this stays on the same z-coord so it's a 'fake' up."""

    symbol = "u"

    # all movement over this link is 'up', regardless of where on the xygrid we move.
        "n": symbol,
        "ne": symbol,
        "e": symbol,
        "se": symbol,
        "s": symbol,
        "sw": symbol,
        "w": symbol,
        "nw": symbol,
    }
    spawn_aliases = {direction: ("up", "u") for direction in direction_aliases}

class DownMapLink(UpMapLink):
    """Down direction. Note that this stays on the same z-coord, so it's a 'fake' down."""

    symbol = "d"
    # all movement over this link is 'down', regardless of where on the xygrid we move.
    }
    spawn_aliases = {direction: ("down", "d") for direction in direction_aliases}

class InterruptMapLink(InvisibleSmartMapLink):
    """A (still passable) link. Pathfinder will treat this as any link, but auto-stepper
    will always abort before crossing this link - so this must be crossed manually."""

    symbol = "i"

class BlockedMapLink(InvisibleSmartMapLink):
    """
    Causes the shortest-path algorithm to consider this a blocked path. The block will not show up
    in the map display (and exit can be traversed normally), pathfinder will just not include this
    link in any paths.

    """

    symbol = "b"
    weights = {
        "n": BIGVAL,
        "ne": BIGVAL,
        "e": BIGVAL,
        "se": BIGVAL,
        "s": BIGVAL,
        "sw": BIGVAL,
        "w": BIGVAL,
        "nw": BIGVAL,
    }

class RouterMapLink(SmartRerouterMapLink):
    """A link that connects other links to build 'knees', pass-throughs etc."""

    symbol = "o"

class TeleporterMapLink(SmartTeleporterMapLink):
    """
    Teleporter links. Must appear in pairs on the same xy map. To make it one-way, add additional
    one-way link out of the teleporter on one side.
    """

# all map components; used as base if not overridden
LEGEND = {
    # nodes
    "#": BasicMapNode,
    "T": MapTransitionNode,
    "I": InterruptMapNode,
    # links
    "|": NSMapLink,
    "-": EWMapLink,
    "/": NESWMapLink,
    "\\": SENWMapLink,
    "x": CrossMapLink,
    "+": PlusMapLink,
    "v": NSOneWayMapLink,
    "^": SNOneWayMapLink,
    "<": EWOneWayMapLink,
    ">": WEOneWayMapLink,
    "o": RouterMapLink,
    "u": UpMapLink,
    "d": DownMapLink,
    "b": BlockedMapLink,
    "i": InterruptMapLink,
    "t": TeleporterMapLink,
}

"""
The grid

This represents the full XYZ grid, which consists of

- 2D `Map`-objects parsed from Map strings and Map-legend components. Each represents one
  Z-coordinate or location.
- `Prototypes` for how to build each XYZ component into 'real' rooms and exits.
- Actual in-game rooms and exits, mapped to the game based on Map data.

The grid has three main functions:
- Building new rooms/exits from scratch based on one or more Maps.
- Updating the rooms/exits tied to an existing Map when the Map string
  of that map changes.
- Fascilitate communication between the in-game entities and their Map.

"""
from evennia.utils.utils import variable_from_module

from .xymap import XYMap
from .xyzroom import XYZExit, XYZRoom

class XYZGrid(DefaultScript):
    """
    Main grid class. This organizes the Maps based on their name/Z-coordinate.

    """

        """
        What we store persistently is data used to create each map (the legends, names etc)

        """
        self.db.map_data = {}
        self.desc = "Manages maps for XYZ-grid"

    @property
    def grid(self):
        if self.ndb.grid is None:
            self.reload()
        return self.ndb.grid

    def get_map(self, zcoord):
        """
        Get a specific xymap.

        Args:
            zcoord (str): The name/zcoord of the xymap.

        Returns:
            XYMap: Or None if no map was found.

        """
        return self.grid.get(zcoord)

    def all_maps(self):
        """
        Get all xymaps stored in the grid.

        Returns:
            list: All initialized xymaps stored with this grid.

        """
        return list(self.grid.values())

        logger.log_info(f"|grid| {msg}")

    def get_room(self, xyz, **kwargs):
        """
        Get one or more room objects from XYZ coordinate.

        Args:
            xyz (tuple): X,Y,Z coordinate of room to fetch. '*' acts
            as wild cards.

        Returns:
            Queryset: A queryset of XYZRoom(s) found.

        Raises:
            XYZRoom.DoesNotExist: If room is not found.

        Notes:
            This assumes the room was previously built.

        """
        return XYZRoom.objects.filter_xyz(xyz=xyz, **kwargs)

    def get_exit(self, xyz, name="north", **kwargs):
        """
        Get one or more exit object at coordinate.

        Args:
            xyz (tuple): X,Y,Z coordinate of the room the
                exit leads out of. '*' acts as a wildcard.
            name (str): The full name of the exit, e.g. 'north' or 'northwest'.
                The '*' acts as a wild card.

        Returns:
            Queryset: A queryset of XYZExit(s) found.

        """
        kwargs["db_key"] = name
        return XYZExit.objects.filter_xyz_exit(xyz=xyz, **kwargs)

    def maps_from_module(self, module_path):
        """
        Load map data from module. The loader will look for a dict XYMAP_DATA or a list of
        XYMAP_DATA_LIST (a list of XYMAP_DATA dicts). Each XYMAP_DATA dict should contain
        `{"xymap": mapstring, "zcoord": mapname/zcoord, "legend": dict, "prototypes": dict}`.

        Args:
            module_path (module_path): A python-path to a module containing
                map data as either `XYMAP_DATA` or `XYMAP_DATA_LIST` variables.

        Returns:
            list: List of zero, one or more xy-map data dicts loaded from the module.

        """
        map_data_list = variable_from_module(module_path, "XYMAP_DATA_LIST")
        if not map_data_list:
            map_data_list = [variable_from_module(module_path, "XYMAP_DATA")]
        # inject the python path in the map data
        for mapdata in map_data_list:
                self.log(f"Could not find or load map from {module_path}.")
                return
            mapdata["module_path"] = module_path
        return map_data_list

    def reload(self):
        """
        Reload and rebuild the grid. This is done on a server reload.

        """
        self.log("(Re)loading grid ...")
        self.ndb.grid = {}
        nmaps = 0
        loaded_mapdata = {}
        changed = []
        mapdata = self.db.map_data

            self.db.mapdata = mapdata = {}

        # generate all Maps - this will also initialize their components
        # and bake any pathfinding paths (or load from disk-cache)
        for zcoord, old_mapdata in mapdata.items():
            self.log(f"Loading map '{zcoord}'...")

            # we reload the map from module
            new_mapdata = loaded_mapdata.get(zcoord)
            if not new_mapdata:
                if "module_path" in old_mapdata:
                    for mapdata in self.maps_from_module(old_mapdata["module_path"]):
                        loaded_mapdata[mapdata["zcoord"]] = mapdata
                else:
                    # nowhere to reload from - use what we have
                    loaded_mapdata[zcoord] = old_mapdata

                new_mapdata = loaded_mapdata.get(zcoord)

            if new_mapdata != old_mapdata:
                self.log(f" XYMap data for Z='{zcoord}' has changed.")
                changed.append(zcoord)

            xymap = XYMap(dict(new_mapdata), Z=zcoord, xyzgrid=self)
            xymap.parse()
            xymap.calculate_path_matrix()
            self.ndb.grid[zcoord] = xymap
            nmaps += 1

        # re-store changed data
        for zcoord in changed:
            self.db.map_data[zcoord] = loaded_mapdata[zcoord]

        # store
        self.log(f"Loaded and linked {nmaps} map(s).")
        self.ndb.loaded = True

    def add_maps(self, *mapdatas):
        """
        Add map or maps to the grid.

        Args:
            *mapdatas (dict): Each argument is a dict structure
                `{"map": <mapstr>, "legend": <legenddict>, "name": <name>,
                "prototypes": <dict-of-dicts>, "module_path": <str>}`. The `prototypes are
                coordinate-specific overrides for nodes/links on the map, keyed with their
                (X,Y) coordinate within that map. The `module_path` is injected automatically
                by self.maps_from_module.

        Raises:
            RuntimeError: If mapdata is malformed.

        """
        for mapdata in mapdatas:
            zcoord = mapdata.get("zcoord")
            if not zcoord is not None:
                raise RuntimeError("XYZGrid.add_map data must contain 'zcoord'.")

            self.db.map_data[zcoord] = mapdata

    def remove_map(self, *zcoords, remove_objects=True):
        """
        Remove an XYmap from the grid.

        Args:
            *zoords (str): The zcoords/XYmaps to remove.
            remove_objects (bool, optional): If the synced database objects (rooms/exits) should
                be removed alongside this map.
        """
        for zcoord in zcoords:
            if zcoord in self.db.map_data:
                self.db.map_data.pop(zcoord)
            if remove_objects:
                # we can't batch-delete because we want to run the .delete
                # method that also wipes exits and moves content to save locations
                for xyzroom in XYZRoom.objects.filter_xyz(xyz=("*", "*", zcoord)):
                    xyzroom.delete()

        """
        Clear the entire grid, including database entities, then the grid too.

        """
        if mapdata:
            self.remove_map(*(zcoord for zcoord in self.db.map_data), remove_objects=True)

    def spawn(self, xyz=("*", "*", "*"), directions=None):
        """
        Create/recreate/update the in-game grid based on the stored Maps or for a specific Map
        or coordinate.

        Args:
            xyz (tuple, optional): An (X,Y,Z) coordinate, where Z is the name of the map. `'*'`
                acts as a wildcard.
            directions (list, optional): A list of cardinal directions ('n', 'ne' etc).
                Spawn exits only the given direction. If unset, all needed directions are spawned.

        Examples:
            - `xyz=('*', '*', '*')` (default) - spawn/update all maps.
            - `xyz=(1, 3, 'foo')` - sync a specific element of map 'foo' only.
            - `xyz=('*', '*', 'foo') - sync all elements of map 'foo'
            - `xyz=(1, 3, '*') - sync all (1,3) coordinates on all maps (rarely useful)
            - `xyz=(1, 3, 'foo')`, `direction='ne'` - sync only the north-eastern exit
                out of the specific node on map 'foo'.

        """
        x, y, z = xyz

        if z == wildcard:
            xymaps = self.grid
        elif self.ndb.grid and z in self.ndb.grid:
            xymaps = {z: self.grid[z]}
        else:
            raise RuntimeError(f"The 'z' coordinate/name '{z}' is not found on the grid.")

        # first build all nodes/rooms
        for zcoord, xymap in xymaps.items():
            self.log(f"spawning/updating nodes for Z='{zcoord}' ...")
            xymap.spawn_nodes(xy=(x, y))

        # next build all links between nodes (including between maps)
            self.log(f"spawning/updating links for Z='{zcoord}' ...")
            xymap.spawn_links(xy=(x, y), directions=directions)

def get_xyzgrid(print_errors=True):
    """
    Helper for getting the grid. This will create the XYZGrid global script if it didn't
    previously exist.

    Args:
        print_errors (bool, optional): Print errors directly to console rather than to log.

    """
    xyzgrid = XYZGrid.objects.all()
    if not xyzgrid:
        # create a new one
        xyzgrid, err = XYZGrid.create("XYZGrid")
        if err:
            raise RuntimeError(err)
        xyzgrid.reload()
        return xyzgrid
    elif len(xyzgrid) > 1:
        print(
            "Warning: More than one XYZGrid instance were found. This is an error and "
            "only the first one will be used. Delete the other one(s) manually."
        )
    xyzgrid = xyzgrid[0]
    try:
        if not xyzgrid.ndb.loaded:
            xyzgrid.reload()
        # raise   # debug
        if print_errors:
            print(err)
        else:
            xyzgrid.log(str(err))
    return xyzgrid

"""
XYZ-aware rooms and exits.

These are intended to be used with the XYZgrid - which interprets the `Z` 'coordinate' as
different (named) 2D XY  maps. But if not wanting to use the XYZgrid gridding, these can also be
used as stand-alone XYZ-coordinate-aware rooms.

"""

from evennia.objects.manager import ObjectManager
from evennia.objects.objects import DefaultExit, DefaultRoom

# name of all tag categories. Note that the Z-coordinate is
# the `map_name` of the XYZgrid
MAP_X_TAG_CATEGORY = "room_x_coordinate"
MAP_Y_TAG_CATEGORY = "room_y_coordinate"
MAP_Z_TAG_CATEGORY = "room_z_coordinate"

MAP_XDEST_TAG_CATEGORY = "exit_dest_x_coordinate"
MAP_YDEST_TAG_CATEGORY = "exit_dest_y_coordinate"
MAP_ZDEST_TAG_CATEGORY = "exit_dest_z_coordinate"

GET_XYZGRID = None

CLIENT_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH

class XYZManager(ObjectManager):
    """
    This is accessed as `.objects` on the coordinate-aware typeclasses (`XYZRoom`, `XYZExit`). It
    has all the normal Object/Room manager methods (filter/get etc) but also special helpers for
    efficiently querying the room in the database based on XY coordinates.

    """

    def filter_xyz(self, xyz=("*", "*", "*"), **kwargs):
        """
        Filter queryset based on XYZ position on the grid. The Z-position is the name of the XYMap
        Set a coordinate to `'*'` to act as a wildcard (setting all coords to `*` will thus find
        *all* XYZ rooms). This will also find children of XYZRooms on the given coordinates.

        Kwargs:
            xyz (tuple, optional): A coordinate tuple (X, Y, Z) where each element is either
                an `int` or `str`. The character `'*'` acts as a wild card. Note that
                the `Z`-coordinate is the name of the map (case-sensitive) in the XYZgrid contrib.
            **kwargs: All other kwargs are passed on to the query.

        Returns:
            django.db.queryset.Queryset: A queryset that can be combined
            with further filtering.

        """
        x, y, z = xyz

        return (
            self.filter_family(**kwargs)
            .filter(
                Q()
                if x == wildcard
                else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)
            )
            .filter(
                Q()
                if y == wildcard
                else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)
            )
            .filter(
                Q()
                if z == wildcard
                else Q(db_tags__db_key__iexact=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)
            )
        )

    def get_xyz(self, xyz=(0, 0, "map"), **kwargs):
        """
        Always return a single matched entity directly. This accepts no `*`-wildcards.
        This will also find children of XYZRooms on the given coordinates.

        Kwargs:
            xyz (tuple): A coordinate tuple of `int` or `str` (not `'*'`, no wildcards are
                allowed in get).  The `Z`-coordinate acts as the name (case-sensitive) of the map in
                the XYZgrid contrib.

        Returns:
            XYRoom: A single room instance found at the combination of x, y and z given.

        Raises:
            XYZRoom.DoesNotExist: If no matching query was found.
            XYZRoom.MultipleObjectsReturned: If more than one match was found (which should not
                possible with a unique combination of x,y,z).

        """
        # filter by tags, then figure out of we got a single match or not
        query = self.filter_xyz(xyz=xyz, **kwargs)
        ncount = query.count()
        if ncount == 1:
            return query.first()

        # error - mimic default get() behavior but with a little more info
        x, y, z = xyz
        inp = f"Query: xyz=({x},{y},{z}), " + ",".join(
            f"{key}={val}" for key, val in kwargs.items()
        )
        if ncount > 1:
            raise self.model.MultipleObjectsReturned(inp)
        else:
            raise self.model.DoesNotExist(inp)

class XYZExitManager(XYZManager):
    """
    Used by Exits.
    Manager that also allows searching for destinations based on XY coordinates.

    """

    def filter_xyz_exit(self, xyz=("*", "*", "*"), xyz_destination=("*", "*", "*"), **kwargs):
        """
        Used by exits (objects with a source and -destination property).
        Find all exits out of a source or to a particular destination. This will also find
        children of XYZExit on the given coords..

        Kwargs:
            xyz (tuple, optional): A coordinate (X, Y, Z) for the source location. Each
                element is either an `int` or `str`. The character `'*'` is used as a wildcard -
                so setting all coordinates to the wildcard will return *all* XYZExits.
            xyz_destination (tuple, optional): Same as `xyz` but for the destination of the
                exit.

        Returns:

        Notes:
            Depending on what coordinates are set to `*`, this can be used to
            e.g. find all exits in a room, or leading to a room or even to rooms
            in a particular X/Y row/column.

            In the XYZgrid, `z_source != z_destination` means a _transit_ between different maps.

        """
        x, y, z = xyz
        xdest, ydest, zdest = xyz_destination

        return (
            .filter(
                Q()
            )
            .filter(
                Q()
            )
            .filter(
                Q()
            )
            .filter(
                Q()
                if xdest == wildcard
                else Q(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY)
            )
            .filter(
                Q()
                if ydest == wildcard
                else Q(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY)
            )
            .filter(
                Q()
                if zdest == wildcard
                else Q(
                    db_tags__db_key__iexact=str(zdest), db_tags__db_category=MAP_ZDEST_TAG_CATEGORY
                )
            )
        )

    def get_xyz_exit(self, xyz=(0, 0, "map"), xyz_destination=(0, 0, "map"), **kwargs):
        """
        Used by exits (objects with a source and -destination property). Get a single
        exit. All source/destination coordinates (as well as the map's name) are required.
        This will also find children of XYZExits on the given coords.

        Kwargs:
                element is either an `int` or `str` (not `*`, no wildcards are allowed for get).
            xyz_destination_coord (tuple, optional): Same as the `xyz` but for the destination of
                the exit.

        Returns:
            XYZExit: A single exit instance found at the combination of x, y and xgiven.

        Raises:
            XYZExit.DoesNotExist: If no matching query was found.
            XYZExit.MultipleObjectsReturned: If more than one match was found (which should not
                be possible with a unique combination of x,y,x).

        Notes:
            All coordinates are required.

        """
        x, y, z = xyz
        # mimic get_family
        paths = [self.model.path] + [
            "%s.%s" % (cls.__module__, cls.__name__) for cls in self._get_subclasses(self.model)
        ]
        kwargs["db_typeclass_path__in"] = paths

        try:
            return (
                self.filter(db_tags__db_key__iexact=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)
                .filter(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)
                .filter(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)
                .filter(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY)
                .filter(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY)
                .filter(
                )
                .get(**kwargs)
            )
            inp = f"xyz=({x},{y},{z}),xyz_destination=({xdest},{ydest},{zdest})," + ",".join(
                f"{key}={val}" for key, val in kwargs.items()
            )
            raise self.model.DoesNotExist(
                f"{self.model.__name__} matching query {inp} does not exist."
            )

class XYZRoom(DefaultRoom):
    """
    A game location aware of its XYZ-position.

    Special properties:
        map_display (bool): If the return_appearance of the room should
            show the map or not.
        map_mode (str): One of 'nodes' or 'scan'. See `return_apperance`
            for examples of how they differ.
        map_visual_range (int): How far on the map one can see. This is a
            fixed value here, but could also be dynamic based on skills,
            light etc.
        map_character_symbol (str): The character symbol to use to show
            the character position. Can contain color info. Default is
            the @-character.
        map_area_client (bool): If True, map area will always fill the entire
            client width. If False, the map area's width will vary with the
            width of the currently displayed location description.
        map_fill_all (bool): I the map area should fill the client width or not.
        map_separator_char (str): The char to use to separate the map area from
            the room description.

    """

    # makes the `room.objects.filter_xymap` available
    objects = XYZManager()

    # default settings for map visualization
    map_display = True
    map_mode = "nodes"  # or 'scan'
    map_visual_range = 2
    map_character_symbol = "|g@|n"
    map_align = "c"
    map_target_path_style = "|y{display_symbol}|n"
    map_fill_all = True
    map_separator_char = "|x~|n"

        return repr(self)

        x, y, z = self.xyz
        return f"<XYZRoom '{self.db_key}', XYZ=({x},{y},{z})>"

    @property
    def xyz(self):
        if not hasattr(self, "_xyz"):
            x = self.tags.get(category=MAP_X_TAG_CATEGORY, return_list=False)
            y = self.tags.get(category=MAP_Y_TAG_CATEGORY, return_list=False)
            z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False)
            if x is None or y is None or z is None:
                # don't cache unfinished coordinate (probably tags have not finished saving)
                return tuple(
                    int(coord) if coord is not None and coord.lstrip("-").isdigit() else coord
                    for coord in (x, y, z)
                )
            # cache result, convert to correct types (tags are strings)
            self._xyz = tuple(
                int(coord) if coord.lstrip("-").isdigit() else coord for coord in (x, y, z)
            )

        return self._xyz

    @property
    def xyzgrid(self):
        global GET_XYZGRID
        if not GET_XYZGRID:
            from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
        return GET_XYZGRID()

    @property
    def xymap(self):
        if not hasattr(self, "_xymap"):
            xyzgrid = self.xyzgrid
            _, _, Z = self.xyz
            self._xymap = xyzgrid.get_map(Z)
        return self._xymap

    def create(cls, key, account=None, xyz=(0, 0, "map"), **kwargs):
        """
        Creation method aware of XYZ coordinates.

        Args:
            key (str): New name of object to create.
            account (Account, optional): Any Account to tie to this entity (usually not used for
                rooms).
            xyz (tuple, optional): A 3D coordinate (X, Y, Z) for this room's location on a
                map grid. Each element can theoretically be either `int` or `str`, but for the
                XYZgrid, the X, Y are always integers while the `Z` coordinate is used for the
                map's name.
            **kwargs: Will be passed into the normal `DefaultRoom.create` method.

        Returns:
            room (Object): A newly created Room of the given typeclass.

        Notes:
            The (X, Y, Z) coordinate must be unique across the game. If trying to create
            a room at a coordinate that already exists, an error will be returned.

        """
        try:
            x, y, z = xyz
            return None, [
                f"XYRroom.create got `xyz={xyz}` - needs a valid (X,Y,Z) "
                "coordinate of ints/strings."
            ]

        existing_query = cls.objects.filter_xyz(xyz=(x, y, z))
        if existing_query.exists():
            existing_room = existing_query.first()
                f"XYRoom XYZ=({x},{y},{z}) already exists "
                f"(existing room is named '{existing_room.db_key}')!"
            ]

        tags = (
            (str(x), MAP_X_TAG_CATEGORY),
            (str(y), MAP_Y_TAG_CATEGORY),
            (str(z), MAP_Z_TAG_CATEGORY),
        )

        return DefaultRoom.create(key, account=account, tags=tags, typeclass=cls, **kwargs)

        """
        Shows both the #dbref and the xyz coord to staff.

        Args:

        Returns:
                including the DBREF and XYZ coord if this user is
                privileged to control the room.

        """
            x, y, z = self.xyz
            return f"{self.name}[#{self.id}({x},{y},{z})]"

        """
        Displays the map in addition to the room description

        Args:
            looker (Object): The one looking.

            map_display (bool): Turn on/off map display.
            map_visual_range (int): How 'far' one can see on the map. For
                'nodes' mode, this is how many connected nodes away, for
                'scan' mode, this is number of characters away on the map.
                Default is a visual range of 2 (nodes).
            map_mode (str): One of 'node' (default) or 'scan'.
            map_character_symbol (str): The character symbol to use. Defaults to '@'.
                This can also be colored with standard color tags. Set to `None`
                to just show the current node.

        Examples:

            Assume this is the full map (where '@' is the character location):
            ::
                #----------------#
                |                |
                |                |
                # @------------#-#
                |                |

            This is how it will look in 'nodes' mode with `visual_range=2`:
            ::
                  @------------#-#

            And in 'scan' mode with `visual_range=2`:
            ::
                |
                |
                # @--
                |
                #----

        Notes:
            The map kwargs default to values with the same names set on the
            XYZRoom class; these can be changed by overriding the room.

            We return the map display as a separate msg() call here, in order
            to make it easier to break this out into a client pane etc. The
            map is tagged with type='xymap'.

        """

        # normal get_appearance of a room
        room_desc = super().return_appearance(looker, **kwargs)

        # get current xymap
        xyz = self.xyz
        xymap = self.xyzgrid.get_map(xyz[2])

        if xymap and kwargs.get("map_display", xymap.options.get("map_display", self.map_display)):
            # show the near-area map.
            map_character_symbol = kwargs.get(
                "map_character_symbol",
                xymap.options.get("map_character_symbol", self.map_character_symbol),
            )
            map_visual_range = kwargs.get(
                "map_visual_range", xymap.options.get("map_visual_range", self.map_visual_range)
            )
            map_mode = kwargs.get("map_mode", xymap.options.get("map_mode", self.map_mode))
            map_align = kwargs.get("map_align", xymap.options.get("map_align", self.map_align))
            map_target_path_style = kwargs.get(
                "map_target_path_style",
                xymap.options.get("map_target_path_style", self.map_target_path_style),
            )
            map_area_client = kwargs.get(
                "map_fill_all", xymap.options.get("map_fill_all", self.map_fill_all)
            )
            map_separator_char = kwargs.get(
                "map_separator_char",
                xymap.options.get("map_separator_char", self.map_separator_char),
            )

            sessions = looker.sessions.get()
                client_width, _ = sessions[0].get_client_size()
            else:
                client_width = CLIENT_DEFAULT_WIDTH

            map_width = xymap.max_x

            if map_area_client:
                display_width = client_width
            else:
                display_width = max(map_width, max(len(line) for line in room_desc.split("\n")))

            # align map
            map_indent = 0
            sep_width = display_width
            if map_align == "r":
                map_indent = max(0, display_width - map_width)
            elif map_align == "c":
                map_indent = max(0, (display_width - map_width) // 2)

            # data set by the goto/path-command, for displaying the shortest path
            path_data = looker.ndb.xy_path_data
            target_xy = path_data.target.xyz[:2] if path_data else None

            # get visual range display from map
            map_display = xymap.get_visual_range(
                (xyz[0], xyz[1]),
                dist=map_visual_range,
                mode=map_mode,
                target=target_xy,
                target_path_style=map_target_path_style,
                character=map_character_symbol,
                max_size=(display_width, None),
                indent=map_indent,
            )
            sep = map_separator_char * sep_width
            map_display = f"{sep}|n\n{map_display}\n{sep}"

            # echo directly to make easier to separate in client
            looker.msg(text=(map_display, {"type": "xymap"}), options=None)

        return room_desc

class XYZExit(DefaultExit):
    """
    An exit that is aware of the XYZ coordinate system.

    """

    objects = XYZExitManager()

        xd, yd, zd = self.xyz_destination
        return f"<XYZExit '{self.db_key}', XYZ=({x},{y},{z})->({xd},{yd},{zd})>"

    @property

    @property
                # don't cache yet unfinished coordinate
                return (x, y, z)
            # cache result
            self._xyz = (x, y, z)

    @property
    def xyz_destination(self):
        if not hasattr(self, "_xyz_destination"):
            xd = self.tags.get(category=MAP_XDEST_TAG_CATEGORY, return_list=False)
            yd = self.tags.get(category=MAP_YDEST_TAG_CATEGORY, return_list=False)
            zd = self.tags.get(category=MAP_ZDEST_TAG_CATEGORY, return_list=False)
            if xd is None or yd is None or zd is None:
                # don't cache unfinished coordinate
                return (xd, yd, zd)
            self._xyz_destination = (xd, yd, zd)
        return self._xyz_destination

    def create(
        cls,
        key,
        account=None,
        xyz=(0, 0, "map"),
        xyz_destination=(0, 0, "map"),
        location=None,
        destination=None,
        **kwargs,
    ):
        """
        Creation method aware of coordinates.

        Args:
            account (Account, optional): Any Account to tie to this entity (unused for exits).
            xyz (tuple or None, optional): A 3D coordinate (X, Y, Z) for this room's location
                on a map grid.  Each element can theoretically be either `int` or `str`, but for the
                XYZgrid contrib, the X, Y are always integers while the `Z` coordinate is used for
                the map's name. Set to `None` if instead using a direct room reference with
                `location`.
            xyz_destination (tuple, optional): The XYZ coordinate of the place the exit
                leads to. Will be ignored if `destination` is given directly.
            location (Object, optional): If given, overrides `xyz` coordinate. This can be used
                to place this exit in any room, including non-XYRoom type rooms.
            destination (Object, optional): If given, overrides `xyz_destination`. This can
                be any room (including non-XYRooms) and is not checked for XYZ coordinates.

        Returns:
            tuple: A tuple `(exit, errors)`, where the errors is a list containing all found
                errors (in which case the returned exit will be `None`).

        """
        tags = []
            source = location
        else:
            try:
                x, y, z = xyz
                return None, ["XYExit.create need either `xyz=(X,Y,Z)` coordinate or a `location`."]
            else:
                source = XYZRoom.objects.get_xyz(xyz=(x, y, z))
                tags.extend(
                    (
                        (str(x), MAP_X_TAG_CATEGORY),
                        (str(y), MAP_Y_TAG_CATEGORY),
                        (str(z), MAP_Z_TAG_CATEGORY),
                    )
                )
        if destination:
            dest = destination
        else:
            try:
                xdest, ydest, zdest = xyz_destination
                return None, [
                    "XYExit.create need either `xyz_destination=(X,Y,Z)` coordinate "
                    "or a `destination`."
                ]
            else:
                dest = XYZRoom.objects.get_xyz(xyz=(xdest, ydest, zdest))
                    (
                        (str(xdest), MAP_XDEST_TAG_CATEGORY),
                        (str(ydest), MAP_YDEST_TAG_CATEGORY),
                        (str(zdest), MAP_ZDEST_TAG_CATEGORY),
                    )
                )

        return DefaultExit.create(
            key, source, dest, account=account, tags=tags, typeclass=cls, **kwargs
        )

"""
Buffs - Tegiminis 2022

A buff is a timed object, attached to a game entity, that modifies values, triggers 
code, or both. It is a common design pattern in RPGs, particularly action games.

This contrib gives you a buff handler to apply to your objects, a buff class to extend them,
a sample property class to show how to automatically check modifiers, some sample buffs to learn from,
and a command which applies buffs.

Assign the handler to a property on the object, like so.

```python
@lazy_property
def buffs(self) -> BuffHandler:
    return BuffHandler(self)```

## Using the Handler

To make use of the handler, you will need:

- Some buffs to add. You can create these by extending the `BaseBuff` class from this module. You can see some examples in `samplebuffs.py`.
- A way to add buffs to the handler. You can see a basic example of this in the `CmdBuff` command in this module.

### Applying a Buff

Call the handler `add(BuffClass)` method. This requires a class reference, and also contains a number of 
optional arguments to customize the buff's duration, stacks, and so on.

```python
self.buffs.add(StrengthBuff)    # A single stack of StrengthBuff with normal duration
self.buffs.add(DexBuff, stacks=3, duration=60)  # Three stacks of DexBuff, with a duration of 60 seconds
self.buffs.add(ReflectBuff, to_cache={'reflect': 0.5})  # A single stack of ReflectBuff, with an extra cache value
```

### Modify

Call the handler `check(value, stat)` method wherever you want to see the modified value. 
This will return the value, modified by and relevant buffs on the handler's owner (identified by 
the `stat` string). For example:

```python
# The method we call to damage ourselves
def take_damage(self, source, damage):
    _damage = self.buffs.check(damage, 'taken_damage')
    self.db.health -= _damage
```

### Trigger

Call the handler `trigger(triggerstring)` method wherever you want an event call. This 
will call the `at_trigger` hook method on all buffs with the relevant trigger.

```python
def Detonate(BaseBuff):
    ...
    triggers = ['take_damage']
    def at_trigger(self, trigger, *args, **kwargs)
        self.owner.take_damage(100)
        self.remove()

def Character(Character):
    ...
    def take_damage(self, source, damage):
        self.buffs.trigger('take_damage')
        self.db.health -= _damage
```

### Tick

Ticking a buff happens automatically once applied, as long as the buff's `tickrate` is more than 0.

```python
def Poison(BaseBuff):
    ...
    tickrate = 5
    def at_tick(self, initial=True, *args, **kwargs):
        _dmg = self.dmg * self.stacks
        if not initial:
            self.owner.location.msg_contents(
                "Poison courses through {actor}'s body, dealing {damage} damage.".format(
                    actor=self.owner.named, damage=_dmg
                )
            )
```

## Buffs

A buff is a class which contains a bunch of immutable data about itself - such as tickrate, triggers, refresh rules, and
so on - and which merges mutable data in from the cache when called.

Buffs are always instanced when they are called for a method. To access a buff's properties and methods, you should do so through
this instance, rather than directly manipulating the buff cache on the object. You can modify a buff's cache through various handler
methods instead.

You can see all the features of the `BaseBuff` class below, or browse `samplebuffs.py` to see how to create some common buffs. Buffs have
many attributes and hook methods you can overload to create complex, interrelated buffs.

"""
from random import random

from evennia import Command
from evennia.utils import search, utils

class BaseBuff:
    key = "template"  # The buff's unique key. Will be used as the buff's key in the handler
    name = "Template"  # The buff's name. Used for user messaging
    flavor = "Template"  # The buff's flavor text. Used for user messaging
    visible = True  # If the buff is considered "visible" to the "view" method

    triggers = []  # The effect's trigger strings, used for functions.

    handler = None
    start = 0

    duration = -1  # Default buff duration; -1 for permanent, 0 for "instant", >0 normal
    playtime = False  # Does this buff autopause when owning object is unpuppeted?

    refresh = True  # Does the buff refresh its timer on application?
    unique = True  # Does the buff overwrite existing buffs with the same key on the same target?
    maxstacks = 1  # The maximum number of stacks the buff can have. If >1, this buff will stack.
    stacks = 1  # Used as the default when applying this buff if no or negative stacks were specified (min: 1)
    tickrate = 0  # How frequent does this buff tick, in seconds (cannot be lower than 1)

    mods = []  # List of mod objects. See Mod class below for more detail
    cache = {}

    @property
    def ticknum(self):
        """Returns how many ticks this buff has gone through as an integer."""
        x = (time.time() - self.start) / max(1, self.tickrate)
        return int(x)

    @property
    def owner(self):
        """Return this buff's owner (the object its handler is attached to)"""
        if not self.handler:
        return self.handler.owner

    @property
    def timeleft(self):
        """Returns how much time this buff has left. If -1, it is permanent."""
        _tl = 0
        if not self.start:
            _tl = self.duration
        else:
            _tl = max(-1, self.duration - (time.time() - self.start))
        return _tl

    @property
    def ticking(self) -> bool:
        """Returns if this buff ticks or not (tickrate => 1)"""
        return self.tickrate >= 1

    @property
    def stacking(self) -> bool:
        """Returns if this buff stacks or not (maxstacks > 1)"""
        return self.maxstacks > 1

    def __init__(self, handler, buffkey, cache) -> None:
        """
        Args:
            handler:    The handler this buff is attached to
            buffkey:    The key this buff uses on the cache
            cache:      The cache dictionary (what you get if you use `handler.buffcache.get(key)`)
        """
        required = {"handler": handler, "buffkey": buffkey, "cache": cache}
        self.__dict__.update(cache)
        self.__dict__.update(required)
        # Init hook
        self.at_init()

    def __setattr__(self, attr, value):
        if attr in self.cache:
            if attr == "tickrate":
                value = max(0, value)
            self.handler.buffcache[self.buffkey][attr] = value
        super().__setattr__(attr, value)

    def conditional(self, *args, **kwargs):
        """Hook function for conditional evaluation.

        This must return True for a buff to apply modifiers, trigger effects, or tick."""

    # region helper methods
    def remove(self, loud=True, expire=False, context=None):
        """Helper method which removes this buff from its handler. Use dispel if you are dispelling it instead.

        Args:
            loud:   (optional) Whether to call at_remove or not (default: True)
            expire: (optional) Whether to call at_expire or not (default: False)
            delay:  (optional) How long you want to delay the remove call for
            context:    (optional) A dictionary you wish to pass to the at_remove/at_expire method as kwargs
        """
        if not context:
            context = {}
        self.handler.remove(self.buffkey, loud=loud, expire=expire, context=context)

    def dispel(self, loud=True, delay=0, context=None):
        """Helper method which dispels this buff (removes and calls at_dispel).

        Args:
            context:    (optional) A dictionary you wish to pass to the at_remove/at_dispel method as kwargs
        """
        self.handler.remove(self.buffkey, loud=loud, dispel=True, delay=delay, context=context)

    def pause(self, context=None):
        """Helper method which pauses this buff on its handler.

        Args:
            context:    (optional) A dictionary you wish to pass to the at_pause method as kwargs"""
        self.handler.pause(self.buffkey, context)

    def unpause(self, context=None):
        """Helper method which unpauses this buff on its handler.

        Args:
            context:    (optional) A dictionary you wish to pass to the at_unpause method as kwargs
        """
        self.handler.unpause(self.buffkey, context)

        """Resets the buff start time as though it were just applied; functionally identical to a refresh"""
        self.start = time.time()
        self.handler.buffcache[self.buffkey]["start"] = time.time()

    def update_cache(self, to_cache: dict):
        """Updates this buff's cache using the given values, both internally (this instance) and on the handler.

        Args:
            to_cache:   The dictionary of values you want to add to the cache"""
        if not isinstance(to_cache, dict):
            raise TypeError
        _cache = dict(self.handler.buffcache[self.buffkey])
        _cache.update(to_cache)
        self.cache = _cache
        self.handler.buffcache[self.buffkey] = _cache

    # endregion

    # region hook methods
    def at_init(self, *args, **kwargs):
        """Hook function called when this buff object is initialized."""
        pass

    def at_apply(self, *args, **kwargs):
        """Hook function to run when this buff is applied to an object."""
        pass

    def at_remove(self, *args, **kwargs):
        """Hook function to run when this buff is removed from an object."""
        pass

    def at_dispel(self, *args, **kwargs):
        """Hook function to run when this buff is dispelled from an object (removed by someone other than the buff holder)."""
        pass

    def at_expire(self, *args, **kwargs):
        """Hook function to run when this buff expires from an object."""
        pass

    def at_pre_check(self, *args, **kwargs):
        """Hook function to run before this buff's modifiers are checked."""
        pass

    def at_post_check(self, *args, **kwargs):
        """Hook function to run after this buff's mods are checked."""
        pass

    def at_trigger(self, trigger: str, *args, **kwargs):
        """Hook for the code you want to run whenever the effect is triggered.
        Passes the trigger string to the function, so you can have multiple
        triggers on one buff."""
        pass

    def at_tick(self, initial: bool, *args, **kwargs):
        """Hook for actions that occur per-tick, a designer-set sub-duration.
        `initial` tells you if it's the first tick that happens (when a buff is applied)."""
        pass

    def at_pause(self, *args, **kwargs):
        """Hook for when this buff is paused"""
        pass

    def at_unpause(self, *args, **kwargs):
        """Hook for when this buff is unpaused."""
        pass

class Mod:
    """A single stat mod object. One buff or trait can hold multiple mods, for the same or different stats."""

    stat = "null"  # The stat string that is checked to see if this mod should be applied
    value = 0  # Buff's value
    perstack = 0  # How much additional value is added to the buff per stack
    modifier = "add"  # The modifier the buff applies. 'add' or 'mult'

    def __init__(self, stat: str, modifier: str, value, perstack=0.0) -> None:
        """
        Args:
            stat:       The stat the buff affects. Normally matches the object attribute name
            mod:        The modifier the buff applies. "add" for add/sub or "mult" for mult/div
            value:      The value of the modifier
            perstack:   How much is added to the base, per stack (including first)."""
        self.stat = stat
        self.modifier = modifier
        self.value = value
        self.perstack = perstack

class BuffHandler:
    ownerref = None
    dbkey = "buffs"
    autopause = False
    _owner = None

    def __init__(self, owner, dbkey=dbkey, autopause=autopause):
        """
        Args:
            owner:  The object this handler is attached to
            dbkey:  (optional) The string key of the db attribute to use for the buff cache
            autopause:  (optional) Whether this handler autopauses playtime buffs on owning object's unpuppet
        """
        self.ownerref = owner.dbref
        self.dbkey = dbkey
        self.autopause = autopause
        if autopause:
            self._validate_state()
            signals.SIGNAL_OBJECT_POST_UNPUPPET.connect(self._pause_playtime)
            signals.SIGNAL_OBJECT_POST_PUPPET.connect(self._unpause_playtime)

    # region properties
    @property
        """The object this handler is attached to."""
        if self.ownerref:
            _owner = search.search_object(self.ownerref)
        if _owner:
            return _owner[0]
        else:

    @property
    def buffcache(self):
        """The object attribute we use for the buff cache. Auto-creates if not present."""
        if not self.owner:
            return {}
        if not self.owner.attributes.has(self.dbkey):
            self.owner.attributes.add(self.dbkey, {})
        return self.owner.attributes.get(self.dbkey)

    @property
    def traits(self):
        """All buffs on this handler that modify a stat."""
        _cache = self.all
        _t = {k: buff for k, buff in _cache.items() if buff.mods}
        return _t

    @property
    def effects(self):
        """All buffs on this handler that trigger off an event."""
        _e = {k: buff for k, buff in _cache.items() if buff.triggers}
        return _e

    @property
    def playtime(self):
        """All buffs on this handler that only count down during active playtime."""
        _pt = {k: buff for k, buff in _cache.items() if buff.playtime}
        return _pt

    @property
    def paused(self):
        """All buffs on this handler that are paused."""
        _p = {k: buff for k, buff in _cache.items() if buff.paused}
        return _p

    @property
    def expired(self):
        """All buffs on this handler that have expired (no duration or no stacks)."""
        _e = {
            k: buff
            for k, buff in _cache.items()
            if not buff.paused
            if buff.duration > -1
            if buff.duration < time.time() - buff.start
        }
        _nostacks = {k: buff for k, buff in _cache.items() if buff.stacks <= 0}
        _e.update(_nostacks)
        return _e

    @property
    def visible(self):
        """All buffs on this handler that are visible."""
        _v = {k: buff for k, buff in _cache.items() if buff.visible}
        return _v

    @property
        """Returns dictionary of instanced buffs equivalent to ALL buffs on this handler,
        regardless of state, type, or anything else."""
        _a = self.get_all()
        return _a

    # region methods
    def add(
        self,
        buff: BaseBuff,
        key: str = None,
        stacks=0,
        duration=None,
        source=None,
        to_cache=None,
        context=None,
        *args,
        **kwargs,
    ):
        """Add a buff to this object, respecting all stacking/refresh/reapplication rules. Takes
        a number of optional parameters to allow for customization.

        Args:
            buff:       The buff class type you wish to add
            key:        (optional) The key you wish to use for this buff; overrides defaults
            stacks:     (optional) The number of stacks you want to add, if the buff is stacking
            duration:   (optional) The amount of time, in seconds, you want the buff to last; overrides defaults
            source:     (optional) The source of this buff. (default: None)
            to_cache:   (optional) A dictionary to store in the buff's cache; does not overwrite default cache keys
            context:    (optional) A dictionary you wish to pass to the at_apply method as kwargs
        """
        if not isinstance(buff, type):
            raise ValueError
        b = {}
        _context = dict(context)

        # Initial cache updating, starting with the class cache attribute and/or to_cache
        if buff.cache:
            b = dict(buff.cache)
        if to_cache:
            b.update(dict(to_cache))

        # Guarantees we stack either at least 1 stack or whatever the class stacks attribute is
        if stacks < 1:
            stacks = min(1, buff.stacks)

        # Create the buff dict that holds a reference and all runtime information.
        b.update(
            {
                "ref": buff,
                "start": time.time(),
                "duration": buff.duration,
                "tickrate": buff.tickrate,
                "prevtick": time.time(),
                "paused": False,
                "stacks": stacks,
                "source": source,
            }
        )

        # Generate the buffkey from the object's dbref and the default buff key.
        # This is the actual key the buff uses on the dictionary
        buffkey = key
        if not buffkey:
            if source:
                mix = str(source.dbref).replace("#", "")
            elif not (buff.unique or buff.refresh) or not source:
                mix = "_ufrf" + str(int((random() * 999999) * 100000))

            buffkey = buff.key if buff.unique is True else buff.key + mix

        # Rules for applying over an existing buff
        if buffkey in self.buffcache.keys():
            existing = dict(self.buffcache[buffkey])
            # Stacking
            if buff.maxstacks > 1:
                b["stacks"] = min(existing["stacks"] + stacks, buff.maxstacks)
            elif buff.maxstacks < 1:
                b["stacks"] = existing["stacks"] + stacks
            # refresh rule for uniques
            if not buff.refresh:
                b["duration"] = existing["duration"]
            # Carrying over old arbitrary cache values
            cur_cache = {k: v for k, v in existing.items() if k not in b.keys()}
            b.update(cur_cache)
        # Setting overloaded duration
        if duration:
            b["duration"] = duration

        # Apply the buff!
        self.buffcache[buffkey] = b

        # Create the buff instance and run the on-application hook method
        instance: BaseBuff = buff(self, buffkey, b)
        instance.at_apply(**_context)
        if instance.ticking:
            tick_buff(self, buffkey, _context)

        # Clean up the buff at the end of its duration through a delayed cleanup call
        if b["duration"] > -1:
            utils.delay(b["duration"], self.cleanup, persistent=True)

    # region removers
    def remove(self, key, stacks=0, loud=True, dispel=False, expire=False, context=None):
        """Remove a buff or effect with matching key from this object. Normally calls at_remove,
        calls at_expire if the buff expired naturally, and optionally calls at_dispel. Can also
        remove stacks instead of the entire buff (still calls at_remove). Typically called via a helper method
        on the buff instance, or other methods on the handler.

        Args:
            key:        The buff key
            loud:       (optional) Calls at_remove when True. (default: True)
            dispel:     (optional) Calls at_dispel when True. (default: False)
            expire:     (optional) Calls at_expire when True. (default: False)
            context:    (optional) A dictionary you wish to pass to the at_remove/at_dispel/at_expire method as kwargs
        """
        if key not in self.buffcache:
            return

        buff: BaseBuff = self.buffcache[key]
        instance: BaseBuff = buff["ref"](self, key, buff)

        if loud:
            if dispel:
                instance.at_dispel(**context)
            elif expire:
                instance.at_expire(**context)
            instance.at_remove(**context)

        del instance
        if not stacks:
            del self.buffcache[key]
        elif stacks:
            self.buffcache[key]["stacks"] -= stacks
            if self.buffcache[key]["stacks"] <= 0:
                del self.buffcache[key]

    def remove_by_type(
        self,
        bufftype: BaseBuff,
        loud=True,
        dispel=False,
        expire=False,
    ):
        """Removes all buffs of a specified type from this object. Functionally similar to remove, but takes a type instead.

        Args:
            bufftype:   The buff class to remove
        """
        _remove = self.get_by_type(bufftype)
        if not _remove:
            return
        self._remove_via_dict(_remove, loud, dispel, expire, context)

    def remove_by_stat(
        self,
        stat,
    ):
        """Removes all buffs modifying the specified stat from this object.

        Args:
            stat:       The stat string to search for
        """
        _remove = self.get_by_stat(stat)
            return

    def remove_by_trigger(
        self,
        trigger,
    ):
        """Removes all buffs with the specified trigger from this object.

        Args:
            trigger:    The stat string to search for
        """
        _remove = self.get_by_trigger(trigger)
            return

    def remove_by_source(
        self,
        source,
    ):
        """Removes all buffs from the specified source from this object.

        Args:
            source:     The source to search for
        """
        _remove = self.get_by_source(source)
            return

    def remove_by_cachevalue(
        self,
        key,
        value=None,
    ):
        """Removes all buffs with the cachevalue from this object. Functionally similar to remove, but checks the buff's cache values instead.

        Args:
            key:         The key of the cache value to check
            value:      (optional) The value to match to. If None, merely checks to see if the value exists
        """
        _remove = self.get_by_cachevalue(key, value)
            return

    def clear(self, loud=True, dispel=False, expire=False, context=None):
        """Removes all buffs on this handler"""
        cache = self.all
        self._remove_via_dict(cache, loud, dispel, expire, context)

    # region getters
    def get(self, key: str):
        """If the specified key is on this handler, return the instanced buff. Otherwise return None.
        You should delete this when you're done with it, so that garbage collection doesn't have to.

        Args:
            key:    The key for the buff you wish to get"""
        buff = self.buffcache.get(key)
        if buff:
            return buff["ref"](self, key, buff)
        else:

    def get_all(self):
        """Returns a dictionary of instanced buffs (all of them) on this handler in the format {buffkey: instance}"""
        _cache = dict(self.buffcache)
        if not _cache:
            return {}
        return {k: buff["ref"](self, k, buff) for k, buff in _cache.items()}

    def get_by_type(self, buff: BaseBuff, to_filter=None):
        """Finds all buffs matching the given type.

        Args:
            buff:       The buff class to search for
            to_filter:  (optional) A dictionary you wish to slice. If not provided, uses the whole buffcache.

        Returns a dictionary of instanced buffs of the specified type in the format {buffkey: instance}.
        """
        _cache = self.get_all() if not to_filter else to_filter
        return {k: _buff for k, _buff in _cache.items() if isinstance(_buff, buff)}

    def get_by_stat(self, stat: str, to_filter=None):
        """Finds all buffs which contain a Mod object that modifies the specified stat.

        Args:
            stat:       The string identifier to find relevant mods

        Returns a dictionary of instanced buffs which modify the specified stat in the format {buffkey: instance}.
        """
        _cache = self.traits if not to_filter else to_filter
        buffs = {k: buff for k, buff in _cache.items() for m in buff.mods if m.stat == stat}
        return buffs

    def get_by_trigger(self, trigger: str, to_filter=None):
        """Finds all buffs with the matching string in their triggers.

        Args:
            trigger:    The string identifier to find relevant buffs

        Returns a dictionary of instanced buffs which fire off the designated trigger, in the format {buffkey: instance}.
        """
        _cache = self.effects if not to_filter else to_filter
        buffs = {k: buff for k, buff in _cache.items() if trigger in buff.triggers}

    def get_by_source(self, source, to_filter=None):
        """Find all buffs with the matching source.

        Args:
            source: The source you want to filter buffs by

        Returns a dictionary of instanced buffs which came from the provided source, in the format {buffkey: instance}.
        """
        _cache = self.all if not to_filter else to_filter
        buffs = {k: buff for k, buff in _cache.items() if buff.source == source}

    def get_by_cachevalue(self, key, value=None, to_filter=None):
        """Find all buffs with a matching {key: value} pair in its cache. Allows you to search buffs by arbitrary cache values

        Args:
            key:    The key of the cache value to check
            value:  (optional) The value to match to. If None, merely checks to see if the value exists

        Returns a dictionary of instanced buffs with cache values matching the specified value, in the format {buffkey: instance}.
        """
            buffs = {k: buff for k, buff in _cache.items() if buff.cache.get(key)}
        elif value:
            buffs = {k: buff for k, buff in _cache.items() if buff.cache.get(key) == value}

    def has(self, buff=None) -> bool:
        """Checks if the specified buff type or key exists on the handler.

        Args:
            buff:   The buff to search for. This can be a string (the key) or a class reference (the buff type)

        Returns a bool. If no buff and no key is specified, returns False."""
        if not buff:
        if not (isinstance(buff, type) or isinstance(buff, str)):

        if isinstance(buff, str):
            for k in self.buffcache.keys():
                if k == buff:
        if isinstance(buff, type):
            for b in self.buffcache.values():
                if b.get("ref") == buff:

    def check(
        self, value: float, stat: str, loud=True, context=None, trigger=False, strongest=False
    ):
        """Finds all buffs and perks related to a stat and applies their effects.

        Args:
            value:  The value you intend to modify
            stat:   The string that designates which stat buffs you want
            loud:   (optional) Call the buff's at_post_check method after checking (default: True)
            context: (optional) A dictionary you wish to pass to the at_pre_check/at_post_check and conditional methods as kwargs
            trigger: (optional) Trigger buffs with the `stat` string as well. (default: False)
            strongest:  (optional) Applies only the strongest mods of the corresponding stat value (default: False)

        Returns the value modified by relevant buffs."""
        # Buff cleanup to make sure all buffs are valid before processing

        # Find all buffs and traits related to the specified stat.
        applied = self.get_by_stat(stat)
        if not applied:
            return value

        # Run pre-check hooks on related buffs
        for buff in applied.values():
            buff.at_pre_check(**context)

        # Sift out buffs that won't be applying their mods (paused, conditional)
        applied = {
            k: buff for k, buff in applied.items() if buff.conditional(**context) if not buff.paused
        }

        # The mod totals
        calc = self._calculate_mods(stat, applied)

        # The calculated final value
        final = self._apply_mods(value, calc, strongest=strongest)

        # Run the "after check" functions on all relevant buffs
            buff: BaseBuff
            if loud:
                buff.at_post_check(**context)
            del buff

        # If you want to, also trigger buffs with the same stat string
        if trigger:
            self.trigger(stat, context)

        return final

    def trigger(self, trigger: str, context: dict = None):
        """Calls the at_trigger method on all buffs with the matching trigger.

        Args:
            trigger:    The string identifier to find relevant buffs. Passed to the at_trigger method.
            context:    (optional) A dictionary you wish to pass to the at_trigger method as kwargs
        """
        _effects = self.get_by_trigger(trigger)
        if not _effects:
            return

        _to_trigger = {
            k: buff
            for k, buff in _effects.items()
            if buff.conditional(**context)
            if trigger in buff.triggers
        }

        # Trigger all buffs whose trigger matches the trigger string
        for buff in _to_trigger.values():
            buff.at_trigger(trigger, **context)

    def pause(self, key: str, context=None):
        """Pauses the buff. This excludes it from being checked for mods, triggered, or cleaned up. Used to make buffs 'playtime' instead of 'realtime'.

        Args:
            key:    The key for the buff you wish to pause
            context:    (optional) A dictionary you wish to pass to the at_pause method as kwargs
        """
        if key in self.buffcache.keys():
            # Mark the buff as paused
            buff = dict(self.buffcache.get(key))
            if buff["paused"]:
                return
            if not context:
                context = {}
            buff["paused"] = True

            # Math assignments
            current = time.time()  # Current Time
            start = buff["start"]  # Start
            duration = buff["duration"]  # Duration
            prevtick = buff["prevtick"]  # Previous tick timestamp
            tickrate = buff["tickrate"]  # Buff's tick rate
            end = start + duration  # End

            # Setting "tickleft"
            if buff["ref"].ticking:
                buff["tickleft"] = max(1, tickrate - (current - prevtick))

            # Setting the new duration (if applicable)
            if duration > -1:
                newduration = end - current  # New duration
                if newduration > 0:
                    buff["duration"] = newduration
                else:
                    self.remove(key)

            # Apply new cache info, call pause hook
            self.buffcache[key] = buff
            instance: BaseBuff = buff["ref"](self, key, buff)
            instance.at_pause(**context)

    def unpause(self, key: str, context=None):
        """Unpauses a buff. This makes it visible to the various buff systems again.

        Args:
        """
            # Mark the buff as unpaused
            if not buff["paused"]:
                return
            buff["paused"] = False

            tickrate = buff["ref"].tickrate
                tickleft = buff["tickleft"]

            # Start our new timer, adjust prevtick
            buff["start"] = current
                buff["prevtick"] = current - (tickrate - tickleft)

            # Apply new cache info, call hook
            instance.at_unpause(**context)

            # Set up typical delays (cleanup/ticking)
            if instance.duration > -1:
                utils.delay(buff["duration"], cleanup_buffs, self, persistent=True)
            if instance.ticking:
                utils.delay(
                    tickrate, tick_buff, handler=self, buffkey=key, initial=False, persistent=True
                )

    def view(self, to_filter=None) -> dict:
        """Returns a buff flavor text as a dictionary of tuples in the format {key: (name, flavor)}. Common use for this is a buff readout of some kind.

        Args:
            to_filter:  (optional) The dictionary of buffs to iterate over. If none is provided, returns all buffs (default: None)
        """
        if not isinstance(to_filter, dict):
        _cache = self.visible if not to_filter else to_filter
        _flavor = {k: (buff.name, buff.flavor) for k, buff in _cache.items()}
        return _flavor

    def view_modifiers(self, stat: str, context=None):
        """Checks all modifiers of the specified stat without actually applying them. Hits the conditional hook for relevant buffs.

        Args:
            stat:   The mod identifier string to search for
            context:    (optional) A dictionary you wish to pass to the conditional hooks as kwargs

        Returns a nested dictionary. The first layer's keys represent the type of modifier ('add' and 'mult'),
        and the second layer's keys represent the type of value ('total' and 'strongest')."""

        applied = {
        }

        # Calculate and return our values dictionary
        return calc

        """Removes expired buffs, ensures pause state is respected."""
        self._validate_state()
        cleanup_buffs(self)

    # region private methods
    def _validate_state(self):
        """Validates the state of paused/unpaused playtime buffs."""
        if not self.autopause:
            return
        if self.owner.has_account:
            self._unpause_playtime()
        elif not self.owner.has_account:
            self._pause_playtime()

    def _pause_playtime(self, sender=owner, **kwargs):
        """Pauses all playtime buffs when attached object is unpuppeted."""
        if sender != self.owner:
            return
        buffs = self.playtime
        if not buffs:
            return
        for buff in buffs.values():
            buff.pause()

    def _unpause_playtime(self, sender=owner, **kwargs):
        """Unpauses all playtime buffs when attached object is puppeted."""
            return
            return
            buff.unpause()
        pass

    def _calculate_mods(self, stat: str, buffs: dict):
        """Calculates the total value of applicable mods.

        Args:
            stat:   The string identifier to search mods for
            buffs:  The dictionary of buffs to calculate mods from

        # The base return dictionary. If you update how modifiers are calculated, make sure to update this too, or you will get key errors!
        calculated = {
            "add": {"total": 0, "strongest": 0},
            "mult": {"total": 0, "strongest": 0},
            "div": {"total": 0, "strongest": 0},
        }
            return calculated

            for mod in buff.mods:
                buff: BaseBuff
                mod: Mod
                if mod.stat == stat:
                    _modval = mod.value + ((buff.stacks) * mod.perstack)
                    calculated[mod.modifier]["total"] += _modval
                    if _modval > calculated[mod.modifier]["strongest"]:
                        calculated[mod.modifier]["strongest"] = _modval
        return calculated

    def _apply_mods(self, value, calc: dict, strongest=False):
        """Applies modifiers to a value.

        Args:
            value:  The value to modify
            calc:   The dictionary of calculated modifier values (see _calculate_mods)

        Returns value modified by the relevant mods."""
        final = value
        if strongest:
            final = (
                (value + calc["add"]["strongest"])
                / max(1, 1.0 + calc["div"]["strongest"])
                * max(0, 1.0 + calc["mult"]["strongest"])
            )
        else:
            final = (
                (value + calc["add"]["total"])
                / max(1, 1.0 + calc["div"]["total"])
                * max(0, 1.0 + calc["mult"]["total"])
            )

    def _remove_via_dict(self, buffs: dict, loud=True, dispel=False, expire=False, context=None):
        """Removes buffs within the provided dictionary from this handler. Used for remove methods besides the basic remove."""
            return
        for k, instance in buffs.items():
            instance: BaseBuff
            if loud:
                if dispel:
                    instance.at_dispel(**context)
                elif expire:
                    instance.at_expire(**context)
                instance.at_remove(**context)
            del instance
            del self.buffcache[k]

class BuffableProperty(AttributeProperty):
    """An example of a way you can extend AttributeProperty to create properties that automatically check buffs for you."""

    def at_get(self, value, obj):
        _value = obj.buffs.check(value, self._key)
        return _value

class CmdBuff(Command):
    """
    Buff a target.

    Usage:
      buff <target> <buff>

    Applies the specified buff to the target. All buffs are defined in the bufflist dictionary on this command.
    """

    key = "buff"
    aliases = ["buff"]
    help_category = "builder"

    bufflist = {"foo": BaseBuff}

        self.args = self.args.split()

            target = caller.search(self.args[0])
            caller.ndb.target = target
        elif caller.ndb.target:
            target = caller.ndb.target
        else:
            caller.msg("You need to pick a target to buff.")
            return

        if self.args[1] not in self.bufflist.keys():
            caller.msg("You must pick a valid buff.")
            return

        if target:
            target.buffs.add(self.bufflist[self.args[1]], source=caller)
            pass

def cleanup_buffs(handler: BuffHandler):
    """Cleans up all expired buffs from a handler."""
    _remove = handler.expired
    for v in _remove.values():
        v.remove(expire=True)

def tick_buff(handler: BuffHandler, buffkey: str, context=None, initial=True):
    """Ticks a buff. If a buff's tickrate is 1 or larger, this is called when the buff is applied, and then once per tick cycle.

    Args:
        handler:    The handler managing the ticking buff
        buffkey:    The key of the ticking buff
        context:    (optional) A dictionary you wish to pass to the at_tick method as kwargs
        initial:    (optional) Whether this tick_buff call is the first one. Starts True, changes to False for future ticks
    """
    # Cache a reference and find the buff on the object
    if buffkey not in handler.buffcache.keys():
        return
    if not context:
        context = {}

    # Instantiate the buff and tickrate
    buff: BaseBuff = handler.get(buffkey)
    tr = max(1, buff.tickrate)

    # This stops the old ticking process if you refresh/stack the buff
    if (tr > time.time() - buff.prevtick and initial != True) or buff.paused:
        return

    # Only fire the at_tick methods if the conditional is truthy
    if buff.conditional():
        # Always tick this buff on initial
        if initial:
            buff.at_tick(initial, **context)

        # Tick this buff one last time, then remove
        if buff.duration > -1 and buff.duration <= time.time() - buff.start:
            if tr < time.time() - buff.prevtick:
                buff.at_tick(initial, **context)
            buff.remove(expire=True)
            return

        # Tick this buff on-time
        if tr <= time.time() - buff.prevtick:

    handler.buffcache[buffkey]["prevtick"] = time.time()

    # Recur this function at the tickrate interval, if it didn't stop/fail
    utils.delay(
        tr,
        tick_buff,
        handler=handler,
        buffkey=buffkey,
        context=context,
        initial=False,
    )

from .buff import BaseBuff, Mod

class Exploit(BaseBuff):
    key = "exploit"
    name = "Exploit"
    flavor = "You are learning your opponent's weaknesses."

    duration = -1
    maxstacks = 20

    triggers = ["hit"]

    stack_msg = {
        1: "You begin to notice flaws in your opponent's defense.",
        10: "You've begun to match the battle's rhythm.",
        20: "You've found a gap in the guard!",
    }

        if self.handler.get_by_type(Exploited):

        chance = self.stacks / 20
        roll = random.random()

        if chance > roll:
            self.handler.add(Exploited)
            self.owner.msg("An opportunity presents itself!")
        elif chance < roll:
            self.handler.add(Exploit)

        if self.stacks in self.stack_msg:
            self.owner.msg(self.stack_msg[self.stacks])

class Exploited(BaseBuff):
    key = "exploited"
    name = "Exploited"
    flavor = "You have sensed your target's vulnerability, and are poised to strike."

    duration = 30

    mods = [Mod("damage", "add", 100)]

        self.owner.msg("You ruthlessly exploit your target's weakness!")
        self.remove(quiet=True)

        self.owner.msg("You have waited too long; the opportunity passes.")

class Leeching(BaseBuff):
    key = "leeching"
    name = "Leeching"
    flavor = "Attacking this target fills you with vigor."

    triggers = ["taken_damage"]

    def at_trigger(self, trigger: str, attacker=None, damage=None, *args, **kwargs):
        if not attacker or not damage:
            return
        attacker.msg("You have been healed for {heal} life!".format(heal=damage * 0.1))

class Poison(BaseBuff):
    key = "poison"
    name = "Poison"
    flavor = "A poison wracks this body with painful spasms."

    duration = 120

    maxstacks = 5
    dmg = 5

    playtime = True

        self.owner.db.prelogout_location.msg_contents(
            "{actor} stops twitching, their flesh a deathly pallor.".format(actor=self.owner)
        )

        self.owner.location.msg_contents(
            "{actor} begins to twitch again, their cheeks flushing red with blood.".format(
                actor=self.owner
            )
        )

                    actor=self.owner, damage=_dmg
                )
            )

class Sated(BaseBuff):
    key = "sated"
    name = "Sated"
    flavor = "You have eaten a great meal!"

    duration = 180
    maxstacks = 3

    mods = [Mod("mood", "add", 15)]

class StatBuff(BaseBuff):
    """Customize the stat this buff affects by feeding a list in the order [stat, mod, base, perstack] to the cache argument when added"""

    key = "statbuff"
    name = "statbuff"
    flavor = "This buff affects the following stats: {stats}"

    maxstacks = 0
    refresh = True
    unique = False

    cache = {"modgen": ["foo", "add", 0, 0]}

    def __init__(self, handler, buffkey, cache={}) -> None:
        super().__init__(handler, buffkey, cache)
        # Finds our "modgen" cache value, which we pass on application
        modgen = list(self.cache.get("modgen"))
        if modgen:
            self.mods = [Mod(*modgen)]
        msg = ""
        _msg = [mod.stat for mod in self.mods]
        for stat in _msg:
            msg += stat
        self.flavor = self.flavor.format(stats=msg)

"""
Tests for the buff system contrib
"""
from unittest.mock import Mock, call, patch

from evennia import DefaultObject, create_object
from evennia.contrib.rpg.buffs import buff
from evennia.utils.test_resources import EvenniaTest
from evennia.utils.utils import lazy_property

from .buff import BaseBuff, BuffableProperty, BuffHandler, Mod
from .samplebuffs import StatBuff

class _EmptyBuff(BaseBuff):
    key = "empty"
    pass

class _TestDivBuff(BaseBuff):
    key = "tdb"
    name = "tdb"
    flavor = "divverbuff"
    mods = [Mod("stat1", "div", 1)]

class _TestModBuff(BaseBuff):
    key = "tmb"
    name = "tmb"
    flavor = "modderbuff"
    mods = [Mod("stat1", "add", 10, 5), Mod("stat2", "mult", 0.5)]

class _TestModBuff2(BaseBuff):
    key = "tmb2"
    name = "tmb2"
    flavor = "modderbuff2"
    maxstacks = 1
    mods = [Mod("stat1", "mult", 1.0), Mod("stat1", "add", 10)]

class _TestTrigBuff(BaseBuff):
    key = "ttb"
    name = "ttb"
    flavor = "triggerbuff"
    triggers = ["test1", "test2"]

        if trigger == "test1":
            self.owner.db.triggertest1 = True
        if trigger == "test2":
            self.owner.db.triggertest2 = True

class _TestConBuff(BaseBuff):
    key = "tcb"
    name = "tcb"
    flavor = "condbuff"
    triggers = ["condtest"]

        return self.owner.db.cond1

    def at_trigger(self, trigger: str, attacker=None, defender=None, damage=0, *args, **kwargs):
        defender.db.att, defender.db.dmg = attacker, damage

class _TestComplexBuff(BaseBuff):
    key = "tcomb"
    name = "complex"
    flavor = "combuff"
    triggers = ["comtest", "complextest"]

    mods = [
        Mod("com1", "add", 0, 10),
        Mod("com1", "add", 15),
        Mod("com1", "mult", 2.0),
        Mod("com2", "add", 100),
    ]

    def conditional(self, cond=False, *args, **kwargs):
        return not cond

        if trigger == "comtest":
            self.owner.db.comtext = {"cond": True}
        else:
            self.owner.db.comtext = {}

class _TestTimeBuff(BaseBuff):
    key = "ttib"
    name = "ttib"
    flavor = "timerbuff"
    tickrate = 1
    duration = 5
    mods = [Mod("timetest", "add", 665)]

        self.owner.db.ticktest = True

class BuffableObject(DefaultObject):
    stat1 = BuffableProperty(10)

    def buffs(self) -> BuffHandler:
        return BuffHandler(self)

        self.stat1, self.buffs
        return super().at_init()

class TestBuffsAndHandler(EvenniaTest):
    "This tests a number of things about buffs."

        self.testobj = create.create_object(BuffableObject, key="testobj")

        """done after every test_* method below"""
        self.testobj.buffs.clear()
        del self.testobj

    @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
    def test_addremove(self):
        """tests adding and removing buffs"""
        # setup
        handler: BuffHandler = self.testobj.buffs
        # add
        handler.add(_TestModBuff, to_cache={"cachetest": True})
        handler.add(_TestTrigBuff)
        self.assertEqual(self.testobj.db.buffs["tmb"]["ref"], _TestModBuff)
        self.assertTrue(self.testobj.db.buffs["tmb"].get("cachetest"))
        self.assertFalse(self.testobj.db.buffs["ttb"].get("cachetest"))
        # has
        self.assertTrue(handler.has(_TestModBuff))
        self.assertTrue(handler.has("tmb"))
        self.assertFalse(handler.has(_EmptyBuff))
        # remove
        handler.remove("tmb")
        self.assertFalse(self.testobj.db.buffs.get("tmb"))
        # remove stacks
        handler.add(_TestModBuff, stacks=3)
        handler.remove("tmb", stacks=3)
        # remove by type
        handler.add(_TestModBuff)
        handler.remove_by_type(_TestModBuff)
        # remove by buff instance
        handler.all["tmb"].remove()
        # remove by source
        handler.remove_by_source(None)
        # remove by cachevalue
        handler.remove_by_cachevalue("failure", True)
        self.assertTrue(self.testobj.db.buffs.get("tmb"))
        # remove all
        handler.clear()
        self.assertFalse(self.testobj.buffs.all)

    def test_getters(self):
        """tests all built-in getters"""
        # setup
        handler.add(_TestModBuff, source=self.obj2)
        handler.add(_TestTrigBuff, to_cache={"ttbcache": True})
        # normal getter
        self.assertTrue(isinstance(handler.get("tmb"), _TestModBuff))
        # stat getters
        self.assertTrue(isinstance(handler.get_by_stat("stat1")["tmb"], _TestModBuff))
        self.assertFalse(handler.get_by_stat("nullstat"))
        # trigger getters
        self.assertTrue("ttb" in handler.get_by_trigger("test1").keys())
        self.assertFalse("ttb" in handler.get_by_trigger("nulltrig").keys())
        # type getters
        self.assertTrue("tmb" in handler.get_by_type(_TestModBuff))
        self.assertFalse("tmb" in handler.get_by_type(_EmptyBuff))
        # source getter
        self.assertTrue("tmb" in handler.get_by_source(self.obj2))
        self.assertFalse("ttb" in handler.get_by_source(self.obj2))
        # cachevalue getter
        self.assertFalse("tmb" in handler.get_by_cachevalue("ttbcache"))
        self.assertTrue("ttb" in handler.get_by_cachevalue("ttbcache"))
        self.assertTrue("ttb" in handler.get_by_cachevalue("ttbcache", True))

        """tests that buff details like name and flavor are correct; also test modifier viewing"""
        self.assertEqual(handler.get("tmb").flavor, "modderbuff")
        self.assertEqual(handler.get("ttb").name, "ttb")
        mods = handler.view_modifiers("stat1")
        _testmods = {
            "add": {"total": 15, "strongest": 15},
        }
        self.assertDictEqual(mods, _testmods)

    def test_modify(self):
        """tests to ensure that values are modified correctly, and stack across mods"""
        # setup
        _stat1, _stat2 = 0, 10
        # stat1 and 2 basic mods
        self.assertEqual(handler.check(_stat1, "stat1"), 15)
        self.assertEqual(handler.check(_stat2, "stat2"), 15)
        # checks can take any base value
        self.assertEqual(handler.check(_stat1, "stat2"), 0)
        self.assertEqual(handler.check(_stat2, "stat1"), 25)
        # change to base stat reflected in check
        _stat1 += 5
        self.assertEqual(handler.check(_stat1, "stat1"), 20)
        _stat2 += 10
        self.assertEqual(handler.check(_stat2, "stat2"), 30)
        # test stacking; single stack, multiple stack, max stacks
        self.assertEqual(handler.check(_stat1, "stat1"), 25)
        self.assertEqual(handler.check(_stat1, "stat1"), 40)
        handler.add(_TestModBuff, stacks=5)
        # stat2 mod doesn't stack
        # layers with second mod
        handler.add(_TestModBuff2)
        self.assertEqual(handler.check(_stat1, "stat1"), 100)
        # apply only the strongest value
        self.assertEqual(handler.check(_stat1, "stat1", strongest=True), 80)
        # removing mod properly reduces value, doesn't affect other mods
        self.assertEqual(handler.check(_stat1, "stat1"), 30)
        self.assertEqual(handler.check(_stat2, "stat2"), 20)
        # divider mod test
        handler.add(_TestDivBuff)

    def test_trigger(self):
        """tests to ensure triggers correctly fire"""
        # setup
        # trigger buffs
        handler.trigger("nulltest")
        self.assertFalse(self.testobj.db.triggertest1)
        self.assertFalse(self.testobj.db.triggertest2)
        handler.trigger("test1")
        self.assertTrue(self.testobj.db.triggertest1)
        handler.trigger("test2")
        self.assertTrue(self.testobj.db.triggertest2)

    def test_context_conditional(self):
        """tests to ensure context is passed to buffs, and also tests conditionals"""
        # setup
        handler.add(_TestConBuff)
        self.testobj.db.cond1, self.testobj.db.att, self.testobj.db.dmg = False, None, 0
        # context to pass, containing basic event data and a little extra to be ignored
        _testcontext = {
            "attacker": self.obj2,
            "defender": self.testobj,
            "damage": 5,
            "overflow": 10,
        }
        # test negative conditional
            handler.get_by_type(_TestConBuff)["tcb"].conditional(**_testcontext), False
        )
        handler.trigger("condtest", _testcontext)
        self.assertEqual(self.testobj.db.att, None)
        self.assertEqual(self.testobj.db.dmg, 0)
        # test positive conditional + context passing
        self.testobj.db.cond1 = True
        self.assertEqual(handler.get_by_type(_TestConBuff)["tcb"].conditional(**_testcontext), True)
        self.assertEqual(self.testobj.db.att, self.obj2)
        self.assertEqual(self.testobj.db.dmg, 5)

    def test_complex(self):
        """tests a complex mod (conditionals, multiple triggers/mods)"""
        # setup
        self.testobj.db.comone, self.testobj.db.comtwo, self.testobj.db.comtext = 10, 0, {}
        handler.add(_TestComplexBuff)
        # stat checks work correctly and separately
        self.assertEqual(self.testobj.db.comtext, {})
        self.assertEqual(handler.check(self.testobj.db.comone, "com1"), 105)
        self.assertEqual(handler.check(self.testobj.db.comtwo, "com2"), 100)
        # stat checks don't happen if the conditional is true
        handler.trigger("comtest", self.testobj.db.comtext)
        self.assertEqual(self.testobj.db.comtext, {"cond": True})
            handler.get_by_type(_TestComplexBuff)["tcomb"].conditional(**self.testobj.db.comtext),
            False,
        )
            handler.check(self.testobj.db.comone, "com1", context=self.testobj.db.comtext), 10
        )
            handler.check(self.testobj.db.comtwo, "com2", context=self.testobj.db.comtext), 0
        )
        handler.trigger("complextest", self.testobj.db.comtext)
        )
        )
        # separate trigger follows different codepath
        self.testobj.db.comtext = {"cond": False}
            handler.check(self.testobj.db.comone, "com1", context=self.testobj.db.comtext), 105
        )
            handler.check(self.testobj.db.comtwo, "com2", context=self.testobj.db.comtext), 100
        )

    @patch("evennia.contrib.rpg.buffs.buff.utils.delay")
    def test_timing(self, mock_delay: Mock):
        """tests timing-related features, such as ticking and duration"""
        # setup
        mock_delay.side_effect = [None, handler.cleanup]
        handler.add(_TestTimeBuff)
        calls = [
            call(
                1,
                buff.tick_buff,
                handler=handler,
                buffkey="ttib",
                context={},
                initial=False,
            ),
            call(5, handler.cleanup, persistent=True),
        ]
        mock_delay.assert_has_calls(calls)
        self.testobj.db.timetest, self.testobj.db.ticktest = 1, False
        # test duration and ticking
        _instance = handler.get("ttib")
        self.assertTrue(_instance.ticking)
        self.assertEqual(_instance.duration, 5)
        _instance.at_tick()
        self.assertTrue(self.testobj.db.ticktest)
        # test duration modification and cleanup
        _instance.duration = 0
        self.assertEqual(handler.get("ttib").duration, 0)
        handler.cleanup()
        self.assertFalse(handler.get("ttib"), None)

    def test_cacheattrlink(self):
        """tests the link between the instance attribute and the cache attribute"""
        # setup
        handler.add(_EmptyBuff)
        self.assertEqual(handler.buffcache["empty"]["duration"], -1)
        empty: _EmptyBuff = handler.get("empty")
        empty.duration = 30
        self.assertEqual(handler.buffcache["empty"]["duration"], 30)

    def test_buffableproperty(self):
        """tests buffable properties"""
        # setup
        self.testobj.buffs.add(_TestModBuff)
        self.assertEqual(self.testobj.stat1, 25)
        self.testobj.buffs.remove("tmb")
        self.assertEqual(self.testobj.stat1, 10)

    def test_stresstest(self):
        """tests large amounts of buffs, and related removal methods"""
        # setup
        for x in range(1, 20):
            self.testobj.buffs.add(_TestModBuff, key="test" + str(x))
            self.testobj.buffs.add(_TestTrigBuff, key="trig" + str(x))
        self.assertEqual(self.testobj.stat1, 295)
        self.testobj.buffs.trigger("test1")
        self.testobj.buffs.remove_by_type(_TestModBuff)

    def test_modgen(self):
        """test generating mods on the fly"""
        # setup
        self.testobj.db.gentest = 5
        self.assertEqual(self.testobj.db.gentest, 5)
        tc = {"modgen": ["gentest", "add", 5, 0]}
        handler.add(StatBuff, key="gentest", to_cache=tc)
        self.assertEqual(handler.check(self.testobj.db.gentest, "gentest"), 10)
        tc = {"modgen": ["gentest", "add", 10, 0]}
        self.assertEqual(handler.check(self.testobj.db.gentest, "gentest"), 15)
            handler.get("gentest").flavor, "This buff affects the following stats: gentest"
        )

"""
Character Creator contrib, by InspectorCaracal

# Features

The primary feature of this contrib is defining the name and attributes
of a new character through an EvMenu. It provides an alternate `charcreate`
command as well as a modified `at_look` method for your Account class.

# Usage

In order to use the contrib, you will need to create your own chargen
EvMenu. The included `example_menu.py` gives a number of useful techniques
and examples, including how to allow players to choose and confirm
character names from within the menu.

"""
import string
from random import choices

from evennia import DefaultAccount
from evennia.commands.default.muxcommand import MuxAccountCommand
from evennia.utils import create, search
from evennia.utils.evmenu import EvMenu

_CHARACTER_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
try:
    _CHARGEN_MENU = settings.CHARGEN_MENU
except AttributeError:
    _CHARGEN_MENU = "evennia.contrib.rpg.character_creator.example_menu"

class ContribCmdCharCreate(MuxAccountCommand):
    """

    Begin creating a new character, or resume character creation for
    an existing in-progress character.

    You can stop character creation at any time and resume where
    you left off later.
    """

    locks = "cmd:pperm(Player) and is_ooc()"

        "create the new character"

        # only one character should be in progress at a time, so we check for WIPs first
        in_progress = [chara for chara in account.characters if chara.db.chargen_step]

        if len(in_progress):
            # we're continuing chargen for a WIP character
            new_character = in_progress[0]
        else:
            # we're making a new character
            charmax = settings.MAX_NR_CHARACTERS

            if not account.is_superuser and (
                account.characters and len(account.characters) >= charmax
            ):
                plural = "" if charmax == 1 else "s"
                self.msg(f"You may only create a maximum of {charmax} character{plural}.")
                return

            # create the new character object, with default settings
            # start_location = ObjectDB.objects.get_id(settings.START_LOCATION)
            default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
            permissions = settings.PERMISSION_ACCOUNT_DEFAULT
            # generate a randomized key so the player can choose a character name later
            key = "".join(choices(string.ascii_letters + string.digits, k=10))
            new_character = create.create_object(
                _CHARACTER_TYPECLASS,
                key=key,
                location=None,
                home=default_home,
                permissions=permissions,
            )
            # only allow creator (and developers) to puppet this char
            new_character.locks.add(
                f"puppet:pid({account.id}) or perm(Developer) or"
                f" pperm(Developer);delete:id({account.id}) or perm(Admin)"
            )
            # initalize the new character to the beginning of the chargen menu
            new_character.db.chargen_step = "menunode_welcome"
            account.characters.add(new_character)

        # set the menu node to start at to the character's last saved step
        startnode = new_character.db.chargen_step
        # attach the character to the session, so the chargen menu can access it
        session.new_char = new_character

        # this gets called every time the player exits the chargen menu
        def finish_char_callback(session, menu):
            char = session.new_char
            if not char.db.chargen_step:
                # this means character creation was completed - start playing!
                # execute the ic command to start puppeting the character
                account.execute_cmd("ic {}".format(char.key))

        EvMenu(session, _CHARGEN_MENU, startnode=startnode, cmd_on_exit=finish_char_callback)

class ContribChargenAccount(DefaultAccount):
    """
    A modified Account class that makes minor changes to the OOC look
    output, to incorporate in-progress characters.
    """

        """
        Called by the OOC look command. It displays a list of playable
        characters and should be mostly identical to the core method.

        Args:
                objects to inspect.

        Returns:
        """

        # list of targets - make list to disconnect from db
        is_su = self.is_superuser

        # text shown when looking in the ooc area
        result = [f"Account |g{self.key}|n (you are Out-of-Character)"]

        if nsess == 1:
            result.append("\n\n|wConnected session:|n")
        elif nsess > 1:
            result.append(f"\n\n|wConnected sessions ({nsess}):|n")
            csessid = sess.sessid
            addr = "{protocol} ({address})".format(
                protocol=sess.protocol_key,
                address=isinstance(sess.address, tuple)
                and str(sess.address[0])
                or str(sess.address),
            )
            if session.sessid == csessid:
                result.append(f"\n |w* {isess+1}|n {addr}")
            else:
                result.append(f"\n   {isess+1} {addr}")

        result.append("\n\n |whelp|n - more commands")
        result.append("\n |wpublic <Text>|n - talk on public channel")

        charmax = settings.MAX_NR_CHARACTERS

        if is_su or len(characters) < charmax:
            result.append("\n |wcharcreate|n - create a new character")

        if characters:
            result.append("\n |wchardelete <name>|n - delete a character (cannot be undone!)")
        plural = "" if len(characters) == 1 else "s"
        result.append("\n |wic <character>|n - enter the game (|wooc|n to return here)")
        if is_su:
            result.append(f"\n\nAvailable character{plural} ({len(characters)}/unlimited):")
        else:
            result.append(f"\n\nAvailable character{plural} ({len(characters)}/{charmax}):")

        for char in characters:
            if char.db.chargen_step:
                # currently in-progress character; don't display placeholder names
                result.append("\n - |Yin progress|n (|wcharcreate|n to continue)")
                continue
            csessions = char.sessions.all()
            if csessions:
                for sess in csessions:
                    # character is already puppeted
                    sid = sess in sessions and sessions.index(sess) + 1
                    if sess and sid:
                        result.append(
                            f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] (played by"
                            f" you in session {sid})"
                        )
                    else:
                            f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] (played by"
                            " someone else)"
                        )
            else:
                # character is available
                result.append(f"\n - {char.key} [{', '.join(char.permissions.all())}]")
        look_string = ("-" * 68) + "\n" + "".join(result) + "\n" + ("-" * 68)
        return look_string

from unittest.mock import patch

from evennia.utils.test_resources import BaseEvenniaCommandTest

from . import character_creator

class TestCharacterCreator(BaseEvenniaCommandTest):
        self.account.swap_typeclass(character_creator.ContribChargenAccount)

    def test_ooc_look(self):

        self.char1.db.chargen_step = "start"

        with patch("evennia.commands.default.account._AUTO_PUPPET_ON_LOGIN", new=False):
            # check that correct output is returning
            output = self.call(
                account.CmdOOCLook(),
                "",
                "Account TestAccount (you are Out-of-Character)",
                caller=self.account,
            )
            # check that char1 is recognized as in progress
            self.assertIn("in progress", output)

    @override_settings(CHARGEN_MENU="evennia.contrib.rpg.character_creator.example_menu")
            character_creator.ContribCmdCharCreate(),
            "",
        )
        menu = self.session.ndb._menutree
        self.assertNotEqual(menu, None)
        self.assertTrue(inherits_from(self.session.new_char, DefaultCharacter))

"""
# Dice

Rolls dice for roleplaying, in-game gambling or GM:ing

This module implements a a dice-roller and a `dice`/`roll` command
to go with it. It uses standard RPG 'd'-syntax (e.g. 2d6 to roll two
six-sided die) and also supports modifiers such as 3d6 + 5.

    > roll 1d20 + 2

One can also specify a standard Python operator in order to specify
eventual target numbers and get results in a fair and guaranteed
unbiased way.  For example a GM could (using the dice command) from
the start define the roll as 2d6 < 8 to show that a roll below 8 is
required to succeed. The command will normally echo this result to all
parties (although it also has options for hidden and secret rolls).

# Installation:

Add the `CmdDice` command from this module to your character's cmdset
(and then restart the server):

```python

# ...
from evennia.contrib.rpg import dice  <---

    # ...
        # ...
        self.add(dice.CmdDice())  # <---

```

# Usage:

    roll 1d100 + 10

To roll dice in code, use the `roll` function from this module:

    from evennia.contrib.rpg import dice

    dice.roll("3d10 + 2")

If your system generates the dice dynamically you can also enter each part
of the roll separately:

    dice.roll(3, 10, ("+", 2))  # 3d10 + 2

"""
import re

from evennia import CmdSet, default_cmds
from evennia.utils.utils import simple_eval

def roll(
    dice,
    dicetype=6,
    modifier=None,
    conditional=None,
    return_tuple=False,
    max_dicenum=10,
    max_dicetype=1000,
):
    """
    This is a standard dice roller.

    Args:
     dice (int or str): If an `int`, this is the number of dice to roll, and `dicetype` is used
        to determine the type. If a `str`, it should be on the form `NdM` where `N` is the number
        of dice and `M` is the number of sides on each die. Also
        `NdM [modifier] [number] [conditional]` is understood, e.g. `1d6 + 3`
        or `2d10 / 2 > 10`.
     dicetype (int, optional): Number of sides of the dice to be rolled. Ignored if
        `dice` is a string.
     modifier (tuple, optional): A tuple `(operator, value)`, where operator is
        one of `"+"`, `"-"`, `"/"` or `"*"`. The result of the dice
        roll(s) will be modified by this value. Ignored if `dice` is a string.
     conditional (tuple, optional): A tuple `(conditional, value)`, where
        conditional is one of `"=="`,`"<"`,`">"`,`">="`,`"<=`" or "`!=`".
        Ignored if `dice` is a string.
     return_tuple (bool): Return a tuple with all individual roll
        results or not.
     max_dicenum (int): The max number of dice to allow to be rolled.
     max_dicetype (int): The max number of sides on the dice to roll.

    Returns:
        int, bool or tuple : By default, this is the result of the roll + modifiers.  If
        `conditional` is given, or `dice` is a string defining a conditional, then a True/False
        value is returned. Finally, if `return_tuple` is set, this is a tuple
        `(result, outcome, diff, rolls)`, where, `result` is the the normal result of the
        roll + modifiers,  `outcome` and `diff` are the boolean absolute difference between the roll
        and the `conditional` input; both will be will be `None` if `conditional` is not set.
        The `rolls` a tuple holding all the individual rolls (one or more depending on how many
        dice were rolled).

    Raises:
        TypeError if non-supported modifiers or conditionals are given.

    Notes:
        All input numbers are converted to integers.

    Examples:
        ::
            # string form
            print roll("3d6 + 2")
            10
            print roll("2d10 + 2 > 10")
            True
            print roll("2d20 - 2 >= 10")
            (8, False, 2, (4, 6)) # roll was 4 + 6 - 2 = 8

            # explicit arguments
            print roll(2, 6) # 2d6
            7
            print roll(1, 100, ('+', 5) # 1d100 + 5
            4
            print roll(1, 20, conditional=('<', 10) # let'say we roll 3
            True
            print roll(3, 10, return_tuple=True)
            (11, None, None, (2, 5, 4))
            print roll(2, 20, ('-', 2), conditional=('>=', 10), return_tuple=True)

    """

    modifier_string = ""
    conditional_string = ""
    conditional_value = None
    if isinstance(dice, str) and "d" in dice.lower():
        # A string is given, parse it as NdM dice notation
        roll_string = dice.lower()

        # split to get the NdM syntax
        dicenum, rest = roll_string.split("d", 1)

        # parse packwards right-to-left
        if any(True for cond in ("==", "<", ">", "!=", "<=", ">=") if cond in rest):
            # split out any conditionals, like '< 12'
            rest, *conditionals = re.split(r"(==|<=|>=|<|>|!=)", rest, maxsplit=1)
            try:
                conditional_value = int(conditionals[1])
                    f"Conditional '{conditionals[-1]}' was not recognized. Must be a number."
                )
            conditional_string = "".join(conditionals)

        if any(True for op in ("+", "-", "*", "/") if op in rest):
            # split out any modifiers, like '+ 2'
            rest, *modifiers = re.split(r"(\+|-|/|\*)", rest, maxsplit=1)
            modifier_string = "".join(modifiers)

        # whatever is left is the dice type
        dicetype = rest

    else:
        # an integer is given - explicit modifiers and conditionals as part of kwargs
        dicenum = int(dice)
        dicetype = int(dicetype)
        if modifier:
            modifier_string = "".join(str(part) for part in modifier)
        if conditional:
            conditional_value = int(conditional[1])
            conditional_string = "".join(str(part) for part in conditional)

    try:
        dicenum = int(dicenum)
            f"The number of dice and dice-size must both be numerical. Got '{dicenum}' "
            f"and '{dicetype}'."
        )
    if 0 < dicenum > max_dicenum:
        raise TypeError(f"Invalid number of dice rolled (must be between 1 and {max_dicenum}).")
    if 0 < dicetype > max_dicetype:
        raise TypeError(f"Invalid die-size used (must be between 1 and {max_dicetype} sides).")

    # roll all dice, remembering each roll
    rolls = tuple([randint(1, dicetype) for _ in range(dicenum)])
    result = sum(rolls)

    if modifier_string:
        result = simple_eval(f"{result} {modifier_string}")

    outcome, diff = None, None
    if conditional_string and conditional_value:
        outcome = simple_eval(f"{result} {conditional_string}")
        diff = abs(result - conditional_value)

    if return_tuple:
        return result, outcome, diff, rolls
    elif conditional or (conditional_string and conditional_value):
        return outcome  # True|False
    else:
        return result  # integer

roll_dice = roll

RE_PARTS = re.compile(r"(d|\+|-|/|\*|<|>|<=|>=|!=|==)")
RE_MOD = re.compile(r"(\+|-|/|\*)")
RE_COND = re.compile(r"(<|>|<=|>=|!=|==)")

class CmdDice(default_cmds.MuxCommand):
    """
    roll dice

    Usage:
      dice[/switch] <nr>d<sides> [modifier] [success condition]

    Switch:
      hidden - tell the room the roll is being done, but don't show the result
      secret - don't inform the room about neither roll nor result

    Examples:
      dice 3d6 + 4
      dice 1d100 - 2 < 50

    This will roll the given number of dice with given sides and modifiers.
    So e.g. 2d6 + 3 means to 'roll a 6-sided die 2 times and add the result,
    then add 3 to the total'.
    Accepted modifiers are +, -, * and /.
    A success condition is given as normal Python conditionals
    (<,>,<=,>=,==,!=). So e.g. 2d6 + 3 > 10 means that the roll will succeed
    only if the final result is above 8. If a success condition is given, the
    outcome (pass/fail) will be echoed along with how much it succeeded/failed
    with. The hidden/secret switches will hide all or parts of the roll from
    everyone but the person rolling.
    """

    key = "dice"
    aliases = ["roll", "@dice"]

        """Mostly parsing for calling the dice roller function"""

            self.caller.msg("Usage: @dice <nr>d<sides> [modifier] [conditional]")
            return
        argstring = "".join(str(arg) for arg in self.args)

        parts = [part for part in RE_PARTS.split(self.args) if part]
        len_parts = len(parts)
        modifier = None
        conditional = None

        if len_parts < 3 or parts[1] != "d":
                "You must specify the die roll(s) as <nr>d<sides>."
                " For example, 2d6 means rolling a 6-sided die 2 times."
            )
            return

        # Limit the number of dice and sides a character can roll to prevent server slow down and crashes
        ndicelimit = 10000  # Maximum number of dice
        nsidelimit = 10000  # Maximum number of sides
        if int(parts[0]) > ndicelimit or int(parts[2]) > nsidelimit:
            self.caller.msg("The maximum roll allowed is %sd%s." % (ndicelimit, nsidelimit))
            return

        ndice, nsides = parts[0], parts[2]
        if len_parts == 3:
            # just something like 1d6
            pass
        elif len_parts == 5:
            # either e.g. 1d6 + 3  or something like 1d6 > 3
            if parts[3] in ("+", "-", "*", "/"):
                modifier = (parts[3], parts[4])
            else:  # assume it is a conditional
                conditional = (parts[3], parts[4])
        elif len_parts == 7:
            # the whole sequence, e.g. 1d6 + 3 > 5
            modifier = (parts[3], parts[4])
            conditional = (parts[5], parts[6])
        else:
            # error
            self.caller.msg("You must specify a valid die roll")
            return
        # do the roll
        try:
            result, outcome, diff, rolls = roll_dice(
                ndice, nsides, modifier=modifier, conditional=conditional, return_tuple=True
            )
                "You need to enter valid integer numbers, modifiers and operators."
                " |w%s|n was not understood." % self.args
            )
            return
        if len(rolls) > 1:
            rolls = ", ".join(str(roll) for roll in rolls[:-1]) + " and " + str(rolls[-1])
        else:
            rolls = rolls[0]
        if outcome is None:
            outcomestring = ""
        elif outcome:
            outcomestring = " This is a |gsuccess|n (by %s)." % diff
        else:
            outcomestring = " This is a |rfailure|n (by %s)." % diff
        yourollstring = "You roll %s%s."
        roomrollstring = "%s rolls %s%s."
        resultstring = " Roll(s): %s. Total result is |w%s|n."

        if "secret" in self.switches:
            # don't echo to the room at all
            string = yourollstring % (argstring, " (secret, not echoed)")
            string += "\n" + resultstring % (rolls, result)
            string += outcomestring + " (not echoed)"
            self.caller.msg(string)
        elif "hidden" in self.switches:
            # announce the roll to the room, result only to caller
            string = yourollstring % (argstring, " (hidden)")
            string = roomrollstring % (self.caller.key, argstring, " (hidden)")
            self.caller.location.msg_contents(string, exclude=self.caller)
            # handle result
            string = resultstring % (rolls, result)
        else:
            # normal roll
            string = yourollstring % (argstring, "")
            string = roomrollstring % (self.caller.key, argstring, "")
            string += outcomestring
            self.caller.location.msg_contents(string)

class DiceCmdSet(CmdSet):
    """
    a small cmdset for testing purposes.
    Add with @py self.cmdset.add("contrib.dice.DiceCmdSet")
    """

        """Called when set is created"""
        self.add(CmdDice())

"""
Testing of TestDice.

"""

from . import dice

@patch("evennia.contrib.rpg.dice.dice.randint", return_value=5)
class TestDice(BaseEvenniaCommandTest):
    def test_roll_dice(self, mocked_randint):
        self.assertEqual(dice.roll(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4)
        self.assertEqual(dice.roll(6, 6, conditional=("<", 35)), True)
        self.assertEqual(dice.roll(6, 6, conditional=(">", 33)), False)

    def test_cmddice(self, mocked_randint):
            dice.CmdDice(), "3d6 + 4", "You roll 3d6 + 4.| Roll(s): 5, 5 and 5. Total result is 19."
        )
        self.call(dice.CmdDice(), "100000d1000", "The maximum roll allowed is 10000d10000.")
        self.call(dice.CmdDice(), "/secret 3d6 + 4", "You roll 3d6 + 4 (secret, not echoed).")

    def test_string_form(self, mocked_randint):
        self.assertEqual(dice.roll("6d6 + 4"), mocked_randint() * 6 + 4)
        self.assertEqual(dice.roll("6d6 < 35"), True)
        self.assertEqual(dice.roll("6d6 > 35"), False)
        self.assertEqual(dice.roll("2d10 + 5 >= 14"), True)

    def test_maxvals(self, mocked_randint):
        with self.assertRaises(TypeError):
            dice.roll(11, 1001, max_dicenum=10, max_dicetype=1000)
            dice.roll(10, 1001, max_dicenum=10, max_dicetype=1000)

"""
Health Bar

The function provided in this module lets you easily display visual
bars or meters - "health bar" is merely the most obvious use for this,
though these bars are highly customizable and can be used for any sort
of appropriate data besides player health.

Today's players may be more used to seeing statistics like health,
stamina, magic, and etc. displayed as bars rather than bare numerical
values, so using this module to present this data this way may make it
more accessible. Keep in mind, however, that players may also be using
a screen reader to connect to your game, which will not be able to
represent the colors of the bar in any way. By default, the values
represented are rendered as text inside the bar which can be read by
screen readers.

The health bar will account for current values above the maximum or
below 0, rendering them as a completely full or empty bar with the
values displayed within.

No installation, just import and use `display_meter` from this
module:

```python
    from evennia.contrib.rpg.health_bar import display_meter

    health_bar = display_meter(23, 100)
    caller.msg(prompt=health_bar)
```

"""

def display_meter(
    cur_value,
    max_value,
    length=30,
    fill_color=["R", "Y", "G"],
    empty_color="B",
    text_color="w",
    align="left",
    pre_text="",
    post_text="",
    show_values=True,
):
    """
    Represents a current and maximum value given as a "bar" rendered with
    ANSI or xterm256 background colors.

    Args:
        cur_value (int): Current value to display
        max_value (int): Maximum value to display

    Keyword Arguments:
        length (int): Length of meter returned, in characters
        fill_color (list): List of color codes for the full portion
            of the bar, sans any sort of prefix - both ANSI and xterm256
            colors are usable. When the bar is empty, colors toward the
            start of the list will be chosen - when the bar is full, colors
            towards the end are picked. You can adjust the 'weights' of
            the changing colors by adding multiple entries of the same
            color - for example, if you only want the bar to change when
            it's close to empty, you could supply ['R','Y','G','G','G']
        empty_color (str): Color code for the empty portion of the bar.
        text_color (str): Color code for text inside the bar.
        align (str): "left", "right", or "center" - alignment of text in the bar
        pre_text (str): Text to put before the numbers in the bar
        post_text (str): Text to put after the numbers in the bar
        show_values (bool): If true, shows the numerical values represented by
            the bar. It's highly recommended you keep this on, especially if
            there's no info given in pre_text or post_text, as players on screen
            readers will be unable to read the graphical aspect of the bar.

    Returns:
        str: The display bar to show.

    """
    # Start by building the base string.
    num_text = ""
    if show_values:
        num_text = "%i / %i" % (cur_value, max_value)
    bar_base_str = pre_text + num_text + post_text
    # Cut down the length of the base string if needed
    if len(bar_base_str) > length:
        bar_base_str = bar_base_str[:length]
    # Pad and align the bar base string
    if align == "right":
        bar_base_str = bar_base_str.rjust(length, " ")
    elif align == "center":
        bar_base_str = bar_base_str.center(length, " ")
    else:
        bar_base_str = bar_base_str.ljust(length, " ")

    if max_value < 1:  # Prevent divide by zero
        max_value = 1
    if cur_value < 0:  # Prevent weirdly formatted 'negative bars'
        cur_value = 0
    if cur_value > max_value:  # Display overfull bars correctly
        cur_value = max_value

    # Now it's time to determine where to put the color codes.
    percent_full = float(cur_value) / float(max_value)
    split_index = round(float(length) * percent_full)
    # Determine point at which to split the bar
    split_index = int(split_index)

    # Separate the bar string into full and empty portions
    full_portion = bar_base_str[:split_index]
    empty_portion = bar_base_str[split_index:]

    # Pick which fill color to use based on how full the bar is
    fillcolor_index = float(len(fill_color)) * percent_full
    fillcolor_index = max(0, int(round(fillcolor_index)) - 1)
    fillcolor_code = "|[" + fill_color[fillcolor_index]

    # Make color codes for empty bar portion and text_color
    emptycolor_code = "|[" + empty_color
    textcolor_code = "|" + text_color

    # Assemble the final bar
    final_bar = (
        fillcolor_code
        + textcolor_code
        + full_portion
        + "|n"
        + emptycolor_code
        + empty_portion
        + "|n"
    )

    return final_bar

"""
Test health bar contrib

"""

from . import health_bar

class TestHealthBar(BaseEvenniaTest):
    def test_healthbar(self):
        expected_bar_str = "|[R|w|n|[B|w            test0 / 200test             |n"
            health_bar.display_meter(
                0, 200, length=40, pre_text="test", post_text="test", align="center"
            ),
            expected_bar_str,
        )
        expected_bar_str = "|[R|w     |n|[B|w       test24 / 200test            |n"
                24, 200, length=40, pre_text="test", post_text="test", align="center"
            ),
        )
        expected_bar_str = "|[Y|w           test100 /|n|[B|w 200test            |n"
                100, 200, length=40, pre_text="test", post_text="test", align="center"
            ),
        )
        expected_bar_str = "|[G|w           test180 / 200test        |n|[B|w    |n"
                180, 200, length=40, pre_text="test", post_text="test", align="center"
            ),
        )
        expected_bar_str = "|[G|w           test200 / 200test            |n|[B|w|n"
                200, 200, length=40, pre_text="test", post_text="test", align="center"
            ),
        )

"""
LLM (Large Language Model) client, for communicating with an LLM backend. This can be used
for generating texts for AI npcs, or for fine-tuning the LLM on a given prompt.

Note that running a LLM locally requires a lot of power, and ideally a powerful GPU. Testing
this with CPU mode on a beefy laptop, still takes some 4s just on a very small model.

The server defaults to output suitable for a local server
https://github.com/oobabooga/text-generation-webui, but could be used for other LLM servers too.

See the LLM instructions on that page for how to set up the server. You'll also need
a model file - there are thousands to try out on https://huggingface.co/models (you want Text
Generation models specifically).

# Optional Evennia settings (if not given, these defaults are used)

DEFAULT_LLM_HOST = "http://localhost:5000"
DEFAULT_LLM_PATH = "/api/v1/generate"
DEFAULT_LLM_HEADERS = {"Content-Type": "application/json"}
DEFAULT_LLM_PROMPT_KEYNAME = "prompt"
DEFAULT_LLM_REQUEST_BODY = {...}   # see below, this controls how to prompt the LLM server.

"""

from twisted.internet import defer, protocol, reactor
from twisted.internet.defer import inlineCallbacks
from twisted.web.client import Agent, HTTPConnectionPool, _HTTP11ClientFactory
from twisted.web.http_headers import Headers
from twisted.web.iweb import IBodyProducer
from zope.interface import implementer

from evennia import logger

DEFAULT_LLM_HOST = "http://127.0.0.1:5000"
DEFAULT_LLM_API_TYPE = ""  # or openai
DEFAULT_LLM_REQUEST_BODY = {
    "max_new_tokens": 250,  # max number of tokens to generate
    "temperature": 0.7,  # higher = more random, lower = more predictable
}

@implementer(IBodyProducer)
class StringProducer:
    """
    Used for feeding a request body to the HTTP client.
    """

    def __init__(self, body):
        self.body = bytes(body, "utf-8")
        self.length = len(body)

    def startProducing(self, consumer):
        consumer.write(self.body)
        return defer.succeed(None)

    def pauseProducing(self):
        pass

    def stopProducing(self):
        pass

class SimpleResponseReceiver(protocol.Protocol):
    """
    Used for pulling the response body out of an HTTP response.
    """

    def __init__(self, status_code, d):
        self.status_code = status_code
        self.buf = b""
        self.d = d

    def dataReceived(self, data):
        self.buf += data

    def connectionLost(self, reason=protocol.connectionDone):
        self.d.callback((self.status_code, self.buf))

class QuietHTTP11ClientFactory(_HTTP11ClientFactory):
    """
    Silences the obnoxious factory start/stop messages in the default client.
    """

    noisy = False

class LLMClient:
    """
    A client for communicating with an LLM server.

    """

    def __init__(self, on_bad_request=None):
        self._conn_pool = HTTPConnectionPool(reactor)
        self._conn_pool._factory = QuietHTTP11ClientFactory

        self.prompt_keyname = getattr(settings, "LLM_PROMPT_KEYNAME", DEFAULT_LLM_PROMPT_KEYNAME)
        self.hostname = getattr(settings, "LLM_HOST", DEFAULT_LLM_HOST)
        self.pathname = getattr(settings, "LLM_PATH", DEFAULT_LLM_PATH)
        self.headers = getattr(settings, "LLM_HEADERS", DEFAULT_LLM_HEADERS)
        self.request_body = getattr(settings, "LLM_REQUEST_BODY", DEFAULT_LLM_REQUEST_BODY)

        self.api_type = getattr(settings, "LLM_API_TYPE", DEFAULT_LLM_API_TYPE)

        self.agent = Agent(reactor, pool=self._conn_pool)

    def _format_request_body(self, prompt):
        """Structure the request body for the LLM server"""
        request_body = self.request_body.copy()

        prompt = "\n".join(make_iter(prompt))

        request_body[self.prompt_keyname] = prompt

        return request_body

    def _handle_llm_response_body(self, response):
        """Get the response body from the response"""
        d = defer.Deferred()
        response.deliverBody(SimpleResponseReceiver(response.code, d))
        return d

    def _handle_llm_error(self, failure):
        """Correctly handle server connection errors"""
        failure.trap(Exception)
        return (500, failure.getErrorMessage())

    def _get_response_from_llm_server(self, prompt):
        """Call the LLM server and handle the response/failure"""
        request_body = self._format_request_body(prompt)

        if settings.DEBUG:
            logger.log_info(f"LLM request body: {request_body}")

        d = self.agent.request(
            b"POST",
            bytes(self.hostname + self.pathname, "utf-8"),
            headers=Headers(self.headers),
            bodyProducer=StringProducer(json.dumps(request_body)),
        )

        d.addCallbacks(self._handle_llm_response_body, self._handle_llm_error)
        return d

    def get_response(self, prompt):
        """
        Get a response from the LLM server for the given npc.

        Args:
            prompt (str or list): The prompt to send to the LLM server. If a list,
                this is assumed to be the chat history so far, and will be added to the
                prompt in a way suitable for the api.

        Returns:
            str: The generated text response. Will return an empty string
                if there is an issue with the server, in which case the
                the caller is expected to handle this gracefully.

        """
        status_code, response = yield self._get_response_from_llm_server(prompt)
        if status_code == 200:
            if settings.DEBUG:
                logger.log_info(f"LLM response: {response}")
            return json.loads(response)["results"][0]["text"]
        else:
            logger.log_err(f"LLM API error (status {status_code}): {response}")
            return ""

"""
Basic class for NPC that makes use of an LLM (Large Language Model) to generate replies.

It comes with a `talk` command; use `talk npc <something>` to talk to the NPC. The NPC will
respond using the LLM response.

Makes use of the LLMClient for communicating with the server. The NPC will also
echo a 'thinking...' message if the LLM server takes too long to respond.

"""

from twisted.internet import reactor, task
from twisted.internet.defer import CancelledError, inlineCallbacks

from evennia import AttributeProperty, Command, DefaultCharacter

from .llm_client import LLMClient

# fallback if not specified anywhere else. Check order is
# npc.db.prompt_prefix, npcClass.prompt_prefix, then settings.LLM_PROMPT_PREFIX, then this
DEFAULT_PROMPT_PREFIX = (
    "You are roleplaying as {name}, a {desc} existing in {location}. "
    "Answer with short sentences. Only respond as {name} would. "
    "From here on, the conversation between {name} and {character} begins."
)

class LLMNPC(DefaultCharacter):
    """An NPC that uses the LLM server to generate its responses. If the server is slow, it will
    echo a thinking message to the character while it waits for a response."""

    # use this to override the prefix per class. Assign an Attribute to override per-instance.
    prompt_prefix = None

    response_template = AttributeProperty(
        "$You() $conj(say) (to $You(character)): {response}", autocreate=False
    )
    thinking_timeout = AttributeProperty(2, autocreate=False)  # seconds
    thinking_messages = AttributeProperty(
        [
            "{name} thinks about what you said ...",
            "{name} ponders your words ...",
            "{name} ponders ...",
        ],
        autocreate=False,
    )

    max_chat_memory_size = AttributeProperty(25, autocreate=False)
    # this is a store of {character: [chat, chat, ...]}
    chat_memory = AttributeProperty(defaultdict(list))

    @property
    def llm_client(self):
        if not self.ndb.llm_client:
            self.ndb.llm_client = LLMClient()
        return self.ndb.llm_client

    @property
    def llm_prompt_prefix(self):
        """get prefix, first from Attribute, then from class variable,
        then from settings, then from default"""
        return self.attributes.get(
            "prompt_prefix",
            default=getattr(
                settings, "LLM_PROMPT_PREFIX", self.prompt_prefix or DEFAULT_PROMPT_PREFIX
            ),
        )

    def _add_to_memory(self, character, who_talked, speech):
        """Add a person's speech to the memory. This is stored as name: chat for the LLM."""
        memory = self.chat_memory[character]
        memory.append(f"{who_talked.get_display_name(self)}: {speech}")

        # trim the memory if it's getting too long in order to save space
        memory = memory[-self.max_chat_memory_size :]
        self.chat_memory[character] = memory

    def build_prompt(self, character, speech):
        """
        Build the prompt to send to the LLM server.

        Args:
            character (Object): The one talking to the NPC.
            speech (str): The latest speech from the character.

        Returns:
            str: The prompt to return.

        """
        name = self.get_display_name(character)
        charname = character.get_display_name(self)

        # get starting prompt
        prompt = self.llm_prompt_prefix.format(
            name=name,
            desc=self.db.desc or "someone",
            location=self.location.key if self.location else "the void",
            character=charname,
        )
        prompt += "\n" + "\n".join(mem for mem in memory)
        return prompt

    def at_talked_to(self, speech, character):
        """Called when this NPC is talked to by a character."""

        def _respond(response):
            """Async handling of the server response"""

            if thinking_defer and not thinking_defer.called:
                # abort the thinking message if we were fast enough
                thinking_defer.cancel()

            if response:
                # remember this response
                self._add_to_memory(character, self, response)
            else:
                response = "... I'm sorry, I was distracted. Can you repeat?"

            response = self.response_template.format(
                name=self.get_display_name(character), response=response
            )

            # tell the character about it
            if character.location:
                character.location.msg_contents(
                    response,
                    mapping={"character": character},
                    from_obj=self,
                )
            else:
                # fallback if character is not in a location
                character.msg(f"{self.get_display_name(character)} says, {response}")

        # if response takes too long, note that the NPC is thinking.

        def _echo_thinking_message():
            """Echo a random thinking message to the character"""
            thinking_message = choice(
                make_iter(self.db.thinking_messages or self.thinking_messages)
            )
                thinking_message = thinking_message.format(name="$You()")
                character.location.msg_contents(thinking_message, from_obj=self)
            else:
                thinking_message = thinking_message.format(name=self.get_display_name(character))
                character.msg(thinking_message)

        def _handle_cancel_error(failure):
            """Suppress task-cancel errors only"""
            failure.trap(CancelledError)

        thinking_defer = task.deferLater(
            reactor, self.thinking_timeout, _echo_thinking_message
        ).addErrback(_handle_cancel_error)

        # remember latest input in memory, so it's included in the prompt
        self._add_to_memory(character, character, speech)

        # build the prompt
        prompt = self.build_prompt(character, speech)

        # get the response from the LLM server
        yield self.llm_client.get_response(prompt).addCallback(_respond)

class CmdLLMTalk(Command):
    """
    Talk to an NPC

    Usage:
        talk npc <something>
        talk npc with spaces in name = <something>

    """

    key = "talk"

        if "s=" in args:
            name, *speech = args.split("=", 1)
        else:
            name, *speech = args.split(" ", 1)
        self.target_name = name
        self.speech = speech[0] if speech else ""

        if not self.target_name:
            self.caller.msg("Talk to who?")
            return

        target = self.caller.search(self.target_name)
            return
            location.msg_contents(
                f"$You() $conj(say) (to $You(target)): {self.speech}",
                mapping={"target": target},
                from_obj=self.caller,
            )
        if hasattr(target, "at_talked_to"):
            target.at_talked_to(self.speech, self.caller)
        else:
            self.caller.msg(f"{target.key} doesn't seem to want to talk to you.")

"""
Unit tests for the LLM Client and npc.

"""

from anything import Something

from .llm_npc import LLMNPC

class TestLLMClient(BaseEvenniaTestCase):
    """
    Test the LLMNPC class.

    """

        self.npc = create_object(LLMNPC, key="Test NPC")
        self.npc.db_home = None  # fix a bug in test suite
        self.npc.save()

        self.npc.delete()

    @override_settings(LLM_PROMPT_PREFIX="You are a test bot.")
    @patch("evennia.contrib.rpg.llm.llm_npc.task.deferLater")
    def test_npc_at_talked_to(self, mock_deferLater):
        """
        Test the npc's at_talked_to method.
        """
        mock_LLMClient = Mock()
        self.npc.ndb.llm_client = mock_LLMClient

        self.npc.at_talked_to("Hello", self.npc)

        mock_deferLater.assert_called_with(Something, self.npc.thinking_timeout, Something)
        mock_LLMClient.get_response.assert_called_with("You are a test bot.\nTest NPC: Hello")

"""
Language and whisper obfuscation system

Evennia contrib - Griatch 2015

This module is intented to be used with an emoting system (such as
contrib/rpsystem.py). It offers the ability to obfuscate spoken words
in the game in various ways:

- Language: The language functionality defines a pseudo-language map
    to any number of languages.  The string will be obfuscated depending
    on a scaling that (most likely) will be input as a weighted average of
    the language skill of the speaker and listener.
- Whisper: The whisper functionality will gradually "fade out" a
    whisper along as scale 0-1, where the fading is based on gradually
    removing sections of the whisper that is (supposedly) easier to
    overhear (for example "s" sounds tend to be audible even when no other
    meaning can be determined).

## Usage

```python
from evennia.contrib import rplanguage

# need to be done once, here we create the "default" lang
rplanguage.add_language()

say = "This is me talking."
whisper = "This is me whispering.

print rplanguage.obfuscate_language(say, level=0.0)
<<< "This is me talking."
print rplanguage.obfuscate_language(say, level=0.5)
<<< "This is me byngyry."
print rplanguage.obfuscate_language(say, level=1.0)
<<< "Daly ly sy byngyry."

result = rplanguage.obfuscate_whisper(whisper, level=0.0)
<<< "This is me whispering"
result = rplanguage.obfuscate_whisper(whisper, level=0.2)
<<< "This is m- whisp-ring"
result = rplanguage.obfuscate_whisper(whisper, level=0.5)
<<< "---s -s -- ---s------"
result = rplanguage.obfuscate_whisper(whisper, level=0.7)
<<< "---- -- -- ----------"
result = rplanguage.obfuscate_whisper(whisper, level=1.0)
<<< "..."

```

## Custom languages

To set up new languages, you need to run `add_language()`
helper function in this module. The arguments of this function (see below)
are used to store the new language in the database (in the LanguageHandler,
which is a type of Script).

If you want to remember the language definitions, you could put them all
in a module along with the `add_language` call as a quick way to
rebuild the language on a db reset:

```python
# a stand-alone module somewhere under mygame. Just import this
# once to automatically add the language!

from evennia.contrib.rpg.rpsystem import rplanguage
grammar = (...)
vowels = "eaouy"
# etc

rplanguage.add_language(grammar=grammar, vowels=vowels, ...)
```

The variables of `add_language` allows you to customize the "feel" of
the semi-random language you are creating. Especially
the `word_length_variance` helps vary the length of translated
words compared to the original. You can also add your own
dictionary and "fix" random words for a list of input words.

## Example

Below is an example module creating "elvish", using "rounder" vowels and sounds:

```python
# vowel/consonant grammar possibilities
grammar = ("v vv vvc vcc vvcc cvvc vccv vvccv vcvccv vcvcvcc vvccvvcc "
           "vcvvccvvc cvcvvcvvcc vcvcvvccvcvv")

# all not in this group is considered a consonant
vowels = "eaoiuy"

# you need a representative of all of the minimal grammars here, so if a
# grammar v exists, there must be atleast one phoneme available with only
# one vowel in it
phonemes = ("oi oh ee ae aa eh ah ao aw ay er ey ow ia ih iy "
            "oy ua uh uw y p b t d f v t dh s z sh zh ch jh k "
            "ng g m n l r w")

# how much the translation varies in length compared to the original. 0 is
# smallest, higher values give ever bigger randomness (including removing
# short words entirely)
word_length_variance = 1

# if a proper noun (word starting with capitalized letter) should be
# translated or not. If not (default) it means e.g. names will remain
# unchanged across languages.
noun_translate = False

# all proper nouns (words starting with a capital letter not at the beginning
# of a sentence) can have either a postfix or -prefix added at all times
noun_postfix = "'la"

# words in dict will always be translated this way. The 'auto_translations'
# is instead a list or filename to file with words to use to help build a
# bigger dictionary by creating random translations of each word in the
# list *once* and saving the result for subsequent use.
manual_translations = {"the":"y'e", "we":"uyi", "she":"semi", "he":"emi",
                      "you": "do", 'me':'mi','i':'me', 'be':"hy'e", 'and':'y'}

rplanguage.add_language(key="elvish", phonemes=phonemes, grammar=grammar,
                         word_length_variance=word_length_variance,
                         noun_translate=noun_translate,
                         noun_postfix=noun_postfix, vowels=vowels,
                         manual_translations=manual_translations,
                         auto_translations="my_word_file.txt")

```

This will produce a decicively more "rounded" and "soft" language
than the default one. The few manual_translations also make sure
to make it at least look superficially "reasonable".

The `auto_translations` keyword is useful, this accepts either a
list or a path to a file of words (one per line) to automatically
create fixed translations for according to the grammatical rules.
This allows to quickly build a large corpus of translated words
that never change (if this is desired).

"""
import re
from random import choice, randint

#
# Obfuscate language
#

# default language grammar
_PHONEMES = (
    "ea oh ae aa eh ah ao aw ai er ey ow ia ih iy oy ua uh uw a e i u y p b t d f v t dh "
    "s z sh zh ch jh k ng g m n l r w"
)
_VOWELS = "eaoiuy"
# these must be able to be constructed from phonemes (so for example,
# if you have v here, there must exist at least one single-character
# vowel phoneme defined above)
_GRAMMAR = "v cv vc cvv vcc vcv cvcc vccv cvccv cvcvcc cvccvcv vccvccvc cvcvccvv cvcvcvcvv"

_RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.DOTALL + re.UNICODE
_RE_GRAMMAR = re.compile(r"vv|cc|v|c", _RE_FLAGS)
_RE_WORD = re.compile(r"\w+", _RE_FLAGS)
# superfluous chars, except ` ... `
_RE_EXTRA_CHARS = re.compile(r"\s+(?!... )(?=\W)|[,.?;](?!.. )(?=[,?;]|\s+[,.?;])", _RE_FLAGS)

class LanguageError(RuntimeError):
    pass

class LanguageExistsError(LanguageError):
    pass

class LanguageHandler(DefaultScript):
    """
    This is a storage class that should usually not be created on its
    own. It's automatically created by a call to `obfuscate_language`
    or `add_language` below.

    Languages are implemented as a "logical" pseudo- consistent language
    algorith here. The idea is that a language is built up from
    phonemes. These are joined together according to a "grammar" of
    possible phoneme- combinations and allowed characters. It may
    sound simplistic, but this allows to easily make
    "similar-sounding" languages. One can also custom-define a
    dictionary of some common words to give further consistency.
    Optionally, the system also allows an input list of common words
    to be loaded and given random translations. These will be stored
    to disk and will thus not change. This gives a decent "stability"
    of the language but if the goal is to obfuscate, this may allow
    players to eventually learn to understand the gist of a sentence
    even if their characters can not. Any number of languages can be
    created this way.

    This nonsense language will partially replace the actual spoken
    language when so desired (usually because the speaker/listener
    don't know the language well enough).

    """

        "Called when script is first started"
        self.key = "language_handler"
        self.db.language_storage = {}

    def add(
        self,
        key="default",
        phonemes=_PHONEMES,
        grammar=_GRAMMAR,
        word_length_variance=0,
        noun_translate=False,
        noun_prefix="",
        noun_postfix="",
        vowels=_VOWELS,
        manual_translations=None,
        auto_translations=None,
        force=False,
    ):
        """
        Add a new language. Note that you generally only need to do
        this once per language and that adding an existing language
        will re-initialize all the random components to new permanent
        values.

        Args:
            key (str, optional): The name of the language. This
                will be used as an identifier for the language so it
                should be short and unique.
            phonemes (str, optional): Space-separated string of all allowed
                phonemes in this language. If either of the base phonemes
                (c, v, cc, vv) are present in the grammar, the phoneme list must
                at least include one example of each.
            grammar (str): All allowed consonant (c) and vowel (v) combinations
                allowed to build up words. Grammars are broken into the base phonemes
                (c, v, cc, vv) prioritizing the longer bases. So cvv would be a
                the c + vv (would allow for a word like 'die' whereas
                cvcvccc would be c+v+c+v+cc+c (a word like 'galosch').
            word_length_variance (real): The variation of length of words.
                0 means a minimal variance, higher variance may mean words
                have wildly varying length; this strongly affects how the
                language "looks".
            noun_translate (bool, optional): If a proper noun should be translated or
                not. By default they will not, allowing for e.g. the names of characters
                to be understandable. A 'noun' is identified as a capitalized word
                *not at the start of a sentence*. This simple metric means that names
                starting a sentence always will be translated (- but hey, maybe
                the fantasy language just never uses a noun at the beginning of
                sentences, who knows?)
            noun_prefix (str, optional): A prefix to go before every noun
                in this language (if any).
            noun_postfix (str, optuonal): A postfix to go after every noun
                in this language (if any, usually best to avoid combining
                with `noun_prefix` or language becomes very wordy).
            vowels (str, optional): Every vowel allowed in this language.
            manual_translations (dict, optional): This allows for custom-setting
                certain words in the language to mean the same thing. It is
                on the form `{real_word: fictional_word}`, for example
                `{"the", "y'e"}` .
            auto_translations (str or list, optional): These are lists
                words that should be auto-translated with a random, but
                fixed, translation. If a path to a file, this file should
                contain a list of words to produce translations for, one
                word per line.  If a list, the list's elements should be
                the words to translate.  The `manual_translations` will
                always override overlapping translations created
                automatically.
            force (bool, optional): Unless true, will not allow the addition
                of a language that is already created.

        Raises:
            LanguageExistsError: Raised if trying to adding a language
                with a key that already exists, without `force` being set.
        Notes:
            The `word_file` is for example a word-frequency list for
            the N most common words in the host language. The
            translations will be random, but will be stored
            persistently to always be the same. This allows for
            building a quick, decently-sounding fictive language that
            tend to produce the same "translation" (mostly) with the
            same input sentence.

        """
        if key in self.db.language_storage and not force:
            raise LanguageExistsError(
                "Language is already created. Re-adding it will re-build"
                " its dictionary map. Use 'force=True' keyword if you are sure."
            )

        # create grammar_component->phoneme mapping
        # {"vv": ["ea", "oh", ...], ...}
        grammar2phonemes = defaultdict(list)
        for phoneme in phonemes.split():
            if re.search(r"\W", phoneme, re.U):
                raise LanguageError("The phoneme '%s' contains an invalid character." % phoneme)
            gram = "".join(["v" if char in vowels else "c" for char in phoneme])
            grammar2phonemes[gram].append(phoneme)

        # allowed grammar are grouped by length
        gramdict = defaultdict(list)
        for gram in grammar.split():
            if re.search(r"\W|(!=[cv])", gram):
                raise LanguageError(
                    "The grammar '%s' is invalid (only 'c' and 'v' are allowed)" % gram
                )
            gramdict[len(gram)].append(gram)
        grammar = dict(gramdict)

        # create automatic translation
        translation = {}

        if auto_translations:
            if isinstance(auto_translations, str):
                # path to a file rather than a list
                with open(auto_translations, "r") as f:
                    auto_translations = f.readlines()
            for word in auto_translations:
                word = word.strip()
                lword = len(word)
                new_word = ""
                wlen = max(0, lword + sum(randint(-1, 1) for i in range(word_length_variance)))
                if wlen not in grammar:
                    # always create a translation, use random length
                    structure = choice(grammar[choice(list(grammar))])
                else:
                    # use the corresponding length
                    structure = choice(grammar[wlen])
                for match in _RE_GRAMMAR.finditer(structure):
                    try:
                        new_word += choice(grammar2phonemes[match.group()])
                        raise IndexError(
                            "Could not find a matching phoneme for the grammar "
                            f"'{match.group()}'. Make there is at least one phoneme matching this "
                            "combination of consonants and vowels."
                        )
                translation[word.lower()] = new_word.lower()

        if manual_translations:
            # update with manual translations
            translation.update(
                dict((key.lower(), value.lower()) for key, value in manual_translations.items())
            )

        # store data
        storage = {
            "translation": translation,
            "grammar": grammar,
            "grammar2phonemes": dict(grammar2phonemes),
            "word_length_variance": word_length_variance,
            "noun_translate": noun_translate,
            "noun_prefix": noun_prefix,
            "noun_postfix": noun_postfix,
        }
        self.db.language_storage[key] = storage

    def _translate_sub(self, match):
        """
        Replacer method called by re.sub when
        traversing the language string.

        Args:
            match (re.matchobj): Match object from regex.

        Returns:
            converted word.
        Notes:
            Assumes self.lastword and self.level is available
            on the object.

        """
        word = match.group()
        lword = len(word)

        # find out what preceeded this word
        wpos = match.start()
        preceeding = match.string[:wpos].strip()
        start_sentence = preceeding.endswith((".", "!", "?")) or not preceeding

        if len(word) <= self.level:
            # below level. Don't translate
            new_word = word
        else:
            # try to translate the word from dictionary
            new_word = self.language["translation"].get(word.lower(), "")
            if not new_word:
                # no dictionary translation. Generate one

                # make up translation on the fly. Length can
                # vary from un-translated word.
                wlen = max(
                    0,
                    lword
                    + sum(randint(-1, 1) for i in range(self.language["word_length_variance"])),
                )
                grammar = self.language["grammar"]
                    if randint(0, 1) == 0:
                        # this word has no direct translation!
                        wlen = 0
                        new_word = ""
                    else:
                        # use random word length
                        wlen = choice(list(grammar.keys()))

                if wlen:
                    grammar2phonemes = self.language["grammar2phonemes"]
                    for match in _RE_GRAMMAR.finditer(structure):
                        # there are only four combinations: vv,cc,c,v
                        try:
                            new_word += choice(grammar2phonemes[match.group()])
                        except KeyError:
                            logger.log_trace(
                                "You need to supply at least one example of each of "
                                "the four base phonemes (c, v, cc, vv)"
                            )
                            # abort translation here
                            new_word = ""
                            break

                if word.istitle():
                    if not start_sentence:
                        # this is a noun. We miss nouns at the start of
                        # sentences this way, but it's as good as we can get
                        # with this simple analysis. Maybe the fantasy language
                        # just don't consider nouns at the beginning of
                        # sentences, who knows?
                        if not self.language.get("noun_translate", False):
                            # don't translate what we identify as proper nouns (names)
                            new_word = word

                        # add noun prefix and/or postfix
                        new_word = "{prefix}{word}{postfix}".format(
                            prefix=self.language["noun_prefix"],
                            word=new_word.capitalize(),
                            postfix=self.language["noun_postfix"],
                        )

            if len(word) > 1 and word.isupper():
                # keep LOUD words loud also when translated
                new_word = new_word.upper()

            if start_sentence:
                new_word = new_word.capitalize()

        return new_word

    def translate(self, text, level=0.0, language="default"):
        """
        Translate the text according to the given level.

        Args:
            text (str): The text to translate
            level (real): Value between 0.0 and 1.0, where
                0.0 means no obfuscation (text returned unchanged) and
                1.0 means full conversion of every word. The closer to
                1, the shorter words will be translated.
            language (str): The language key identifier.

        Returns:
            text (str): A translated string.

        """
        if level == 0.0:
            # no translation
        language = self.db.language_storage.get(language, None)
        if not language:
        self.language = language

        # configuring the translation
        self.level = int(10 * (1.0 - max(0, min(level, 1.0))))
        translation = _RE_WORD.sub(self._translate_sub, text)
        # the substitution may create too long empty spaces, remove those
        return _RE_EXTRA_CHARS.sub("", translation)

# Language access functions

_LANGUAGE_HANDLER = None

def obfuscate_language(text, level=0.0, language="default"):
    """
    Main access method for the language parser.

    Args:
        text (str): Text to obfuscate.
        level (real, optional): A value from 0.0-1.0 determining
            the level of obfuscation where 0 means no obfuscation
            (string returned unchanged) and 1.0 means the entire
            string is obfuscated.
        language (str, optional): The identifier of a language
            the system understands.

    Returns:
        translated (str): The translated text.

    """
    # initialize the language handler and cache it
    global _LANGUAGE_HANDLER
    if not _LANGUAGE_HANDLER:
        try:
            _LANGUAGE_HANDLER = LanguageHandler.objects.get(db_key="language_handler")
        except LanguageHandler.DoesNotExist:
            if not _LANGUAGE_HANDLER:
                from evennia import create_script

                _LANGUAGE_HANDLER = create_script(LanguageHandler)
    return _LANGUAGE_HANDLER.translate(text, level=level, language=language)

def add_language(**kwargs):
    """
    Access function to creating a new language. See the docstring of
    `LanguageHandler.add` for list of keyword arguments.

    """
        try:

    _LANGUAGE_HANDLER.add(**kwargs)

def available_languages():
    """
    Returns all available language keys.

    Returns:
        languages (list): List of key strings of all available
        languages.
    """
        try:

    return list(_LANGUAGE_HANDLER.attributes.get("language_storage", {}))

# -----------------------------------------------------------------------------
#
# Whisper obscuration
#
# This obsucration table is designed by obscuring certain vowels first,
# following by consonants that tend to be more audible over long distances,
# like s. Finally it does non-auditory replacements, like exclamation marks and
# capitalized letters (assumed to be spoken louder) that may still give a user
# some idea of the sentence structure. Then the  word lengths are also
# obfuscated and finally the whisper length itself.
#
# ------------------------------------------------------------------------------

_RE_WHISPER_OBSCURE = [
    re.compile(r"^$", _RE_FLAGS),  # This is a Test! #0 full whisper
    re.compile(r"[ae]", _RE_FLAGS),  # This -s - Test! #1 add uy
    re.compile(r"[aeuy]", _RE_FLAGS),  # This -s - Test! #2 add oue
    re.compile(r"[aeiouy]", _RE_FLAGS),  # Th-s -s - T-st! #3 add all consonants
    re.compile(r"[aeiouybdhjlmnpqrv]", _RE_FLAGS),  # T--s -s - T-st! #4 add hard consonants
    re.compile(r"[a-eg-rt-z]", _RE_FLAGS),  # T--s -s - T-s-! #5 add all capitals
    re.compile(r"[A-EG-RT-Za-eg-rt-z]", _RE_FLAGS),  # ---s -s - --s-! #6 add f
    re.compile(r"[A-EG-RT-Za-rt-z]", _RE_FLAGS),  # ---s -s - --s-! #7 add s
    re.compile(r"[A-EG-RT-Za-z]", _RE_FLAGS),  # ---- -- - ----! #8 add capital F
    re.compile(r"[A-RT-Za-z]", _RE_FLAGS),  # ---- -- - ----! #9 add capital S
    re.compile(r"[\w]", _RE_FLAGS),  # ---- -- - ----! #10 non-alphanumerals
    re.compile(r"[\S]", _RE_FLAGS),  # ---- -- - ----  #11 words
    re.compile(r"[\w\W]", _RE_FLAGS),  # --------------  #12 whisper length
    re.compile(r".*", _RE_FLAGS),
]  # ...             #13 (always same length)

def obfuscate_whisper(whisper, level=0.0):
    """
    Obfuscate whisper depending on a pre-calculated level
    (that may depend on distance, listening skill etc)

    Args:
        whisper (str): The whisper string to obscure. The
            entire string will be considered in the obscuration.
        level (real, optional): This is a value 0-1, where 0
            means not obscured (whisper returned unchanged) and 1
            means fully obscured.

    """
    level = min(max(0.0, level), 1.0)
    olevel = int(13.0 * level)
    if olevel == 13:
        return "..."
    else:
        return _RE_WHISPER_OBSCURE[olevel].sub("-", whisper)

"""
Roleplaying base system for Evennia

Contribution - Griatch, 2015

This module contains the ContribRPObject, ContribRPRoom and
ContribRPCharacter typeclasses.  If you inherit your
objects/rooms/character from these (or make them the defaults) from
these you will get the following features:

- Objects/Rooms will get the ability to have poses and will report
  the poses of items inside them (the latter most useful for Rooms).
- Characters will get poses and also sdescs (short descriptions)
that will be used instead of their keys. They will gain commands
for managing recognition (custom sdesc-replacement), masking
themselves as well as an advanced free-form emote command.

In more detail, This RP base system introduces the following features
to a game, common to many RP-centric games:

- emote system using director stance emoting (names/sdescs).
    This uses a customizable replacement noun (/me, @ etc) to
    represent you in the emote. You can use /sdesc, /nick, /key or
    /alias to reference objects in the room. You can use any
    number of sdesc sub-parts to differentiate a local sdesc, or
    use /1-sdesc etc to differentiate them. The emote also
    identifies nested says and separates case.
- sdesc obscuration of real character names for use in emotes
    and in any referencing such as object.search().  This relies
    on an SdescHandler `sdesc` being set on the Character and
    makes use of a custom Character.get_display_name hook. If
    sdesc is not set, the character's `key` is used instead. This
    is particularly used in the emoting system.
- recog system to assign your own nicknames to characters, can then
    be used for referencing. The user may recog a user and assign
    any personal nick to them. This will be shown in descriptions
    and used to reference them. This is making use of the nick
    functionality of Evennia.
- masks to hide your identity (using a simple lock).
- pose system to set room-persistent poses, visible in room
    descriptions and when looking at the person/object.  This is a
    simple Attribute that modifies how the characters is viewed when
    in a room as sdesc + pose.
- in-emote says, including seamless integration with language
    obscuration routine (such as contrib/rpg/rplanguage.py)

Add `RPSystemCmdSet` from this module to your CharacterCmdSet:

```python
# mygame/commands/default_cmdsets.py

# ...

from evennia.contrib.rpg.rpsystem.rpsystem import RPSystemCmdSet  <---

class CharacterCmdSet(default_cmds.CharacterCmdset):
    # ...
        # ...
        self.add(RPSystemCmdSet())  # <---

```

You also need to make your Characters/Objects/Rooms inherit from
the typeclasses in this module:

```python
# in mygame/typeclasses/characters.py

from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPCharacter

class Character(ContribRPCharacter):
    # ...

```

```python
# in mygame/typeclasses/objects.py

from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject

class Object(ContribRPObject):
    # ...

```

```python
# in mygame/typeclasses/rooms.py

from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPRoom

class Room(ContribRPRoom):
    # ...

```

Examples:

> look
Tavern
The tavern is full of nice people

*A tall man* is standing by the bar.

Above is an example of a player with an sdesc "a tall man". It is also
an example of a static *pose*: The "standing by the bar" has been set
by the player of the tall man, so that people looking at him can tell
at a glance what is going on.

> emote /me looks at /Tall and says "Hello!"

I see:
    Griatch looks at Tall man and says "Hello".
Tall man (assuming his name is Tom) sees:
    The godlike figure looks at Tom and says "Hello".

Note that by default, the case of the tag matters, so `/tall` will
lead to 'tall man' while `/Tall` will become 'Tall man' and /TALL
becomes /TALL MAN. If you don't want this behavior, you can pass
case_sensitive=False to the `send_emote` function.

Extra Installation Instructions:

1. In typeclasses/character.py:
   Import the `ContribRPCharacter` class:
       `from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPCharacter`
   Inherit ContribRPCharacter:
       Change "class Character(DefaultCharacter):" to
       `class Character(ContribRPCharacter):`
   If you have any overriden calls in `at_object_creation(self)`:
       Add `super().at_object_creation()` as the top line.
2. In `typeclasses/rooms.py`:
       Import the `ContribRPRoom` class:
       `from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPRoom`
   Inherit `ContribRPRoom`:
       Change `class Room(DefaultRoom):` to
       `class Room(ContribRPRoom):`
3. In `typeclasses/objects.py`
       Import the `ContribRPObject` class:
       `from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject`
   Inherit `ContribRPObject`:
       Change `class Object(DefaultObject):` to
       `class Object(ContribRPObject):`
4. Reload the server (`reload` or from console: "evennia reload")
5. Force typeclass updates as required. Example for your character:
       `type/reset/force me = typeclasses.characters.Character`

"""
import re

import inflect

from evennia.objects.objects import DefaultCharacter, DefaultObject
from evennia.utils import ansi, logger
)

_INFLECT = inflect.engine()

# Emote parser

# Settings

# The prefix is the (single-character) symbol used to find the start
# of a object reference, such as /tall (note that
# the system will understand multi-word references like '/a tall man' too).
_PREFIX = "/"

# The num_sep is the (single-character) symbol used to separate the
# sdesc from the number when  trying to separate identical sdescs from
# one another. This is the same syntax used in the rest of Evennia, so
# by default, multiple "tall" can be separated by entering 1-tall,
# 2-tall etc.
_NUM_SEP = "-"

# Texts

_EMOTE_NOMATCH_ERROR = """|RNo match for |r{ref}|R.|n"""

_EMOTE_MULTIMATCH_ERROR = """|RMultiple possibilities for {ref}:
    |r{reflist}|n"""

_RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.UNICODE

_RE_PREFIX = re.compile(rf"^{_PREFIX}", re.UNICODE)

# This regex will return groups (num, word), where num is an optional counter to
# separate multimatches from one another and word is the first word in the
# marker. So entering "/tall man" will return groups ("", "tall")
# and "/2-tall man" will return groups ("2", "tall").
# the negative lookbehind for [:/] is to avoid http:// urls being detected as a /sdesc
_RE_OBJ_REF_START = re.compile(rf"(?<![:/]){_PREFIX}(?:([0-9]+){_NUM_SEP})*(\w+)", _RE_FLAGS)

_RE_LEFT_BRACKETS = re.compile(r"\{+", _RE_FLAGS)
_RE_RIGHT_BRACKETS = re.compile(r"\}+", _RE_FLAGS)
# Reference markers are used internally when distributing the emote to
# all that can see it. They are never seen by players and are on the form {#dbref<char>}
# with the <char> indicating case of the original reference query (like ^ for uppercase)
_RE_REF = re.compile(r"\{+\#([0-9]+[\^\~tv]{0,1})\}+")

# This regex is used to quickly reference one self in an emote.
_RE_SELF_REF = re.compile(r"(/me|@)(?=\W+)", _RE_FLAGS)

# regex for non-alphanumberic end of a string
_RE_CHAREND = re.compile(r"\W+$", _RE_FLAGS)

# reference markers for language
_RE_REF_LANG = re.compile(r"\{+\##([0-9]+)\}+")
# language says in the emote are on the form "..." or langname"..." (no spaces).
# this regex returns in groups (langname, say), where langname can be empty.
_RE_LANGUAGE = re.compile(r"(?:\((\w+)\))*(\".+?\")")

# the emote parser works in two steps:
#  1) convert the incoming emote into an intermediary
#     form with all object references mapped to ids.
#  2) for every person seeing the emote, parse this
#     intermediary form into the one valid for that char.

class EmoteError(Exception):
    pass

class SdescError(Exception):
    pass

class RecogError(Exception):
    pass

class LanguageError(Exception):
    pass

def _get_case_ref(string):
    """
    Helper function which parses capitalization and
    returns the appropriate case-ref character for emotes.
    """
    # default to retaining the original case
    case = "~"
    # internal flags for the case used for the original /query
    # - t for titled input (like /Name)
    # - ^ for all upercase input (like /NAME)
    # - v for lower-case input (like /name)
    # - ~ for mixed case input (like /nAmE)
    if string.istitle():
        case = "t"
    elif string.isupper():
        case = "^"
    elif string.islower():
        case = "v"

    return case

# emoting mechanisms
def parse_language(speaker, emote):
    """
    Parse the emote for language. This is
    used with a plugin for handling languages.

    Args:
        speaker (Object): The object speaking.
        emote (str): An emote possibly containing
            language references.

    Returns:
        (emote, mapping) (tuple): A tuple where the
            `emote` is the emote string with all says
            (including quotes) replaced with reference
            markers on the form {##n} where n is a running
            number. The `mapping` is a dictionary between
            the markers and a tuple (langname, saytext), where
            langname can be None.
    Raises:
        evennia.contrib.rpg.rpsystem.LanguageError: If an invalid language was
        specified.

    Notes:
        Note that no errors are raised if the wrong language identifier
        is given.
        This data, together with the identity of the speaker, is
        intended to be used by the "listener" later, since with this
        information the language skill of the speaker can be offset to
        the language skill of the listener to determine how much
        information is actually conveyed.

    """
    # escape mapping syntax on the form {##id} if it exists already in emote,
    # if so it is replaced with just "id".
    emote = _RE_REF_LANG.sub(r"\1", emote)

    errors = []
    mapping = {}
    for imatch, say_match in enumerate(reversed(list(_RE_LANGUAGE.finditer(emote)))):
        # process matches backwards to be able to replace
        # in-place without messing up indexes for future matches
        # note that saytext includes surrounding "...".
        langname, saytext = say_match.groups()
        istart, iend = say_match.start(), say_match.end()
        # the key is simply the running match in the emote
        key = f"##{imatch}"
        # replace say with ref markers in emote
        emote = "{start}{{{key}}}{end}".format(start=emote[:istart], key=key, end=emote[iend:])
        mapping[key] = (langname, saytext)

    if errors:
        # catch errors and report
        raise LanguageError("\n".join(errors))

    # at this point all says have been replaced with {##nn} markers
    # and mapping maps 1:1 to this.
    return emote, mapping

def parse_sdescs_and_recogs(
    sender, candidates, string, search_mode=False, case_sensitive=True, fallback=None
):
    """
    Read a raw emote and parse it into an intermediary
    format for distributing to all observers.

    Args:
        sender (Object): The object sending the emote. This object's
            recog data will be considered in the parsing.
        candidates (iterable): A list of objects valid for referencing
            in the emote.
    string (str): The string (like an emote) we want to analyze for keywords.
    search_mode (bool, optional): If `True`, the "emote" is a query string
        we want to analyze. If so, the return value is changed.
    case_sensitive (bool, optional): If set, the case of /refs matter, so that
        /tall will come out as 'tall man' while /Tall will become 'Tall man'.
        This allows for more grammatically correct emotes at the cost of being
        a little more to learn for players. If disabled, the original sdesc case
        is always kept and are inserted as-is.
    fallback (string, optional): If set, any references that don't match a target
        will be replaced with the fallback string. If `None` (default), the
        parsing will fail and give a warning about the missing reference.

    Returns:
        (emote, mapping) (tuple): If `search_mode` is `False`
            (default), a tuple where the emote is the emote string, with
            all references replaced with internal-representation {#dbref}
            markers and mapping is a dictionary `{"#dbref":obj, ...}`.
        result (list): If `search_mode` is `True` we are
            performing a search query on `string`, looking for a specific
            object. A list with zero, one or more matches.

    Raises:
        EmoteException: For various ref-matching errors.

    Notes:
        The parser analyzes and should understand the following
        _PREFIX-tagged structures in the emote:
        - self-reference (/me)
        - recogs (any part of it) stored on emoter, matching obj in `candidates`.
        - sdesc (any part of it) from any obj in `candidates`.
        - N-sdesc, N-recog separating multi-matches (1-tall, 2-tall)
        - says, "..." are

    """
    # build a list of candidates with all possible referrable names
    # include 'me' keyword for self-ref
    candidate_map = []
    for obj in candidates:
        # check if sender has any recogs for obj and add
        if hasattr(sender, "recog"):
            if recog := sender.recog.get(obj):
                candidate_map.append((obj, recog))
        # check if obj has an sdesc and add
        if hasattr(obj, "sdesc"):
            candidate_map.append((obj, obj.sdesc.get()))
        # if no sdesc, include key plus aliases instead
        else:
            candidate_map.append((obj, obj.key))
        candidate_map.extend([(obj, alias) for alias in obj.aliases.all()])

    # escape mapping syntax on the form {#id} if it exists already in emote,
    string = _RE_REF.sub(r"\1", string)
    # escape loose { } brackets since this will clash with formatting
    string = _RE_LEFT_BRACKETS.sub("{{", string)
    string = _RE_RIGHT_BRACKETS.sub("}}", string)

    # we now loop over all references and analyze them
    errors = []
    obj = None
    nmatches = 0
    # first, find and replace any self-refs
    for self_match in list(_RE_SELF_REF.finditer(string)):
        matched = self_match.group()
        case = _get_case_ref(matched.lstrip(_PREFIX)) if case_sensitive else ""
        key = f"#{sender.id}{case}"
        # replaced with ref
        string = _RE_SELF_REF.sub(f"{{{key}}}", string, count=1)
        mapping[key] = sender

    for marker_match in reversed(list(_RE_OBJ_REF_START.finditer(string))):
        # we scan backwards so we can replace in-situ without messing
        # up later occurrences. Given a marker match, query from
        # start index forward for all candidates.

        # first see if there is a number given (e.g. 1-tall)
        num_identifier, _ = marker_match.groups("")  # return "" if no match, rather than None
        match_index = marker_match.start()
        # split the emote string at the reference marker, to process everything after it
        head = string[:match_index]
        tail = string[match_index + 1 :]

        if search_mode:
            # match the candidates against the whole search string after the marker
            rquery = "".join(
                [
                    r"\b(" + re.escape(word.strip(punctuation)) + r").*"
                    for word in iter(tail.split())
                ]
            )
            matches = (
                (re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map
            )
            # filter out any non-matching candidates
            bestmatches = [(obj, match.group()) for match, obj, text in matches if match]

        else:
            # to find the longest match, we start from the marker and lengthen the
            # match query one word at a time.
            word_list = []
            bestmatches = []
            # preserve punctuation when splitting
            tail = re.split("(\W)", tail)
            iend = 0
            for i, item in enumerate(tail):
                # don't add non-word characters to the search query
                if not item.isalpha():
                    continue
                word_list.append(item)
                rquery = "".join([r"\b(" + re.escape(word) + r").*" for word in word_list])
                # match candidates against the current set of words
                matches = (
                    (re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map
                )
                matches = [(obj, match.group()) for match, obj, text in matches if match]
                if len(matches) == 0:
                    # no matches at this length, keep previous iteration as best
                    break
                # since this is the longest match so far, set latest match set as best matches
                bestmatches = matches
                # save current index as end point of matched text
                iend = i

            # save search string
            matched_text = "".join(tail[1:iend])
            # recombine remainder of emote back into a string
            tail = "".join(tail[iend + 1 :])

        nmatches = len(bestmatches)

        if not nmatches:
            # no matches
            obj = None
            nmatches = 0
        elif nmatches == 1:
            # an exact match.
            obj, match_str = bestmatches[0]
        elif all(bestmatches[0][0].id == obj.id for obj, text in bestmatches):
            # multi-match but all matches actually reference the same
            # obj (could happen with clashing recogs + sdescs)
            nmatches = 1
        else:
            # multi-match.
            # was a numerical identifier given to help us separate the multi-match?
            inum = min(max(0, int(num_identifier) - 1), nmatches - 1) if num_identifier else None
            if inum is not None:
                # A valid inum is given. Use this to separate data.
                obj, match_str = bestmatches[inum]
                nmatches = 1
            else:
                # no identifier given - a real multimatch.
                obj = bestmatches

            # single-object search mode. Don't continue loop.
            break
        elif nmatches == 0:
            if fallback:
                # replace unmatched reference with the fallback string
                string = f"{head}{fallback}{tail}"
            else:
                errors.append(_EMOTE_NOMATCH_ERROR.format(ref=marker_match.group()))
            # a unique match - parse into intermediary representation
            case = _get_case_ref(marker_match.group()) if case_sensitive else ""
            # recombine emote with matched text replaced by ref
            key = f"#{obj.id}{case}"
            string = f"{head}{{{key}}}{tail}"
            mapping[key] = obj

        else:
            # multimatch error
            refname = marker_match.group()
            reflist = [
                "{num}{sep}{name} ({text}{key})".format(
                    num=inum + 1,
                    sep=_NUM_SEP,
                    name=_RE_PREFIX.sub("", refname),
                    text=text,
                    key=f" ({sender.key})" if sender == ob else "",
                )
                for inum, (ob, text) in enumerate(obj)
            ]
                _EMOTE_MULTIMATCH_ERROR.format(
                    ref=marker_match.group(), reflist="\n    ".join(reflist)
                )
            )
    if search_mode:
        # return list of object(s) matching
        if nmatches == 0:
            return []
            return [obj]
        else:
            return [tup[0] for tup in obj]

    if errors:
        # make sure to not let errors through.
        raise EmoteError("\n".join(errors))

    # at this point all references have been replaced with {#xxx} markers and the mapping contains
    # a 1:1 mapping between those inline markers and objects.
    return string, mapping

def send_emote(sender, receivers, emote, msg_type="pose", anonymous_add="first", **kwargs):
    """
    Main access function for distribute an emote.

    Args:
        sender (Object): The one sending the emote.
        receivers (iterable): Receivers of the emote. These
            will also form the basis for which sdescs are
            'valid' to use in the emote.
        emote (str): The raw emote string as input by emoter.
        msg_type (str): The type of emote this is. "say" or "pose"
            for example. This is arbitrary and used for generating
            extra data for .msg(text) tuple.
        anonymous_add (str or None, optional): If `sender` is not
            self-referencing in the emote, this will auto-add
            `sender`'s data to the emote. Possible values are
            - None: No auto-add at anonymous emote
            - 'last': Add sender to the end of emote as [sender]
            - 'first': Prepend sender to start of emote.
    Kwargs:
        case_sensitive (bool): Defaults to True, but can be unset
            here. When enabled, /tall will lead to a lowercase
            'tall man' while /Tall will lead to 'Tall man' and
            /TALL will lead to 'TALL MAN'. If disabled, the sdesc's
            case will always be used, regardless of the /ref case used.
        any: Other kwargs will be passed on into the receiver's process_sdesc and
            process_recog methods, and can thus be used to customize those.

    """
    case_sensitive = kwargs.pop("case_sensitive", True)
    fallback = kwargs.pop("fallback", None)
    try:
        emote, obj_mapping = parse_sdescs_and_recogs(
            sender, receivers, emote, case_sensitive=case_sensitive, fallback=fallback
        )
        emote, language_mapping = parse_language(sender, emote)
    except (EmoteError, LanguageError) as err:
        # handle all error messages, don't hide actual coding errors
        sender.msg(str(err))
        return

    skey = f"#{sender.id}"

    # we escape the object mappings since we'll do the language ones first
    # (the text could have nested object mappings).
    emote = _RE_REF.sub(r"{{#\1}}", emote)
    # if anonymous_add is passed as a kwarg, collect and remove it from kwargs
    if "anonymous_add" in kwargs:
        anonymous_add = kwargs.pop("anonymous_add")
    # make sure to catch all possible self-refs
    self_refs = [f"{skey}{ref}" for ref in ("t", "^", "v", "~", "")]
    if anonymous_add and not any(1 for tag in obj_mapping if tag in self_refs):
        # no self-reference in the emote - add it
        if anonymous_add == "first":
            # add case flag for initial caps
            skey += "t"
            # don't put a space after the self-ref if it's a possessive emote
            femote = "{key}{emote}" if emote.startswith("'") else "{key} {emote}"
        else:
            # add it to the end
            femote = "{emote} [{key}]"
        emote = femote.format(key="{{" + skey + "}}", emote=emote)
        obj_mapping[skey] = sender

    # broadcast emote to everyone
    for receiver in receivers:
        # first handle the language mapping, which always produce different keys ##nn
        if hasattr(receiver, "process_language") and callable(receiver.process_language):
            receiver_lang_mapping = {
                key: receiver.process_language(saytext, sender, langname)
                for key, (langname, saytext) in language_mapping.items()
            }
        else:
                key: saytext for key, (langname, saytext) in language_mapping.items()
            }
        # map the language {##num} markers. This will convert the escaped sdesc markers on
        # the form {{#num}} to {#num} markers ready to sdesc-map in the next step.
        sendemote = emote.format_map(receiver_lang_mapping)

        # map the ref keys to sdescs
        receiver_sdesc_mapping = dict(
            (
                ref,
                obj.get_display_name(receiver, ref=ref, noid=True),
            )
            for ref, obj in obj_mapping.items()
        )

        # do the template replacement of the sdesc/recog {#num} markers
            text=(sendemote.format_map(receiver_sdesc_mapping), {"type": msg_type}),
            from_obj=sender,
            **kwargs,
        )

# Handlers for sdesc and recog

class SdescHandler:
    """
    This Handler wraps all operations with sdescs. We
    need to use this since we do a lot preparations on
    sdescs when updating them, in order for them to be
    efficient to search for and query.

    The handler stores data in the following Attributes

        _sdesc   - a string
        _regex  - an empty dictionary

    """

        """

        Args:
            obj (Object): The entity on which this handler is stored.

        """
        self.sdesc = ""
        self._cache()

    def _cache(self):
        """
        Cache data from storage
        """
        self.sdesc = self.obj.attributes.get("_sdesc", default=self.obj.key)

    def add(self, sdesc, max_length=60):
        """
        Add a new sdesc to object, replacing the old one.

        Args:
            sdesc (str): The sdesc to set. This may be stripped
                of control sequences before setting.
            max_length (int, optional): The max limit of the sdesc.

        Returns:
            sdesc (str): The actually set sdesc.

        Raises:
            SdescError: If the sdesc is empty, can not be set or is
            longer than `max_length`.

        """
        # strip emote components from sdesc
        sdesc = _RE_REF.sub(
            r"\1",
            _RE_REF_LANG.sub(
                r"\1",
                _RE_SELF_REF.sub(r"", _RE_LANGUAGE.sub(r"", _RE_OBJ_REF_START.sub(r"", sdesc))),
            ),
        )

        # make an sdesc clean of ANSI codes
        cleaned_sdesc = ansi.strip_ansi(sdesc)

        if not cleaned_sdesc:
            raise SdescError("Short desc cannot be empty.")

        if len(cleaned_sdesc) > max_length:
            raise SdescError(
                "Short desc can max be {} chars long (was {} chars).".format(
                    max_length, len(cleaned_sdesc)
                )
            )

        # store to attributes
        self.obj.attributes.add("_sdesc", sdesc)
        # local caching
        self.sdesc = sdesc

        return sdesc

        """
        Clear sdesc.

        """
        self.obj.attributes.remove("_sdesc")

        """
        Simple getter. The sdesc should never be allowed to
        be empty, but if it is we must fall back to the key.

        """
        return self.sdesc or self.obj.key

class RecogHandler:
    """
    This handler manages the recognition mapping
    of an Object.

    The handler stores data in Attributes as dictionaries of
    the following names:

        _recog_ref2recog
        _recog_obj2recog

    """

        """

        Args:

        """
        # mappings
        self.ref2recog = {}
        self.obj2recog = {}

        """
        Load data to handler cache
        """
        self.ref2recog = self.obj.attributes.get("_recog_ref2recog", default={})
        obj2recog = self.obj.attributes.get("_recog_obj2recog", default={})
        self.obj2recog = dict((obj, recog) for obj, recog in obj2recog.items() if obj)

    def add(self, obj, recog, max_length=60):
        """
        Assign a custom recog (nick) to the given object.

        Args:
            obj (Object): The object ot associate with the recog
                string. This is usually determined from the sdesc in the
                room by a call to parse_sdescs_and_recogs, but can also be
                given.
            recog (str): The replacement string to use with this object.
            max_length (int, optional): The max length of the recog string.

        Returns:
            recog (str): The (possibly cleaned up) recog string actually set.

        Raises:
            SdescError: When recog could not be set or sdesc longer
                than `max_length`.

        """
        if not obj.access(self.obj, "enable_recog", default=True):
            raise SdescError("This person is unrecognizeable.")

        # strip emote components from recog
        recog = _RE_REF.sub(
            r"\1",
                r"\1",
                _RE_SELF_REF.sub(r"", _RE_LANGUAGE.sub(r"", _RE_OBJ_REF_START.sub(r"", recog))),
            ),
        )

        # make an recog clean of ANSI codes
        cleaned_recog = ansi.strip_ansi(recog)

        if not cleaned_recog:
            raise SdescError("Recog string cannot be empty.")

        if len(cleaned_recog) > max_length:
            raise RecogError(
                "Recog string cannot be longer than {} chars (was {} chars)".format(
                    max_length, len(cleaned_recog)
                )
            )

        # mapping #dbref:obj
        key = f"#{obj.id}"
        self.obj.attributes.get("_recog_ref2recog", default={})[key] = recog
        self.obj.attributes.get("_recog_obj2recog", default={})[obj] = recog
        self.ref2recog[key] = recog
        self.obj2recog[obj] = recog
        return recog

    def get(self, obj):
        """
        Get recog replacement string, if one exists.

        Args:
            obj (Object): The object, whose sdesc to replace
        Returns:
            recog (str or None): The replacement string to use, or
                None if there is no recog for this object.

        Notes:
            This method will respect a "enable_recog" lock set on
            `obj` (True by default) in order to turn off recog
            mechanism. This is useful for adding masks/hoods etc.
        """
        if obj.access(self.obj, "enable_recog", default=True):
            # check an eventual recog_masked lock on the object
            # to avoid revealing masked characters. If lock
            # does not exist, pass automatically.
            return self.obj2recog.get(obj, None)
        else:
            # recog_mask lock not passed, disable recog

        """
        Get a mapping of the recogs stored in handler.

        Returns:
            recogs (dict): A mapping of {recog: obj} stored in handler.

        """
        return {self.obj2recog[obj]: obj for obj in self.obj2recog.keys()}

    def remove(self, obj):
        """
        Clear recog for a given object.

        Args:
            obj (Object): The object for which to remove recog.
        """
        if obj in self.obj2recog:
            del self.obj.db._recog_obj2recog[obj]
            del self.obj.db._recog_ref2recog[f"#{obj.id}"]

# RP Commands

class RPCommand(Command):
    "simple parent"

        "strip extra whitespace"

class CmdEmote(RPCommand):  # replaces the main emote
    """
    Emote an action, allowing dynamic replacement of
    text in the emote.

    Usage:
      emote text

    Example:
      emote /me looks around.
      emote With a flurry /me attacks /tall man with his sword.
      emote "Hello", /me says.

    Describes an event in the world. This allows the use of /ref
    markers to replace with the short descriptions or recognized
    strings of objects in the same room. These will be translated to
    emotes to match each person seeing it. Use "..." for saying
    things and langcode"..." without spaces to say something in
    a different language.

    """

    aliases = [":"]

        "Perform the emote."
            self.caller.msg("What do you want to do?")
        else:
            # we also include ourselves here.
            emote = self.args
            targets = self.caller.location.contents
            if not emote.endswith((".", "?", "!", '"')):  # If emote is not punctuated or speech,
                emote += "."  # add a full-stop for good measure.
            send_emote(self.caller, targets, emote, anonymous_add="first")

class CmdSay(RPCommand):  # replaces standard say
    """

    Usage:

    """

    key = "say"

        "Run the say command"

            return

        # calling the speech modifying hook
        speech = caller.at_pre_say(self.args)
        targets = self.caller.location.contents
        send_emote(self.caller, targets, speech, msg_type="say", anonymous_add=None)

class CmdSdesc(RPCommand):  # set/look at own sdesc
    """
    Assign yourself a short description (sdesc).

    Usage:
      sdesc <short description>
      sdesc         - view current sdesc
      sdesc clear   - remove sdesc

    Assigns a short description to yourself.

    """

    key = "sdesc"

        "Assign the sdesc"
            sdesc = caller.sdesc.get()
            if not sdesc:
                caller.msg("You have no short description set.")
            else:
                caller.msg(f'Your short description is "{sdesc}".')
        elif self.args == "clear":
            ret = yield "Do you want to clear your sdesc? [Y]/n?"
            if ret.lower() in ("n", "no"):
                caller.msg("Aborted.")
            else:
                caller.sdesc.clear()
                caller.msg(f'Cleared sdesc, using name "{caller.key}".')
        else:
            # strip non-alfanum chars from end of sdesc
            sdesc = _RE_CHAREND.sub("", self.args)
            try:
                sdesc = caller.sdesc.add(sdesc)
            except SdescError as err:
                caller.msg(err)
                return
                caller.msg(f"Cannot set sdesc on {caller.key}.")
                return
            caller.msg(f"{caller.key}'s sdesc was set to '{sdesc}'.")

class CmdPose(RPCommand):  # set current pose and default pose
    """
    Set a static pose

    Usage:
        pose <pose>
        pose default <pose>
        pose reset
        pose obj = <pose>
        pose default obj = <pose>
        pose reset obj =

    Examples:
        pose leans against the tree
        pose is talking to the barkeep.
        pose box = is sitting on the floor.

    Set a static pose. This is the end of a full sentence that starts
    with your sdesc. If no full stop is given, it will be added
    automatically. The default pose is the pose you get when using
    pose reset. Note that you can use sdescs/recogs to reference
    people in your pose, but these always appear as that person's
    sdesc in the emote, regardless of who is seeing it.

    """

        """
        Extract the "default" alternative to the pose.
        """
        default = args.startswith("default")
        reset = args.startswith("reset")
        if default:
            args = re.sub(r"^default", "", args)
        if reset:
            args = re.sub(r"^reset", "", args)
        if "=" in args:
            target, args = [part.strip() for part in args.split("=", 1)]

        self.target = target
        self.reset = reset
        self.default = default

        "Create the pose"
        pose = self.args
        target = self.target
        if not pose and not self.reset:
            caller.msg("Usage: pose <pose-text> OR pose obj = <pose-text>")
            return

        if not pose.endswith((".", "?", "!", '"')):
            pose += "."
        if target:
            # affect something else
            target = caller.search(target)
                return
            if not target.access(caller, "edit"):
                caller.msg("You can't pose that.")
                return
        else:
            target = caller

        target_name = target.sdesc.get() if hasattr(target, "sdesc") else target.key
        if not target.attributes.has("pose"):
            caller.msg(f"{target_name} cannot be posed.")
            return

        # set the pose
        if self.reset:
            pose = target.db.pose_default
            target.db.pose = pose
        elif self.default:
            target.db.pose_default = pose
            caller.msg(f"Default pose is now '{target_name} {pose}'.")
            return
        else:
            # set the pose. We do one-time ref->sdesc mapping here.
            parsed, mapping = parse_sdescs_and_recogs(caller, caller.location.contents, pose)
            mapping = dict(
                (ref, obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key)
                for ref, obj in mapping.items()
            )
            pose = parsed.format_map(mapping)

            if len(target_name) + len(pose) > 60:
                caller.msg(f"'{pose}' is too long.")
                return

        caller.msg(f"Pose will read '{target_name} {pose}'.")

class CmdRecog(RPCommand):  # assign personal alias to object in room
    """
    Recognize another person in the same room.

    Usage:
      recog
      recog sdesc as alias
      forget alias

    Example:
        recog tall man as Griatch
        forget griatch

    This will assign a personal alias for a person, or forget said alias.
    Using the command without arguments will list all current recogs.

    """

    key = "recog"
    aliases = ["recognize", "forget"]

        "Parse for the sdesc as alias structure"
        self.sdesc, self.alias = "", ""
        if " as " in self.args:
            self.sdesc, self.alias = [part.strip() for part in self.args.split(" as ", 2)]
        elif self.args:
            # try to split by space instead
            try:
                self.sdesc, self.alias = [part.strip() for part in self.args.split(None, 1)]
                self.sdesc, self.alias = self.args.strip(), ""

        "Assign the recog"
        alias = self.alias.rstrip(".?!")
        sdesc = self.sdesc

        recog_mode = self.cmdstring != "forget" and alias and sdesc
        forget_mode = self.cmdstring == "forget" and sdesc
        list_mode = not self.args

        if not (recog_mode or forget_mode or list_mode):
            caller.msg("Usage: recog, recog <sdesc> as <alias> or forget <alias>")
            return

        if list_mode:
            # list all previously set recogs
            all_recogs = caller.recog.all()
            if not all_recogs:
                    "You recognize no-one. (Use 'recog <sdesc> as <alias>' to recognize people."
                )
            else:
                # note that we don't skip those failing enable_recog lock here,
                # because that would actually reveal more than we want.
                lst = "\n".join(
                    " {}  ({})".format(key, obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key)
                    for key, obj in all_recogs.items()
                )
                    "Currently recognized (use 'recog <sdesc> as <alias>' to add "
                    f"new and 'forget <alias>' to remove):\n{lst}"
                )
            return

        prefixed_sdesc = sdesc if sdesc.startswith(_PREFIX) else _PREFIX + sdesc
        candidates = caller.location.contents
        matches = parse_sdescs_and_recogs(caller, candidates, prefixed_sdesc, search_mode=True)
        nmatches = len(matches)
        # handle 0 and >1 matches
            caller.msg(_EMOTE_NOMATCH_ERROR.format(ref=sdesc))
        elif nmatches > 1:
            reflist = [
                "{num}{sep}{sdesc} ({recog}{key})".format(
                    sdesc=_RE_PREFIX.sub("", sdesc),
                    recog=caller.recog.get(obj) or "no recog",
                    key=f" ({caller.key})" if caller == obj else "",
                )
                for inum, obj in enumerate(matches)
            ]
            caller.msg(_EMOTE_MULTIMATCH_ERROR.format(ref=sdesc, reflist="\n    ".join(reflist)))

        else:
            # one single match
            obj = matches[0]
            if not obj.access(self.obj, "enable_recog", default=True):
                # don't apply recog if object doesn't allow it (e.g. by being masked).
                caller.msg("It's impossible to recognize them.")
                return
            if forget_mode:
                # remove existing recog
                caller.recog.remove(obj)
                    "You will now know them only as '{}'.".format(
                        obj.get_display_name(caller, noid=True)
                    )
                )
            else:
                # set recog
                sdesc = obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
                try:
                    alias = caller.recog.add(obj, alias)
                except RecogError as err:
                    caller.msg(err)
                    return
                caller.msg("You will now remember |w{}|n as |w{}|n.".format(sdesc, alias))

class CmdMask(RPCommand):
    """
    Wear a mask

    Usage:
        mask <new sdesc>
        unmask

    This will put on a mask to hide your identity. When wearing
    a mask, your sdesc will be replaced by the sdesc you pick and
    people's recognitions of you will be disabled.

    """

    key = "mask"
    aliases = ["unmask"]

        if self.cmdstring == "mask":
            # wear a mask
                caller.msg("Usage: (un)mask sdesc")
                return
            if caller.db.unmasked_sdesc:
                caller.msg("You are already wearing a mask.")
                return
            sdesc = f"{sdesc} |H[masked]|n"
            if len(sdesc) > 60:
                caller.msg("Your masked sdesc is too long.")
                return
            caller.db.unmasked_sdesc = caller.sdesc.get()
            caller.locks.add("enable_recog:false()")
            caller.sdesc.add(sdesc)
            caller.msg(f"You wear a mask as '{sdesc}'.")
        else:
            # unmask
            old_sdesc = caller.db.unmasked_sdesc
            if not old_sdesc:
                caller.msg("You are not wearing a mask.")
                return
            del caller.db.unmasked_sdesc
            caller.locks.remove("enable_recog")
            caller.sdesc.add(old_sdesc)
            caller.msg(f"You remove your mask and are again '{old_sdesc}'.")

class RPSystemCmdSet(CmdSet):
    """
    Mix-in for adding rp-commands to default cmdset.
    """

        self.add(CmdSay())
        self.add(CmdSdesc())
        self.add(CmdPose())
        self.add(CmdRecog())
        self.add(CmdMask())

# RP typeclasses

class ContribRPObject(DefaultObject):
    """
    This class is meant as a mix-in or parent for objects in an
    rp-heavy game. It implements the base functionality for poses.
    """

    def sdesc(self):
        return SdescHandler(self)

        """
        Called at initial creation.
        """

        # emoting/recog data
        self.db.pose = ""
        self.db.pose_default = "is here."
        self.db._sdesc = ""

        self,
        global_search=False,
        attribute_name=None,
        exact=False,
        candidates=None,
        use_dbref=None,
    ):
        """
        Returns an Object matching a search string/condition, taking
        sdescs into account.

        Perform a standard object search in the database, handling
        multiple results and lack thereof gracefully. By default, only
        objects in the current `location` of `self` or its inventory are searched for.

        Args:
            searchdata (str or obj): Primary search criterion. Will be matched
                against `object.key` (with `object.aliases` second) unless
                the keyword attribute_name specifies otherwise.
                **Special strings:**
                - `#<num>`: search by unique dbref. This is always
                   a global search.
                - `me,self`: self-reference to this object
                - `<num>-<string>` - can be used to differentiate
                   between multiple same-named matches
            global_search (bool): Search all objects globally. This is overruled
                by `location` keyword.
            use_nicks (bool): Use nickname-replace (nicktype "object") on `searchdata`.
            typeclass (str or Typeclass, or list of either): Limit search only
                to `Objects` with this typeclass. May be a list of typeclasses
                for a broader search.
            location (Object or list): Specify a location or multiple locations
                to search. Note that this is used to query the *contents* of a
                location and will not match for the location itself -
                if you want that, don't set this or use `candidates` to specify
                exactly which objects should be searched.
            attribute_name (str): Define which property to search. If set, no
                key+alias search will be performed. This can be used
                to search database fields (db_ will be automatically
                appended), and if that fails, it will try to return
                objects having Attributes with this name and value
                equal to searchdata. A special use is to search for
                "key" here if you want to do a key-search without
                including aliases.
            quiet (bool): don't display default error messages - this tells the
                search method that the user wants to handle all errors
                themselves. It also changes the return value type, see
                below.
            exact (bool): if unset (default) - prefers to match to beginning of
                string rather than not matching at all. If set, requires
                exact matching of entire string.
            candidates (list of objects): this is an optional custom list of objects
                to search (filter) between. It is ignored if `global_search`
                is given. If not set, this list will automatically be defined
                to include the location, the contents of location and the
                caller's contents (inventory).
            nofound_string (str):  optional custom string for not-found error message.
            multimatch_string (str): optional custom string for multimatch error header.
            use_dbref (bool or None): If None, only turn off use_dbref if we are of a lower
                permission than Builder. Otherwise, honor the True/False value.

        Returns:
            match (Object, None or list): will return an Object/None if `quiet=False`,
                otherwise it will return a list of 0, 1 or more matches.

        Notes:
            To find Accounts, use eg. `evennia.account_search`. If
            `quiet=False`, error messages will be handled by
            `settings.SEARCH_AT_RESULT` and echoed automatically (on
            error, return will be `None`). If `quiet=True`, the error
            messaging is assumed to be handled by the caller.

        """
        is_string = isinstance(searchdata, str)

        if is_string:
            # searchdata is a string; wrap some common self-references
            if searchdata.lower() in ("here",):
                return [self.location] if quiet else self.location
            if searchdata.lower() in ("me", "self"):
                return [self] if quiet else self

        if use_nicks:
            # do nick-replacement on search
            searchdata = self.nicks.nickreplace(
                searchdata, categories=("object", "account"), include_account=True
            )

        if global_search or (
            is_string
            and searchdata.startswith("#")
            and len(searchdata) > 1
            and searchdata[1:].isdigit()
        ):
            # only allow exact matching if searching the entire database
            # or unique #dbrefs
            exact = True
        elif candidates is None:
            # no custom candidates given - get them automatically
            if location:
                # location(s) were given
                candidates = []
                for obj in make_iter(location):
                    candidates.extend(obj.contents)
            else:
                # local search. Candidates are taken from
                # self.contents, self.location and
                # self.location.contents
                location = self.location
                candidates = self.contents
                    candidates = candidates + [location] + location.contents
                else:
                    # normally we don't need this since we are
                    # included in location.contents
                    candidates.append(self)

        # the sdesc-related substitution
        is_builder = self.locks.check_lockstring(self, "perm(Builder)")
        use_dbref = is_builder if use_dbref is None else use_dbref

        def search_obj(string):
            "helper wrapper for searching"
            return ObjectDB.objects.object_search(
                string,
                attribute_name=attribute_name,
                typeclass=typeclass,
                candidates=candidates,
                exact=exact,
                use_dbref=use_dbref,
            )

        if candidates:
            candidates = parse_sdescs_and_recogs(
                self, candidates, _PREFIX + searchdata, search_mode=True
            )
            for candidate in candidates:
                # we search by candidate keys here; this allows full error
                # management and use of all kwargs - we will use searchdata
                # in eventual error reporting later (not their keys). Doing
                # it like this e.g. allows for use of the typeclass kwarg
                # limiter.
                results.extend([obj for obj in search_obj(candidate.key) if obj not in results])

            if not results and is_builder:
                # builders get a chance to search only by key+alias
                results = search_obj(searchdata)
        else:
            # global searches / #drefs end up here. Global searches are
            # only done in code, so is controlled, #dbrefs are turned off
            # for non-Builders.
            results = search_obj(searchdata)

        if quiet:
            return results
        return _AT_SEARCH_RESULT(
            results,
            self,
            query=searchdata,
            nofound_string=nofound_string,
            multimatch_string=multimatch_string,
        )

    def get_posed_sdesc(self, sdesc, **kwargs):
        """
        Displays the object with its current pose string.

        Returns:
            pose (str): A string containing the object's sdesc and
                current or default pose.
        """

        # get the current pose, or default if no pose is set
        pose = self.db.pose or self.db.pose_default

        # return formatted string, or sdesc as fallback
        return f"{sdesc} {pose}" if pose else sdesc

        """

        Args:

            pose (bool): Include the pose (if available) in the return.
            ref (str): The reference marker found in string to replace.
                This is on the form #{num}{case}, like '#12^', where
                the number is a processing location in the string and the
                case symbol indicates the case of the original tag input
                - `t` - input was Titled, like /Tall
                - `^` - input was all uppercase, like /TALL
                - `v` - input was all lowercase, like /tall
                - `~` - input case should be kept, or was mixed-case
            noid (bool): Don't show DBREF even if viewer has control access.

        Returns:
            name (str): A string of the sdesc containing the name of the object,
                if this is defined. By default, included the DBREF if this user
                is privileged to control said object.

        """
        ref = kwargs.get("ref", "~")

        if looker == self:
            # always show your own key
            sdesc = self.key
        else:
            try:
                # get the sdesc looker should see
                sdesc = looker.get_sdesc(self, ref=ref)
                # use own sdesc as a fallback
                sdesc = self.sdesc.get()

        # add dbref is looker has control access and `noid` is not set
        if self.access(looker, access_type="control") and not kwargs.get("noid", False):
            sdesc = f"{sdesc}(#{self.id})"

        return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc

    def get_display_characters(self, looker, pose=True, **kwargs):
        """
        Get the ‘characters’ component of the object description. Called by return_appearance.
        """

            return (obj for obj in obj_list if obj != looker and obj.access(looker, "view"))

        characters = _filter_visible(self.contents_get(content_type="character"))
        character_names = "\n".join(
            char.get_display_name(looker, pose=pose, **kwargs) for char in characters
        )

        return f"\n{character_names}" if character_names else ""

    def get_display_things(self, looker, pose=True, **kwargs):
        """
        Get the 'things' component of the object description. Called by `return_appearance`.

        Args:
        Returns:
            str: The things display data.

        """
        if not pose:
            # if poses aren't included, we can use the core version instead
            return super().get_display_things(looker, **kwargs)

            return [obj for obj in obj_list if obj != looker and obj.access(looker, "view")]

        posed_things = defaultdict(list)
            pose = thing.db.pose or thing.db.pose_default
            if not pose:
                pose = ""
            posed_things[pose].append(thing)

        display_strings = []

        for pose, thinglist in posed_things.items():
            grouped_things = defaultdict(list)
            for thing in thinglist:
                grouped_things[thing.get_display_name(looker, pose=False, **kwargs)].append(thing)

            thing_names = []
            for thingname, samethings in sorted(grouped_things.items()):
                nthings = len(samethings)
                thing = samethings[0]
                singular, plural = thing.get_numbered_name(nthings, looker, key=thingname)
                thing_names.append(singular if nthings == 1 else plural)
            thing_names = iter_to_str(thing_names)

            if pose:
                pose = _INFLECT.plural(pose) if nthings != 1 else pose
            grouped_names = f"{thing_names} {pose}"
            grouped_names = grouped_names[0].upper() + grouped_names[1:]
            display_strings.append(grouped_names)

        if not display_strings:
            return ""

        return "\n" + "\n".join(display_strings)

class ContribRPRoom(ContribRPObject):
    """
    Dummy inheritance for rooms.
    """

    pass

class ContribRPCharacter(DefaultCharacter, ContribRPObject):
    """
    This is a character class that has poses, sdesc and recog.
    """

    def recog(self):
        return RecogHandler(self)

        """

        Args:

        Returns:

        Notes:
            The RPCharacter version adds additional processing to sdescs to make
            characters stand out from other objects.

        """

            # process your key as recog since you recognize yourself
            sdesc = self.process_recog(self.key, self)
        else:
            try:
                # get the sdesc looker should see, with formatting
                sdesc = looker.get_sdesc(self, process=True, ref=ref)

        """
        """

        self.db._recog_ref2recog = {}
        self.db._recog_obj2recog = {}

        self.cmdset.add(RPSystemCmdSet, persistent=True)
        # initializing sdesc
        self.sdesc.add("A normal person")

    def at_pre_say(self, message, **kwargs):
        """
        Called before the object says or whispers anything, return modified message.

        Args:
            message (str): The suggested say/whisper text spoken by self.
            whisper (bool): If True, this is a whisper rather than a say.

        """
        if kwargs.get("whisper"):
            return f'/Me whispers "{message}"'
        return f'/Me says, "{message}"'

    def get_sdesc(self, obj, process=False, **kwargs):
        """
        Single method to handle getting recogs with sdesc fallback in an
        aware manner, to allow separate processing of recogs from sdescs.
        Gets the sdesc or recog for obj from the view of self.

        Args:
            obj (Object): the object whose sdesc or recog is being gotten
            process (bool): If True, the sdesc/recog is run through the
                appropriate process method for self - .process_sdesc or
                .process_recog
        """
        # always see own key
        if obj == self:
            recog = self.key
        else:
            # first check if we have a recog for this object
            recog = self.recog.get(obj)
            # set sdesc to recog, using sdesc as a fallback, or the object's key if no sdesc
            sdesc = recog or (hasattr(obj, "sdesc") and obj.sdesc.get()) or obj.key

        if process:
            # process the sdesc as a recog if a recog was found, else as an sdesc
            sdesc = (self.process_recog if recog else self.process_sdesc)(sdesc, obj, **kwargs)

    def process_sdesc(self, sdesc, obj, **kwargs):
        """
        Allows to customize how your sdesc is displayed (primarily by
        changing colors).

        Args:
            sdesc (str): The sdesc to display.
            obj (Object): The object to which the adjoining sdesc
                belongs. If this object is equal to yourself, then
                you are viewing yourself (and sdesc is your key).
                This is not used by default.

        Kwargs:

        Returns:
            sdesc (str): The processed sdesc ready
                for display.

        """
        if not sdesc:
            return ""

        ref = kwargs.get("ref", "~")  # ~ to keep sdesc unchanged
        if "t" in ref:
            # we only want to capitalize the first letter if there are many words
            sdesc = sdesc.lower()
            sdesc = sdesc[0].upper() + sdesc[1:] if len(sdesc) > 1 else sdesc.upper()
        elif "^" in ref:
            sdesc = sdesc.upper()
        elif "v" in ref:
        return f"|b{sdesc}|n"

    def process_recog(self, recog, obj, **kwargs):
        """
        Allows to customize how a recog string is displayed.

        Args:
            recog (str): The recog string. It has already been
                translated from the original sdesc at this point.
            obj (Object): The object the recog:ed string belongs to.

        Returns:
            recog (str): The modified recog string.

        """
        if not recog:
            return ""

        return f"|m{recog}|n"

    def process_language(self, text, speaker, language, **kwargs):
        """
        Allows to process the spoken text, for example
        by obfuscating language based on your and the
        speaker's language skills. Also a good place to
        put coloring.

        Args:
            text (str): The text to process.
            speaker (Object): The object delivering the text.
            language (str): An identifier string for the language.

        Return:
            text (str): The optionally processed text.

        Notes:
            This is designed to work together with a string obfuscator
            such as the `obfuscate_language` or `obfuscate_whisper` in
            the evennia.contrib.rpg.rplanguage module.

        """
        return "{label}|w{text}|n".format(label=f"|W({language})" if language else "", text=text)

"""
Tests for RP system

"""

from . import rplanguage, rpsystem

mtrans = {"testing": "1", "is": "2", "a": "3", "human": "4"}
atrans = ["An", "automated", "advantageous", "repeatable", "faster"]

text = (
    "Automated testing is advantageous for a number of reasons: "
    "tests may be executed Continuously without the need for human "
    "intervention, They are easily repeatable, and often faster."
)

class TestLanguage(BaseEvenniaTest):
        rplanguage.add_language(
            key="testlang",
            word_length_variance=1,
            noun_prefix="bara",
            noun_postfix="'y",
            manual_translations=mtrans,
            auto_translations=atrans,
            force=True,
        )
            key="binary",
            phonemes="oo ii a ck w b d t",
            grammar="cvvv cvv cvvcv cvvcvv cvvvc cvvvcvv cvvc",
            noun_prefix="beep-",
            word_length_variance=4,
        )

        rplanguage._LANGUAGE_HANDLER.delete()
        rplanguage._LANGUAGE_HANDLER = None

    def test_obfuscate_language(self):
        result0 = rplanguage.obfuscate_language(text, level=0.0, language="testlang")
        self.assertEqual(result0, text)
        result1 = rplanguage.obfuscate_language(text, level=1.0, language="testlang")
        result2 = rplanguage.obfuscate_language(text, level=1.0, language="testlang")
        result3 = rplanguage.obfuscate_language(text, level=1.0, language="binary")

        self.assertNotEqual(result1, text)
        self.assertNotEqual(result3, text)
        result1, result2 = result1.split(), result2.split()
        self.assertEqual(result1[:4], result2[:4])
        self.assertEqual(result1[1], "1")
        self.assertEqual(result1[2], "2")
        self.assertEqual(result2[-1], result2[-1])

    def test_faulty_language(self):
        self.assertRaises(
            rplanguage.LanguageError,
            rplanguage.add_language,
            key="binary2",
            phonemes="w b d t oe ee, oo e o a wh dw bw",  # erroneous comma
            grammar="cvvv cvv cvvcv cvvcvvo cvvvc cvvvcvv cvvc c v cc vv ccvvc ccvvccvv ",
            vowels="oea",
        )

    def test_available_languages(self):
        self.assertEqual(list(sorted(rplanguage.available_languages())), ["binary", "testlang"])

    def test_obfuscate_whisper(self):
        self.assertEqual(rplanguage.obfuscate_whisper(text, level=0.0), text)
        assert rplanguage.obfuscate_whisper(text, level=0.1).startswith(
            "-utom-t-d t-sting is -dv-nt-g-ous for - numb-r of r--sons: t-sts m-y b- -x-cut-d"
            " Continuously"
        )
        assert rplanguage.obfuscate_whisper(text, level=0.5).startswith(
            "--------- --s---- -s -----------s f-- - ------ -f ---s--s: --s-s "
        )
        self.assertEqual(rplanguage.obfuscate_whisper(text, level=1.0), "...")

# Testing of emoting / sdesc / recog system

sdesc0 = "A nice sender of emotes"
sdesc1 = "The first receiver of emotes."
sdesc2 = "Another nice colliding sdesc-guy for tests"
recog01 = "Mr Receiver"
recog02 = "Mr Receiver2"
recog10 = "Mr Sender"
emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."'
fallback_emote = "/Me is distracted from /first by /nomatch."
case_emote = "/Me looks at /first. Then, /me looks at /FIRST, /First and /Colliding twice."
poss_emote = "/Me frowns at /first for trying to steal /me's test."

class TestRPSystem(BaseEvenniaTest):

        self.room = create_object(rpsystem.ContribRPRoom, key="Location")
        self.speaker = create_object(rpsystem.ContribRPCharacter, key="Sender", location=self.room)
        self.receiver1 = create_object(
            rpsystem.ContribRPCharacter, key="Receiver1", location=self.room
        )
        self.receiver2 = create_object(
            rpsystem.ContribRPCharacter, key="Receiver2", location=self.room
        )

    def test_posed_contents(self):
        self.obj1 = create_object(rpsystem.ContribRPObject, key="thing", location=self.room)
        self.obj2 = create_object(rpsystem.ContribRPObject, key="thing", location=self.room)
        self.obj3 = create_object(rpsystem.ContribRPObject, key="object", location=self.room)
        room_display = self.room.return_appearance(self.speaker)
        self.assertIn("An object and two things are here.", room_display)
        self.obj3.db.pose = "is on the ground."
        self.assertIn("Two things are here.", room_display)
        self.assertIn("An object is on the ground.", room_display)

    def test_sdesc_handler(self):
        self.speaker.sdesc.add(sdesc0)
        self.assertEqual(self.speaker.sdesc.get(), sdesc0)
        self.speaker.sdesc.add("This is {#324} ignored")
        self.assertEqual(self.speaker.sdesc.get(), "This is 324 ignored")

    def test_recog_handler(self):
        self.receiver1.sdesc.add(sdesc1)
        self.speaker.recog.add(self.receiver1, recog01)
        self.speaker.recog.add(self.receiver2, recog02)
        self.assertEqual(self.speaker.recog.get(self.receiver1), recog01)
        self.assertEqual(self.speaker.recog.get(self.receiver2), recog02)
        self.speaker.recog.remove(self.receiver1)
        self.assertEqual(self.speaker.recog.get(self.receiver1), None)

        self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2})

    def test_parse_language(self):
            rpsystem.parse_language(self.speaker, emote),
            (
                "With a flair, /me looks at /first and /colliding sdesc-guy. She says {##0}",
                {"##0": (None, '"This is a test."')},
            ),
        )

    def test_parse_sdescs_and_recogs(self):
        speaker = self.speaker
        speaker.sdesc.add(sdesc0)
        self.receiver2.sdesc.add(sdesc2)
        id0 = f"#{speaker.id}"
        id1 = f"#{self.receiver1.id}"
        id2 = f"#{self.receiver2.id}"
        candidates = (self.receiver1, self.receiver2)
        result = (
            "With a flair, {"
            + id0
            + "} looks at {"
            + id1
            + "} and {"
            + id2
            + '}. She says "This is a test."',
            {
                id2: self.receiver2,
                id1: self.receiver1,
                id0: speaker,
            },
        )
            rpsystem.parse_sdescs_and_recogs(speaker, candidates, emote, case_sensitive=False),
            result,
        )
            result,
        )

    def test_possessive_selfref(self):
        result = (
            "{" + id0 + "} frowns at {" + id1 + "} for trying to steal {" + id0 + "}'s test.",
            {
            },
        )
            rpsystem.parse_sdescs_and_recogs(speaker, candidates, poss_emote, case_sensitive=False),
            result,
        )

    def test_get_sdesc(self):
        looker = self.speaker  # Sender
        target = self.receiver1  # Receiver1
        looker.sdesc.add(sdesc0)  # A nice sender of emotes
        target.sdesc.add(sdesc1)  # The first receiver of emotes.

        # sdesc with no processing
        self.assertEqual(looker.get_sdesc(target), "The first receiver of emotes.")
        # sdesc with processing
            looker.get_sdesc(target, process=True), "|bThe first receiver of emotes.|n"
        )

        looker.recog.add(target, recog01)  # Mr Receiver

        # recog with no processing
        self.assertEqual(looker.get_sdesc(target), "Mr Receiver")
        # recog with processing
        self.assertEqual(looker.get_sdesc(target, process=True), "|mMr Receiver|n")

    def test_send_emote(self):
        receiver1 = self.receiver1
        receiver2 = self.receiver2
        receivers = [speaker, receiver1, receiver2]
        receiver1.sdesc.add(sdesc1)
        receiver2.sdesc.add(sdesc2)
        speaker.msg = lambda text, **kwargs: setattr(self, "out0", text)
        receiver1.msg = lambda text, **kwargs: setattr(self, "out1", text)
        receiver2.msg = lambda text, **kwargs: setattr(self, "out2", text)
        rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False)
            self.out0[0],
            "With a flair, |mSender|n looks at |bThe first receiver of emotes.|n "
            'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
        )
            self.out1[0],
            "With a flair, |bA nice sender of emotes|n looks at |mReceiver1|n and "
            '|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
        )
            self.out2[0],
            "With a flair, |bA nice sender of emotes|n looks at |bThe first "
            'receiver of emotes.|n and |mReceiver2|n. She says |w"This is a test."|n',
        )

    def test_send_emote_fallback(self):
        rpsystem.send_emote(speaker, receivers, fallback_emote, fallback="something")
            "|mSender|n is distracted from |bthe first receiver of emotes.|n by something.",
        )
            "|bA nice sender of emotes|n is distracted from |mReceiver1|n by something.",
        )
            "|bA nice sender of emotes|n is distracted from |bthe first receiver of emotes.|n by"
            " something.",
        )

    def test_send_case_sensitive_emote(self):
        """Test new case-sensitive rp-parsing"""
        rpsystem.send_emote(speaker, receivers, case_emote)
            "|mSender|n looks at |bthe first receiver of emotes.|n. Then, |mSender|n "
            "looks at |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n "
            "and |bAnother nice colliding sdesc-guy for tests|n twice.",
        )
            "|bA nice sender of emotes|n looks at |mReceiver1|n. Then, "
            "|ba nice sender of emotes|n looks at |mReceiver1|n, |mReceiver1|n "
        )
            "|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n. "
            "Then, |ba nice sender of emotes|n looks at |bTHE FIRST RECEIVER OF EMOTES.|n, "
            "|bThe first receiver of emotes.|n and |mReceiver2|n twice.",
        )

    def test_rpsearch(self):
        self.speaker.msg = lambda text, **kwargs: setattr(self, "out0", text)
        self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1)
        self.assertEqual(self.speaker.search("colliding"), self.receiver2)

class TestRPSystemCommands(BaseEvenniaCommandTest):
        self.char1.swap_typeclass(rpsystem.ContribRPCharacter)
        self.char2.swap_typeclass(rpsystem.ContribRPCharacter)

    def test_commands(self):
            rpsystem.CmdSdesc(), "Foobar Character", "Char's sdesc was set to 'Foobar Character'."
        )
            rpsystem.CmdSdesc(),
            "BarFoo Character",
            "Char2's sdesc was set to 'BarFoo Character'.",
        )

        self.call(rpsystem.CmdSdesc(), "", 'Your short description is "Foobar Character".')

        self.call(rpsystem.CmdSay(), "Hello!", 'Char says, "Hello!"')
        self.call(rpsystem.CmdEmote(), "/me smiles to /BarFoo.", "Char smiles to BarFoo Character")

        # escape urls in say
        self.call(rpsystem.CmdSay(), "https://evennia.com", 'Char says, "https://evennia.com"')
        self.call(rpsystem.CmdSay(), "http://evennia.com", 'Char says, "http://evennia.com"')

            rpsystem.CmdPose(),
            "stands by the bar",
            "Pose will read 'Foobar Character stands by the bar.'.",
        )
            rpsystem.CmdRecog(),
            "barfoo as friend",
            "You will now remember BarFoo Character as friend.",
        )
            "",
            "Currently recognized (use 'recog <sdesc> as <alias>' to add new "
            "and 'forget <alias>' to remove):\n friend  (BarFoo Character)",
        )
            "friend",
            "You will now know them only as 'BarFoo Character'",
            cmdstring="forget",
        )

        self.call(rpsystem.CmdSdesc(), "clear", 'Cleared sdesc, using name "Char".', inputs=["Y"])

#!/usr/bin/env python

"""
Unit test module for Trait classes.

"""

from evennia.utils.test_resources import BaseEvenniaTestCase, EvenniaTest

from . import traits

class _MockObj:
        self.attributes = MagicMock()
        self.attributes.get = self.get
        self.attributes.add = self.add
        self.dbstore = {}
        self.category = "traits"

    def get(self, key, category=None):
        assert category == self.category
        return self.dbstore.get(key)

    def add(self, key, value, category=None):
        self.dbstore[key] = value

# we want to test the base traits too
_TEST_TRAIT_CLASS_PATHS = [
    "evennia.contrib.rpg.traits.Trait",
    "evennia.contrib.rpg.traits.StaticTrait",
    "evennia.contrib.rpg.traits.CounterTrait",
    "evennia.contrib.rpg.traits.GaugeTrait",
]

class _TraitHandlerBase(BaseEvenniaTestCase):
    "Base for trait tests"

    @patch("evennia.contrib.rpg.traits.traits._TRAIT_CLASS_PATHS", new=_TEST_TRAIT_CLASS_PATHS)
        self.obj = _MockObj()
        self.traithandler = traits.TraitHandler(self.obj)
        self.obj.traits = self.traithandler

    def _get_dbstore(self, key):
        return self.obj.dbstore["traits"][key]

class TraitHandlerTest(_TraitHandlerBase):
    """Testing for TraitHandler"""

        self.traithandler.add("test1", name="Test1", trait_type="trait")
        self.traithandler.add(
            "test2",
            name="Test2",
            trait_type="trait",
            value=["foo", {"1": [1, 2, 3]}, 4],
        )

    def test_add_trait(self):
            self._get_dbstore("test1"),
            {
                "name": "Test1",
                "trait_type": "trait",
                "value": None,
            },
        )
            self._get_dbstore("test2"),
            {
                "name": "Test2",
                "value": ["foo", {"1": [1, 2, 3]}, 4],
            },
        )
        self.assertEqual(len(self.traithandler), 2)

    def test_cache(self):
        """
        Cache should not be set until first get
        """
        self.assertEqual(len(self.traithandler._cache), 0)
        self.traithandler.all()  # does not affect cache
        self.traithandler.test1
        self.assertEqual(len(self.traithandler._cache), 1)
        self.traithandler.test2
        self.assertEqual(len(self.traithandler._cache), 2)

    def test_setting(self):
        "Don't allow setting stuff on traithandler"
        with self.assertRaises(traits.TraitException):
            self.traithandler.foo = "bar"
            self.traithandler["foo"] = "bar"
            self.traithandler.test1 = "foo"

    def test_getting(self):
        "Test we are getting data from the dbstore"
            self.traithandler.test1._data, {"name": "Test1", "trait_type": "trait", "value": None}
        )
        self.assertEqual(self.traithandler._cache, Something)
            self.traithandler.test2._data,
            {"name": "Test2", "trait_type": "trait", "value": ["foo", {"1": [1, 2, 3]}, 4]},
        )
        self.assertFalse(self.traithandler.get("foo"))
        self.assertFalse(self.traithandler.bar)

    def test_all(self):
        "Test all method"
        self.assertEqual(self.traithandler.all(), ["test1", "test2"])

        "Test remove method"
        self.traithandler.remove("test2")
        self.assertEqual(len(self.traithandler), 1)
        self.assertTrue(bool(self.traithandler.get("test1")))  # this populates cache
            self.traithandler.remove("foo")

    def test_clear(self):
        "Test clear method"
        self.traithandler.clear()
        self.assertEqual(len(self.traithandler), 0)

    def test_trait_db_connection(self):
        "Test that updating a trait property actually updates value in db"
        trait = self.traithandler.test1
        self.assertEqual(trait.value, None)
        trait.value = 10
        self.assertEqual(trait.value, 10)
        self.assertEqual(self.obj.attributes.get("traits", category="traits")["test1"]["value"], 10)
        trait.value = 20
        self.assertEqual(trait.value, 20)
        self.assertEqual(self.obj.attributes.get("traits", category="traits")["test1"]["value"], 20)
        del trait.value
            self.obj.attributes.get("traits", category="traits")["test1"]["value"], None
        )

class TestTrait(_TraitHandlerBase):
    """
    Test the base Trait class
    """

            "test1",
            name="Test1",
            value="value",
            extra_val1="xvalue1",
            extra_val2="xvalue2",
        )
        self.trait = self.traithandler.get("test1")

    def test_init(self):
            self.trait._data,
            {
                "value": "value",
                "extra_val1": "xvalue1",
                "extra_val2": "xvalue2",
            },
        )

    def test_validate_input__valid(self):
        """Test valid validation input"""
        # all data supplied, and extras
        dat = {"name": "Test", "trait_type": "trait", "value": 10, "extra_val": 1000}
        expected = copy(dat)  # we must break link or return === dat always
        self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat))

        # don't supply value, should get default
        dat = {
            "name": "Test",
            "trait_type": "trait",
            # missing value
            "extra_val": 1000,
        }
        expected = copy(dat)
        expected["value"] = traits.Trait.default_keys["value"]

        # make sure extra values are cleaned if trait accepts no extras
        dat = {
            "value": 10,
            "extra_val1": 1000,
            "extra_val2": "xvalue",
        }
        expected.pop("extra_val1")
        expected.pop("extra_val2")
        with patch.object(traits.Trait, "allow_extra_properties", False):
            self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat))

    def test_validate_input__fail(self):
        """Test failing validation"""
        dat = {
            # missing name
        }
            traits.Trait.validate_input(traits.Trait, dat)

        # make value a required key
        mock_default_keys = {"value": traits.MandatoryTraitKey}
        with patch.object(traits.Trait, "default_keys", mock_default_keys):
            dat = {
                "name": "Trait",
                # missing value, now mandatory
                "extra_val": 1000,
            }
            with self.assertRaises(traits.TraitException):
                traits.Trait.validate_input(traits.Trait, dat)

    def test_trait_getset(self):
        """Get-set-del operations on trait"""
        self.assertEqual(self.trait.name, "Test1")
        self.assertEqual(self.trait["name"], "Test1")
        self.assertEqual(self.trait.value, "value")
        self.assertEqual(self.trait["value"], "value")
        self.assertEqual(self.trait.extra_val1, "xvalue1")
        self.assertEqual(self.trait["extra_val2"], "xvalue2")

        self.trait.value = 20
        self.assertEqual(self.trait["value"], 20)
        self.trait["value"] = 20
        self.assertEqual(self.trait.value, 20)
        self.trait.extra_val1 = 100
        self.assertEqual(self.trait.extra_val1, 100)
        # additional properties
        self.trait.foo = "bar"
        self.assertEqual(self.trait.foo, "bar")

        del self.trait.foo
        with self.assertRaises(KeyError):
            self.trait["foo"]
        with self.assertRaises(AttributeError):
            self.trait.foo
        del self.trait.extra_val1
            self.trait.extra_val1
        del self.trait.value
        # fall back to default
        self.assertTrue(self.trait.value == traits.Trait.default_keys["value"])

    def test_repr(self):
        self.assertEqual(repr(self.trait), Something)
        self.assertEqual(str(self.trait), Something)

class TestTraitStatic(_TraitHandlerBase):
    """
    Test for static Traits
    """

            "test1",
            trait_type="static",
            base=1,
            mod=2,
            mult=1.0,
        )

    def _get_values(self):
        return self.trait.base, self.trait.mod, self.trait.mult, self.trait.value

            {
                "trait_type": "static",
                "base": 1,
                "mod": 2,
                "mult": 1.0,
            },
        )

    def test_value(self):
        """value is (base + mod) * mult"""
        self.assertEqual(self._get_values(), (1, 2, 1.0, 3))
        self.trait.base += 4
        self.assertEqual(self._get_values(), (5, 2, 1.0, 7))
        self.trait.mod -= 1
        self.assertEqual(self._get_values(), (5, 1, 1.0, 6))
        self.trait.mult += 1.0
        self.assertEqual(self._get_values(), (5, 1, 2.0, 12))
        self.trait.mult = 0.75
        self.assertEqual(self._get_values(), (5, 1, 0.75, 4.5))

    def test_delete(self):
        """Deleting resets to default."""
        self.trait.mult = 2.0
        del self.trait.base
        self.assertEqual(self._get_values(), (0, 2, 2.0, 4))
        del self.trait.mult
        self.assertEqual(self._get_values(), (0, 2, 1.0, 2))
        del self.trait.mod
        self.assertEqual(self._get_values(), (0, 0, 1.0, 0))

class TestTraitCounter(_TraitHandlerBase):
    """
    Test for counter- Traits
    """

            "test1",
            trait_type="counter",
            base=1,
            mod=2,
            mult=1.0,
            min=0,
            max=10,
            descs={
                0: "range0",
                2: "range1",
                5: "range2",
                7: "range3",
            },
        )

        """Get (base, mod, mult, value, min, max)."""
        return (
            self.trait.base,
            self.trait.mod,
            self.trait.mult,
            self.trait.value,
            self.trait.min,
            self.trait.max,
        )

            {
                "trait_type": "counter",
                "base": 1,
                "mod": 2,
                "min": 0,
                "max": 10,
                "descs": {
                    0: "range0",
                    2: "range1",
                    5: "range2",
                    7: "range3",
                },
                "rate": 0,
                "ratetarget": None,
                "last_update": None,
            },
        )

        """value is (current + mod) * mult, where current defaults to base"""
        self.assertEqual(self._get_values(), (1, 2, 1.0, 3, 0, 10))
        self.assertEqual(self._get_values(), (5, 2, 1.0, 7, 0, 10))
        self.assertEqual(self._get_values(), (5, 1, 1.0, 6, 0, 10))
        self.assertEqual(self._get_values(), (5, 1, 2.0, 10, 0, 10))

    def test_boundaries__minmax(self):
        """Test range"""
        # should not exceed min/max values
        self.trait.base += 20
        self.assertEqual(self._get_values(), (8, 2, 1.0, 10, 0, 10))
        self.trait.base = 100
        self.trait.base -= 40
        self.assertEqual(self._get_values(), (-2, 2, 1.0, 0, 0, 10))
        self.trait.base = -100

    def test_boundaries__bigmod(self):
        """add a big mod"""
        self.trait.base = 5
        self.trait.mod = 100
        self.assertEqual(self._get_values(), (5, 5, 1.0, 10, 0, 10))
        self.trait.mod = -100
        self.assertEqual(self._get_values(), (5, -5, 1.0, 0, 0, 10))

    def test_boundaries__change_boundaries(self):
        """Change boundaries after base/mod change"""
        self.trait.min = -20
        self.assertEqual(self._get_values(), (5, -5, 1.0, 0, -20, 10))
        self.trait.mod -= 100
        self.assertEqual(self._get_values(), (5, -25, 1.0, -20, -20, 10))
        self.trait.max = 20
        self.assertEqual(self._get_values(), (5, 5, 1.0, 10, -20, 20))
        self.assertEqual(self._get_values(), (5, 15, 1.0, 20, -20, 20))

    def test_boundaries__disable(self):
        """Disable and re-enable boundaries"""
        del self.trait.max
        self.assertEqual(self.trait.max, None)
        del self.trait.min
        self.assertEqual(self.trait.min, None)
        self.assertEqual(self._get_values(), (100, 5, 1.0, 105, None, None))
        self.trait.base = -200
        self.assertEqual(self._get_values(), (-200, 5, 1.0, -195, None, None))

        # re-activate boundaries
        self.trait.max = 15
        self.trait.min = 10  # his is blocked since base+mod is lower
        self.assertEqual(self._get_values(), (-200, 5, 1.0, -195, -195, 15))

    def test_boundaries__inverse(self):
        """Set inverse boundaries - limited by base"""
        self.trait.mod = 0
        self.assertEqual(self._get_values(), (1, 0, 1.0, 1, 0, 10))
        self.trait.min = 20  # will be set to base
        self.assertEqual(self._get_values(), (1, 0, 1.0, 1, 1, 10))
        self.trait.max = -20
        self.assertEqual(self._get_values(), (1, 0, 1.0, 1, 1, 1))

    def test_current(self):
        """Modifying current value"""
        self.trait.current = 5
        self.assertEqual(self._get_values(), (1, 2, 1.0, 7, 0, 10))
        self.trait.current = 10
        self.assertEqual(self._get_values(), (1, 2, 1.0, 10, 0, 10))
        self.trait.current = 12
        self.trait.current = -1
        self.assertEqual(self._get_values(), (1, 2, 1.0, 2, 0, 10))
        self.trait.current -= 10

        self.assertEqual(self._get_values(), (0, 2, 1.0, 2, 0, 10))
        self.assertEqual(self._get_values(), (0, 0, 1.0, 0, 0, 10))
        self.assertEqual(self._get_values(), (0, 0, 1.0, 0, None, None))

    def test_percentage(self):
        """Test percentage calculation"""
        self.trait.base = 8
        self.trait.mod = 2
        self.trait.mult = 1.0
        self.trait.min = 0
        self.trait.max = 10
        self.assertEqual(self.trait.percent(), "100.0%")
        self.trait.current = 3
        self.assertEqual(self.trait.percent(), "50.0%")
        self.trait.current = 1
        self.assertEqual(self.trait.percent(), "30.0%")
        # have to lower this since max cannot be lowered below base+mod
        self.trait.mod = 1
        self.trait.current = 2
        self.trait.max -= 1
        self.assertEqual(self.trait.percent(), "33.3%")
        # open boundary

    def test_descs(self):
        """Test descriptions"""
        self.trait.min = -5
        self.assertEqual(self._get_values(), (1, 0, 1.0, 1, -5, 10))
        self.trait.current = -2
        self.assertEqual(self.trait.desc(), "range0")
        self.trait.current = 0
        self.assertEqual(self.trait.desc(), "range1")
        self.assertEqual(self.trait.desc(), "range2")
        self.trait.current = 9
        self.assertEqual(self.trait.desc(), "range3")
        self.trait.current = 100

class TestTraitCounterTimed(_TraitHandlerBase):
    """
    Test for trait with timer component
    """

    @patch("evennia.contrib.rpg.traits.traits.time", new=MagicMock(return_value=1000))
            "test1",
            base=1,
            mod=2,
            mult=1.0,
            min=0,
            max=100,
            descs={
            },
            rate=1,
            ratetarget=None,
        )

    def _get_timer_data(self):
        return (
            self.trait.current,
            self.trait.rate,
            self.trait._data["last_update"],
            self.trait.ratetarget,
        )

    @patch("evennia.contrib.rpg.traits.traits.time")
    def test_timer_rate(self, mock_time):
        """Test time stepping"""
        mock_time.return_value = 1000
        self.assertEqual(self._get_timer_data(), (3, 1, 1, 1000, None))
        mock_time.return_value = 1001
        self.assertEqual(self._get_timer_data(), (4, 2, 1, 1001, None))
        mock_time.return_value = 1096
        self.assertEqual(self._get_timer_data(), (99, 97, 1, 1096, None))
        # hit maximum boundary
        mock_time.return_value = 1120
        self.assertEqual(self._get_timer_data(), (100, 98, 1, None, None))
        mock_time.return_value = 1200
        # drop current
        self.trait.current = 50
        self.assertEqual(self._get_timer_data(), (52, 50, 1, 1200, None))
        # set a new rate
        self.trait.rate = 2
        mock_time.return_value = 1210
        self.assertEqual(self._get_timer_data(), (72, 70, 2, 1210, None))
        self.trait.rate = -10
        mock_time.return_value = 1214
        self.assertEqual(self._get_timer_data(), (32, 30, -10, 1214, None))
        mock_time.return_value = 1218
        self.assertEqual(self._get_timer_data(), (0, -2, -10, None, None))

    def test_timer_ratetarget(self, mock_time):
        """test ratetarget"""
        self.trait.ratetarget = 60
        self.assertEqual(self._get_timer_data(), (3, 1, 1, 1000, 60))
        mock_time.return_value = 1056
        self.assertEqual(self._get_timer_data(), (59, 57, 1, 1056, 60))
        mock_time.return_value = 1057
        self.assertEqual(self._get_timer_data(), (60, 58, 1, None, 60))
        mock_time.return_value = 1060
        self.trait.ratetarget = 70
        mock_time.return_value = 1066
        self.assertEqual(self._get_timer_data(), (66, 64, 1, 1066, 70))
        mock_time.return_value = 1070
        self.assertEqual(self._get_timer_data(), (70, 68, 1, None, 70))

class TestTraitGauge(_TraitHandlerBase):
            "test1",
            trait_type="gauge",
            base=8,  # max = (base + mod) * mult
            mod=2,
            mult=1.0,
            descs={
            },
        )

        return (
        )

            {
                "trait_type": "gauge",
                "base": 8,
                "mod": 2,
                "min": 0,
                "descs": {
                },
                "rate": 0,
            },
        )

        """value is current, where current defaults to base + mod"""
        # current unset - follows base + mod
        self.assertEqual(self._get_values(), (12, 2, 1.0, 14, 0, 14))
        self.assertEqual(self._get_values(), (12, 1, 1.0, 13, 0, 13))
        self.assertEqual(self._get_values(), (12, 1, 2.0, 26, 0, 26))
        # set current, decouple from base + mod
        self.assertEqual(self._get_values(), (12, 1, 2.0, 5, 0, 26))
        self.trait.mod += 1
        self.trait.base -= 4
        self.trait.mult -= 1.0
        self.assertEqual(self._get_values(), (8, 2, 1.0, 5, 0, 10))
        self.trait.min = -100
        self.trait.base = -20
        self.assertEqual(self._get_values(), (-20, 2, 1.0, -18, -100, -18))

        # current unset - tied to base + mod
        self.assertEqual(self._get_values(), (28, 2, 1.0, 30, 0, 30))
        # set current - decouple from base + mod
        self.trait.current = 19
        self.assertEqual(self._get_values(), (28, 2, 1.0, 19, 0, 30))
        # test upper bound
        # with multiplier
        self.assertEqual(self._get_values(), (28, 2, 2.0, 30, 0, 60))
        self.assertEqual(self._get_values(), (28, 2, 2.0, 60, 0, 60))
        # min defaults to 0
        self.trait.current = -10
        self.assertEqual(self._get_values(), (28, 2, 1.0, 0, 0, 30))
        self.assertEqual(self._get_values(), (28, 2, 1.0, 0, -20, 30))
        self.assertEqual(self._get_values(), (28, 2, 1.0, -10, -20, 30))

        self.assertEqual(self._get_values(), (5, 100, 1.0, 105, 0, 105))
        # restricted by min
        self.assertEqual(self._get_values(), (5, -5, 1.0, 0, 0, 0))
        self.trait.min = -200
        self.assertEqual(self._get_values(), (5, -5, 1.0, 0, -200, 0))

        """Change boundaries after current change"""
        self.trait.current = 20
        self.trait.mod = 102
        self.assertEqual(self._get_values(), (8, 102, 1.0, 10, 0, 110))
        # raising min past current value will force it upwards
        self.trait.min = 20
        self.assertEqual(self._get_values(), (8, 102, 1.0, 20, 20, 110))

        """Disable and re-enable boundary"""
        self.trait.min = 1
        self.assertEqual(self._get_values(), (5, 2, 1.0, 7, 1, 7))
        self.assertEqual(self._get_values(), (5, 2, 1.0, 7, 0, 7))
        self.assertEqual(self._get_values(), (0, 0, 1.0, 0, 0, 0))
            del self.trait.max

        """Try to set reversed boundaries"""
        self.trait.base = -10  # limited by min
        self.trait.min = -10
        self.assertEqual(self._get_values(), (0, 0, 1.0, 0, -10, 0))
        self.trait.base = -10
        self.assertEqual(self._get_values(), (-10, 0, 1.0, -10, -10, -10))
        self.min = 0  # limited by base + mod

        self.trait.base = 10
        self.assertEqual(self._get_values(), (10, 2, 1.0, 5, 0, 12))
        self.assertEqual(self._get_values(), (10, 2, 1.0, 10, 0, 12))
        self.assertEqual(self._get_values(), (10, 2, 1.0, 12, 0, 12))
        self.assertEqual(self._get_values(), (10, 2, 1.0, 0, 0, 12))

        self.assertEqual(self._get_values(), (8, 0, 1.0, 8, 0, 8))
        self.assertEqual(self._get_values(), (0, 2, 1.0, 2, 0, 2))
        self.assertEqual(self._get_values(), (0, 2, 1.0, 2, -10, 2))

        self.assertEqual(self._get_values(), (8, 2, 1.0, 10, -5, 10))

class TestTraitGaugeTimed(_TraitHandlerBase):
    """
    """

            "test1",
            base=98,
            mod=2,
            min=0,
            descs={
            },
            rate=1,
        )

        return (
        )

        self.assertEqual(self._get_timer_data(), (1, 1, 1, 1000, None))
        self.assertEqual(self._get_timer_data(), (2, 2, 1, 1001, None))
        self.assertEqual(self._get_timer_data(), (97, 97, 1, 1096, None))
        self.assertEqual(self._get_timer_data(), (100, 100, 1, None, None))
        self.assertEqual(self._get_timer_data(), (50, 50, 1, 1200, None))
        self.assertEqual(self._get_timer_data(), (70, 70, 2, 1210, None))
        self.assertEqual(self._get_timer_data(), (30, 30, -10, 1214, None))
        self.assertEqual(self._get_timer_data(), (0, 0, -10, None, None))

        self.assertEqual(self._get_timer_data(), (1, 1, 1, 1000, 60))
        self.assertEqual(self._get_timer_data(), (57, 57, 1, 1056, 60))
        mock_time.return_value = 1059
        self.assertEqual(self._get_timer_data(), (60, 60, 1, None, 60))
        self.assertEqual(self._get_timer_data(), (66, 66, 1, 1066, 70))
        self.assertEqual(self._get_timer_data(), (70, 70, 1, None, 70))

class TestNumericTraitOperators(BaseEvenniaTestCase):
    """Test case for numeric magic method implementations."""

        # direct instantiation for testing only; use TraitHandler in production
        self.st = traits.Trait(
            {
                "name": "Strength",
                "value": 8,
            }
        )
        self.at = traits.Trait(
            {
                "name": "Attack",
                "value": 4,
            }
        )

        self.st, self.at = None, None

    def test_pos_shortcut(self):
        """overridden unary + operator returns `value` property"""
        self.assertIn(type(+self.st), (float, int))
        self.assertEqual(+self.st, self.st.value)
        self.assertEqual(+self.st, 8)

    def test_add_traits(self):
        """test addition of `Trait` objects"""
        # two Trait objects
        self.assertEqual(self.st + self.at, 12)
        # Trait and numeric
        self.assertEqual(self.st + 1, 9)
        self.assertEqual(1 + self.st, 9)

    def test_sub_traits(self):
        """test subtraction of `Trait` objects"""
        self.assertEqual(self.st - self.at, 4)
        self.assertEqual(self.st - 1, 7)
        self.assertEqual(10 - self.st, 2)

    def test_mul_traits(self):
        """test multiplication of `Trait` objects"""
        # between two Traits
        self.assertEqual(self.st * self.at, 32)
        # between Trait and numeric
        self.assertEqual(self.at * 4, 16)
        self.assertEqual(4 * self.at, 16)

    def test_floordiv(self):
        """test floor division of `Trait` objects"""
        self.assertEqual(self.st // self.at, 2)
        self.assertEqual(self.st // 2, 4)
        self.assertEqual(18 // self.st, 2)

    def test_comparisons_traits(self):
        """test equality comparison between `Trait` objects"""
        self.assertNotEqual(self.st, self.at)
        self.assertLess(self.at, self.st)
        self.assertLessEqual(self.at, self.st)
        self.assertGreater(self.st, self.at)
        self.assertGreaterEqual(self.st, self.at)

    def test_comparisons_numeric(self):
        """equality comparisons between `Trait` and numeric"""
        self.assertEqual(self.st, 8)
        self.assertEqual(8, self.st)
        self.assertNotEqual(self.st, 0)
        self.assertNotEqual(0, self.st)
        self.assertLess(self.st, 10)
        self.assertLess(0, self.st)
        self.assertLessEqual(self.st, 8)
        self.assertLessEqual(8, self.st)
        self.assertLessEqual(self.st, 10)
        self.assertLessEqual(0, self.st)
        self.assertGreater(self.st, 0)
        self.assertGreater(10, self.st)
        self.assertGreaterEqual(self.st, 8)
        self.assertGreaterEqual(8, self.st)
        self.assertGreaterEqual(self.st, 0)
        self.assertGreaterEqual(10, self.st)

class DummyCharacter(_MockObj):
    strength = traits.TraitProperty("Strength", trait_type="static", base=10, mod=2)
    hunting = traits.TraitProperty("Hunting skill", trait_type="counter", base=10, mod=1, max=100)
    health = traits.TraitProperty("Health value", trait_type="gauge", base=100)

class TestTraitFields(BaseEvenniaTestCase):
    """
    Test the TraitField class.

    """

    def test_traitfields(self):
        obj = DummyCharacter()
        obj2 = DummyCharacter()

        self.assertEqual(12, obj.strength.value)
        self.assertEqual(11, obj.hunting.value)
        self.assertEqual(100, obj.health.value)

        obj.strength.base += 5
        self.assertEqual(17, obj.strength.value)

        obj.strength.berserk = True
        self.assertEqual(obj.strength.berserk, True)

        self.assertEqual(100, obj.traits.health)
        self.assertEqual(None, obj.traits.hp)

        # the traithandler still works
        obj.traits.health.current -= 1
        self.assertEqual(99, obj.health.value)

        # making sure Descriptors are separate
        self.assertEqual(12, obj2.strength.value)

        obj2.strength.base += 1
        obj.strength.base += 3

        self.assertEqual(13, obj2.strength.value)
        self.assertEqual(20, obj.strength.value)

class TraitContribTestingChar(DefaultCharacter):
    HP = traits.TraitProperty("health", trait_type="trait", value=5)

class TraitPropertyTestCase(EvenniaTest):
    """
    Test atomic updating.

    """

    character_typeclass = TraitContribTestingChar

    def test_round1(self):
        self.char1.HP.value = 1

    def test_round2(self):
        self.char1.HP.value = 2

"""
Traits

Whitenoise 2014, Ainneve contributors,
Griatch 2020

A `Trait` represents a modifiable property on (usually) a Character. They can
be used to represent everything from attributes (str, agi etc) to skills
(hunting 10, swords 14 etc) and dynamically changing things like HP, XP etc.

Traits use Evennia Attributes under the hood, making them persistent (they survive
a server reload/reboot).

Traits are always added to a typeclass, such as the Character class.

There are two ways to set up Traits on a typeclass. The first sets up the `TraitHandler`
as a property `.traits` on your class and you then access traits as e.g. `.traits.strength`.
The other alternative uses a `TraitProperty`, which makes the trait available directly
as e.g. `.strength`. This solution also uses the `TraitHandler`, but you don't need to
define it explicitly. You can combine both styles if you like.

### Traits with TraitHandler

Here's an example for adding the TraitHandler to the Character class:

```python
# mygame/typeclasses/objects.py

from evennia.utils import lazy_property
from evennia.contrib.rpg.traits import TraitHandler

# ...

class Character(DefaultCharacter):
    ...
        # this adds the handler as .traits
        return TraitHandler(self)

        # (or wherever you want)
        self.traits.add("str", "Strength", trait_type="static", base=10, mod=2, mult=2.0)
        self.traits.add("hp", "Health", trait_type="gauge", min=0, max=100)
        self.traits.add("hunting", "Hunting Skill", trait_type="counter",
                        base=10, mod=1, min=0, max=100)

```
When adding the trait, you supply the name of the property (`hunting`) along
with a more human-friendly name ("Hunting Skill"). The latter will show if you
print the trait etc. The `trait_type` is important, this specifies which type
of trait this is (see below).

### TraitProperties

Using `TraitProperties` makes the trait available directly on the class, much like Django model
fields. The drawback is that you must make sure that the name of your Traits don't collide with any
other properties/methods on your class.

```python

from evennia.contrib.rpg.traits import TraitProperty

# ...

class Object(DefaultObject):
    ...
    strength = TraitProperty("Strength", trait_type="static", base=10, mod=2, mult=1.5)
    health = TraitProperty("Health", trait_type="gauge", min=0, base=100, mod=2)
    hunting = TraitProperty("Hunting Skill", trait_type="counter", base=10, mod=1, mult=2.0, min=0, max=100)

```

> Note that the property-name will become the name of the trait and you don't supply `trait_key`
> separately.

> The `.traits` TraitHandler will still be created (it's used under the
> hood. But it will only be created when the TraitProperty has been accessed at least once,
> so be careful if mixing the two styles. If you want to make sure `.traits` is always available,
> add the `TraitHandler` manually like shown earlier - the `TraitProperty` will by default use
> the same handler (`.traits`).

## Using traits

A trait is added to the traithandler (if you use `TraitProperty` the handler is just created under
the hood) after which one can access it as a property on the handler (similarly to how you can do
.db.attrname for Attributes in Evennia).

All traits have a _read-only_ field `.value`. This is only used to read out results, you never
manipulate it directly (if you try, it will just remain unchanged). The `.value` is calculated based
on combining fields, like `.base` and `.mod` - which fields are available and how they relate to
each other depends on the trait type.

```python
> obj.traits.strength.value
18                                  # (base + mod) * mult

> obj.traits.strength.base += 6
obj.traits.strength.value
27

> obj.traits.hp.value
102                                 # (base + mod) * mult

> obj.traits.hp.base -= 200
0                                   # min of 0

> obj.traits.hp.reset()
100

# you can also access properties like a dict
> obj.traits.hp["value"]
100

# you can store arbitrary data persistently for easy reference
> obj.traits.hp.effect = "poisoned!"
> obj.traits.hp.effect
"poisoned!"

# with TraitProperties:

> obj.hunting.value
22

> obj.strength.value += 5
> obj.strength.value
32

```

## Trait types

All default traits have a read-only `.value` property that shows the relevant or
'current' value of the trait. Exactly what this means depends on the type of trait.

Traits can also be combined to do arithmetic with their .value, if both have a
compatible type.

```python
> trait1 + trait2
54

> trait1.value
3

> trait1 + 2
5

```

Two numerical traits can also be compared (bigger-than etc), which is useful in
all sorts of rule-resolution.

```python

if trait1 > trait2:
    # do stuff

```
## Static trait

`value = (base + mod) * mult`

The static trait has a `base` value and an optional `mod`-ifier and 'mult'-iplier.
The modifier defaults to 0, and the multiplier to 1.0, for no change in value.
A typical use of a static trait would be a Strength stat or Skill value. That is,
somethingthat varies slowly or not at all, and which may be modified in-place.

```python
> obj.traits.add("str", "Strength", trait_type="static", base=10, mod=2)
> obj.traits.mytrait.value
12   # base + mod

> obj.traits.mytrait.base += 2
> obj.traits.mytrait.mod += 1
15

> obj.traits.mytrait.mod = 0
> obj.traits.mytrait.mult = 2.0
20

```

### Counter
::

    min/unset     base    base+mod                       max/unset
    |--------------|--------|---------X--------X------------|
                                  current    value
                                             = (current
                                             + mod)
                                             * mult

A counter describes a value that can move from a base. The `.current` property
is the thing usually modified. It starts at the `.base`. One can also add a
modifier, which is added to both the base and to current. '.value' is then formed
by multiplying by the multiplier, which defaults to 1.0 for no change. The min/max
of the range are optional, a boundary set to None will remove it. A suggested use
for a Counter Trait would be to track skill values.

```python
> obj.traits.add("hunting", "Hunting Skill", trait_type="counter",
                   base=10, mod=1, mult=1.0, min=0, max=100)
> obj.traits.hunting.value
11  # current starts at base + mod

> obj.traits.hunting.current += 10
21

# reset back to base+mod by deleting current
> del obj.traits.hunting.current
11

> obj.traits.hunting.max = None  # removing upper bound
> obj.traits.hunting.mult = 100.0
1100

# for TraitProperties, pass the args/kwargs of traits.add() to the
# TraitProperty constructor instead.

```

Counters have some extra properties:

#### .descs

The `descs` property is a dict {upper_bound:text_description}. This allows for easily
storing a more human-friendly description of the current value in the
interval. Here is an example for skill values between 0 and 10:
::

    {0: "unskilled", 1: "neophyte", 5: "trained", 7: "expert", 9: "master"}

The keys must be supplied from smallest to largest. Any values below the lowest and above the
highest description will be considered to be included in the closest description slot.
By calling `.desc()` on the Counter, you will get the text matching the current `value`.

```python
# (could also have passed descs= to traits.add())
> obj.traits.hunting.descs = {
    0: "unskilled", 10: "neophyte", 50: "trained", 70: "expert", 90: "master"}
11

> obj.traits.hunting.desc()
"neophyte"
> obj.traits.hunting.current += 60
71

"expert"

```

#### .rate

The `rate` property defaults to 0. If set to a value different from 0, it
allows the trait to change value dynamically. This could be used for example
for an attribute that was temporarily lowered but will gradually (or abruptly)
recover after a certain time. The rate is given as change of the current
`.value` per-second, and this will still be restrained by min/max boundaries,
if those are set.

It is also possible to set a `.ratetarget`, for the auto-change to stop at
(rather than at the min/max boundaries). This allows the value to return to
a previous value.

```python

71

> obj.traits.hunting.ratetarget = 71
# debuff hunting for some reason
> obj.traits.hunting.current -= 30
41

> obj.traits.hunting.rate = 1  # 1/s increase
# Waiting 5s
46

# Waiting 8s
54

# Waiting 100s
71    # we have stopped at the ratetarget

> obj.traits.hunting.rate = 0  # disable auto-change

```
Note that when retrieving the `current`, the result will always be of the same
type as the `.base` even `rate` is a non-integer value. So if `base` is an `int`
(default)`, the `current` value will also be rounded the closest full integer.
If you want to see the exact `current` value, set `base` to a float - you
will then need to use `round()` yourself on the result if you want integers.

#### .percent()

If both min and max are defined, the `.percent()` method of the trait will
return the value as a percentage.

```python
> obj.traits.hunting.percent()
"71.0%"

> obj.traits.hunting.percent(formatting=None)
71.0

```

### Gauge

This emulates a [fuel-] gauge that empties from a base+mod value.
::

    min/0                                            max=base+mod
     |-----------------------X---------------------------|
                           value
                          = current

The `.current` value will start from a full gauge. The .max property is
read-only and is set by `.base` + `.mod`. So contrary to a `Counter`, the
`.mod` modifier only applies to the max value of the gauge and not the current
value. The minimum bound defaults to 0 if not set explicitly.

This trait is useful for showing commonly depletable resources like health,
stamina and the like.

```python
> obj.traits.add("hp", "Health", trait_type="gauge", base=100)
> obj.traits.hp.value  # (or .current)
100

> obj.traits.hp.mod = 10
110

> obj.traits.hp.current -= 30
80

```

The Gauge trait is subclass of the Counter, so you have access to the same
methods and properties where they make sense. So gauges can also have a
`.descs` dict to describe the intervals in text, and can use `.percent()` to
get how filled it is as a percentage etc.

The `.rate` is particularly relevant for gauges - useful for everything
from poison slowly draining your health, to resting gradually increasing it.

### Trait

A single value of any type.

This is the 'base' Trait, meant to inherit from if you want to invent
trait-types from scratch (most of the time you'll probably inherit from some of
the more advanced trait-type classes though).

Unlike other Trait-types, the single `.value` property of the base `Trait` can
be editied. The value can hold any data that can be stored in an Attribute. If
it's an integer/float you can do arithmetic with it, but otherwise this acts just
like a glorified Attribute.

```python
> obj.traits.add("mytrait", "My Trait", trait_type="trait", value=30)
30

> obj.traits.mytrait.value = "stringvalue"
"stringvalue"

```

## Expanding with your own Traits

A Trait is a class inhering from `evennia.contrib.rpg.traits.Trait` (or from one of
the existing Trait classes).

```python
# in a file, say, 'mygame/world/traits.py'

from evennia.contrib.rpg.traits import StaticTrait

class RageTrait(StaticTrait):

    trait_type = "rage"
    default_keys = {
        "rage": 0
    }

    def berserk(self):
        self.mod = 100

    def sedate(self):
        self.mod = 0

```

Above is an example custom-trait-class "rage" that stores a property "rage" on
itself, with a default value of 0. This has all the functionality of a Trait -
for example, if you do del on the `rage` property, it will be set back to its
default (0). Above we also added some helper methods.

To add your custom RageTrait to Evennia, add the following to your settings file
(assuming your class is in mygame/world/traits.py):
::

    TRAIT_CLASS_PATHS = ["world.traits.RageTrait"]

Reload the server and you should now be able to use your trait:

```python
> obj.traits.add("mood", "A dark mood", rage=30, trait_type='rage')
> obj.traits.mood.rage
30

# as TraitProperty

    rage = TraitProperty("A dark mood", rage=30, trait_type='rage')

```

----

"""

from functools import total_ordering
from time import time

from evennia.utils.dbserialize import _SaverDict
    percent,
)

# Available Trait classes.
# This way the user can easily supply their own. Each
# class should have a class-property `trait_type` to
# identify the Trait class. The default ones are "static",
# "counter" and "gauge".

_TRAIT_CLASS_PATHS = [
]

if hasattr(settings, "TRAIT_CLASS_PATHS"):
    _TRAIT_CLASS_PATHS += settings.TRAIT_CLASS_PATHS

# delay trait-class import to avoid circular import
_TRAIT_CLASSES = None

def _delayed_import_trait_classes():
    """
    Import classes based on the given paths. Note that
    imports from settings are last in the list, so if they
    have the same trait_type set, they will replace the
    default.
    """
    global _TRAIT_CLASSES
    if _TRAIT_CLASSES is None:
        _TRAIT_CLASSES = {}
        for classpath in _TRAIT_CLASS_PATHS:
            try:
                cls = class_from_module(classpath)
                logger.log_trace(f"Could not import Trait from {classpath}.")
            else:
                if hasattr(cls, "trait_type"):
                    trait_type = cls.trait_type
                else:
                    trait_type = str(cls.__name___).lower()
                _TRAIT_CLASSES[trait_type] = cls

# this is the default we offer in TraitHandler.add
DEFAULT_TRAIT_TYPE = "static"

class TraitException(RuntimeError):
    """
    Base exception class raised by `Trait` objects.

    Args:
        msg (str): informative error message

    """

    def __init__(self, msg):
        self.msg = msg

class MandatoryTraitKey:
    """
    This represents a required key that must be
    supplied when a Trait is initialized. It's used
    by Trait classes when defining their required keys.

    """

class TraitHandler:
    """
    Factory class that instantiates Trait objects. Must be assigned as a property
    on the class, usually with `lazy_property`.

    Example:
    ::
        class Object(DefaultObject):
            ...
            @lazy_property
            def traits(self):
                # this adds the handler as .traits
                return TraitHandler(self)

    """

    def __init__(self, obj, db_attribute_key="traits", db_attribute_category="traits"):
        """
        Initialize the handler and set up its internal Attribute-based storage.

        Args:
            obj (Object): Parent Object typeclass for this TraitHandler
            db_attribute_key (str): Name of the DB attribute for trait data storage.
            db_attribute_category (str):  Name of DB attribute's category to trait data storage.

        """
        # load the available classes, if necessary
        _delayed_import_trait_classes()

        # initialize any
        # Note that .trait_data retains the connection to the database, meaning every
        # update we do to .trait_data automatically syncs with database.
        self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category)
        if self.trait_data is None:
            # no existing storage; initialize it, we then have to fetch it again
            # to retain the db connection
            obj.attributes.add(db_attribute_key, {}, category=db_attribute_category)
            self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category)
        self._cache = {}

    def __len__(self):
        """Return number of Traits registered with the handler"""
        return len(self.trait_data)

    def __setattr__(self, trait_key, value):
        """
        Returns error message if trait objects are assigned directly.

        Args:
            trait_key (str): The Trait-key, like "hp".
            value (any): Data to store.
        """
        if trait_key in ("trait_data", "_cache"):
            _SA(self, trait_key, value)
        else:
            trait_cls = self._get_trait_class(trait_key=trait_key)
            valid_keys = list_to_string(list(trait_cls.default_keys.keys()), endsep="or")
            raise TraitException(
                f"Trait object not settable directly. Assign to {trait_key}.{valid_keys}."
            )

    def __setitem__(self, trait_key, value):
        """Returns error message if trait objects are assigned directly."""
        return self.__setattr__(trait_key, value)

    def __getattr__(self, trait_key):
        """Returns Trait instances accessed as attributes."""
        return self.get(trait_key)

    def __getitem__(self, trait_key):
        """Returns `Trait` instances accessed as dict keys."""

        return "TraitHandler ({num} Trait(s) stored): {keys}".format(
            num=len(self), keys=", ".join(self.all())
        )

    def _get_trait_class(self, trait_type=None, trait_key=None):
        """
        Helper to retrieve Trait class based on type (like "static")
        or trait-key (like "hp").

        """
        if not trait_type and trait_key:
            try:
                trait_type = self.trait_data[trait_key]["trait_type"]
                raise TraitException(f"Trait class for Trait {trait_key} could not be found.")
        try:
            return _TRAIT_CLASSES[trait_type]
            raise TraitException(f"Trait class for {trait_type} could not be found.")

        """
        Get all trait keys in this handler.

        Returns:
            list: All Trait keys.

        """
        return list(self.trait_data.keys())

    def get(self, trait_key):
        """
        Args:
            trait_key (str): key from the traits dict containing config data.

        Returns:
            (`Trait` or `None`): named Trait class or None if trait key
            is not found in traits collection.

        """
        trait = self._cache.get(trait_key)
        if trait is None and trait_key in self.trait_data:
            trait_type = self.trait_data[trait_key]["trait_type"]
            trait_cls = self._get_trait_class(trait_type)
            trait = self._cache[trait_key] = trait_cls(_GA(self, "trait_data")[trait_key])
        return trait

    def add(
        self, trait_key, name=None, trait_type=DEFAULT_TRAIT_TYPE, force=True, **trait_properties
    ):
        """
        Create a new Trait and add it to the handler.

        Args:
            trait_key (str): This is the name of the property that will be made
                available on this handler (example 'hp').
            name (str, optional): Name of the Trait, like "Health". If
                not given, will use `trait_key` starting with a capital letter.
            trait_type (str, optional): One of 'static', 'counter' or 'gauge'.
            force (bool): If set, create a new Trait even if a Trait with
                the same `trait_key` already exists.
            trait_properties (dict): These will all be use to initialize
                the new trait. See the `properties` class variable on each
                Trait class to see which are required.

        Raises:
            TraitException: If specifying invalid values for the given Trait,
                the `trait_type` is not recognized, or an existing trait
                already exists (and `force` is unset).

        """

        if trait_key in self.trait_data:
            if force:
                self.remove(trait_key)
            else:
                raise TraitException(f"Trait '{trait_key}' already exists.")

        trait_class = _TRAIT_CLASSES.get(trait_type)
        if not trait_class:
            raise TraitException(f"Trait-type '{trait_type}' is invalid.")

        trait_properties["name"] = trait_key.title() if not name else name
        trait_properties["trait_type"] = trait_type

        # this will raise exception if input is insufficient
        trait_properties = trait_class.validate_input(trait_class, trait_properties)

        self.trait_data[trait_key] = trait_properties

    def remove(self, trait_key):
        """
        Remove a Trait from the handler's parent object.

        Args:
            trait_key (str): The name of the trait to remove.

        """
        if trait_key not in self.trait_data:
            raise TraitException(f"Trait '{trait_key}' not found.")

        if trait_key in self._cache:
            del self._cache[trait_key]
        del self.trait_data[trait_key]

        """
        Remove all Traits from the handler's parent object.
        """
        for trait_key in self.all():
            self.remove(trait_key)

class TraitProperty:
    """
    Optional extra: Allows for applying traits as individual properties directly on the parent class
    instead for properties on the `.traits` handler. So with this you could access data e.g. as
    `character.hp.value` instead of `character.traits.hp.value`. This still uses the traitshandler
    under the hood.

    Example:
    ::
        from evennia.utils import lazy_property
        from evennia.contrib.rpg.traits import TraitProperty

        class Character(DefaultCharacter):

            strength = TraitProperty(name="STR", trait_type="static", base=10, mod=2)
            hunting = TraitProperty("Hunting Skill", trait_type="counter",
                                    base=10, mod=1, max=100)
            health = TraitProperty(trait_type="gauge", min=0, base=100)

    """

    def __init__(self, name=None, trait_type=DEFAULT_TRAIT_TYPE, force=True, **trait_properties):
        """
        Initialize a TraitField. Mimics TraitHandler.add input except no `trait_key`.

        Args:
        Kwargs:
            traithandler_name (str): If given, this is used as the name of the TraitHandler created
                behind the scenes. If not set, this will be a property `traits` on the class.
            any: All other trait_properties are the same as for adding a new trait of the given type
                using the normal TraitHandler.

        """
        self._traithandler_name = trait_properties.pop("traithandler_name", "traits")

        trait_properties.update({"name": name, "trait_type": trait_type, "force": force})
        self._trait_properties = trait_properties

    def __set_name__(self, instance, name):
        """
        This is called the very first time the Descriptor is assigned to the
        class; we store it so we can create new instances with this later.

        """
        self._trait_key = name

        """
        Descriptor definition. This is called when the trait-name is aqcuired on the
        instance and reroutes to fetching the actual Trait from the connected
        TraitHandler (the connection is set up on-demand).

        Returns:
            Trait: The trait this property represents.

        Notes:
            We have one descriptor on the class, but we don't want each instance to share the
            state (self) of that descriptor. So we must make sure to cache the trait per-instance
            or we would end up with cross-use between instances.

        """
        if instance not in self._cache:
            try:
                traithandler = getattr(instance, self._traithandler_name)
                # traithandler not found; create a new on-demand
                traithandler = TraitHandler(instance)
                setattr(instance, self._traithandler_name, traithandler)

            # this will either get the trait from attribute or make a new one
            trait = traithandler.get(self._trait_key)
            if trait is None:
                # initialize the trait
                traithandler.add(self._trait_key, **self._trait_properties)
                trait = traithandler.get(self._trait_key)  # caches it in the traithandler
            self._cache[instance] = trait
        return self._cache[instance]

        """
        We don't set data directly, it's all rerouted to the trait.

        """
        pass

# Parent Trait class

@total_ordering
class Trait:
    """Represents an object or Character trait. This simple base is just
    storing anything in it's 'value' property, so it's pretty much just a
    different wrapper to an Attribute. It does no type-checking of what is
    stored.

    Note:
        See module docstring for configuration details.

    value

    """

    # this is the name used to refer to this trait when adding
    # a new trait in the TraitHandler
    trait_type = "trait"

    # Property kwargs settable when creating a Trait of this type. This is a
    # dict of key: default. To indicate a mandatory kwarg and raise an error if
    # not given, set the default value to the `traits.MandatoryTraitKey` class.
    # Apart from the keys given here, "name" and "trait_type" will also always
    # have to be a apart of the data.
    default_keys = {"value": None}

    # enable to set/retrieve other arbitrary properties on the Trait
    # and have them treated like data to store.
    allow_extra_properties = True

    def __init__(self, trait_data):
        """
        This both initializes and validates the Trait on creation. It must
        raise exception if validation fails. The TraitHandler will call this
        when the trait is furst added, to make sure it validates before
        storing.

        Args:
            trait_data (any): Any pickle-able values to store with this trait.
                This must contain any cls.default_keys that do not have a default
                value in cls.data_default_values. Any extra kwargs will be made
                available as extra properties on the Trait, assuming the class
                variable `allow_extra_properties` is set.

        Raises:
            TraitException: If input-validation failed.

        """
        self._data = self.__class__.validate_input(self.__class__, trait_data)

        if not isinstance(trait_data, _SaverDict):
            logger.log_warn(
                f"Non-persistent Trait data (type(trait_data)) loaded for {type(self).__name__}."
            )

    def validate_input(cls, trait_data):
        """
        Validate input

        Args:
            trait_data (dict or _SaverDict): Data to be used for
                initialization of this trait.
        Returns:
            dict: Validated data, possibly complemented with default
                values from default_keys.
        Raises:
            TraitException: If finding unset keys without a default.

        """

        def _raise_err(unset_required):
            """Helper method to format exception."""
                "Trait {} could not be created - misses required keys {}.".format(
                    cls.trait_type, list_to_string(list(unset_required), addquote=True)
                )
            )

        inp = set(trait_data.keys())

        # separate check for name/trait_type, those are always required.
        req = set(("name", "trait_type"))
        unsets = req.difference(inp.intersection(req))
        if unsets:
            _raise_err(unsets)

        # check other keys, these likely have defaults to fall back to
        req = set(list(cls.default_keys.keys()))
        unset_defaults = {key: cls.default_keys[key] for key in unsets}

        if MandatoryTraitKey in unset_defaults.values():
            # we have one or more unset keys that was mandatory
            _raise_err([key for key, value in unset_defaults.items() if value == MandatoryTraitKey])
        # apply the default values
        trait_data.update(unset_defaults)

        if not cls.allow_extra_properties:
            # don't allow any extra properties - remove the extra data
            for key in (key for key in inp.difference(req) if key not in ("name", "trait_type")):
                del trait_data[key]

        return trait_data

    # Grant access to properties on this Trait.

    def __getitem__(self, key):
        """Access extra parameters as dict keys."""
        try:
            return self.__getattr__(key)
            raise KeyError(key)

        """Set extra parameters as dict keys."""
        self.__setattr__(key, value)

    def __delitem__(self, key):
        """Delete extra parameters as dict keys."""
        self.__delattr__(key)

    def __getattr__(self, key):
        """Access extra parameters as attributes."""
        if key in ("default_keys", "data_default", "trait_type", "allow_extra_properties"):
            return _GA(self, key)
        try:
            return self._data[key]
            raise AttributeError(
                "{!r} {} ({}) has no property {!r}.".format(
                    self._data["name"], type(self).__name__, self.trait_type, key
                )
            )

    def __setattr__(self, key, value):
        """Set extra parameters as attributes.

        Arbitrary attributes set on a Trait object will be
        stored in the 'extra' key of the `_data` attribute.

        This behavior is enabled by setting the instance
        variable `_locked` to True.

        """
        propobj = getattr(self.__class__, key, None)
        if isinstance(propobj, property):
            # we have a custom property named as this key, find and use its setter
            if propobj.fset:
                propobj.fset(self, value)
            return
        else:
            # this is some other value
            if key in ("_data",):
                _SA(self, key, value)
                return
            if _GA(self, "allow_extra_properties"):
                _GA(self, "_data")[key] = value
                return
        raise AttributeError(f"Can't set attribute {key} on {self.trait_type} Trait.")

    def __delattr__(self, key):
        """
        Delete or reset parameters.

        Args:
            key (str): property-key to delete.
        Raises:
            TraitException: If trying to delete a data-key
                without a default value to reset to.
        Notes:
            This will outright delete extra keys (if allow_extra_properties is
            set). Keys in self.default_keys with a default value will be
            reset to default. A data_key with a default of MandatoryDefaultKey
            will raise a TraitException. Unfound matches will be silently ignored.

        """
        if key in self.default_keys:
            if self.default_keys[key] == MandatoryTraitKey:
                raise TraitException(
                    "Trait-Key {key} cannot be deleted: It's a mandatory property "
                    "with no default value to fall back to."
                )
            # set to default
            self._data[key] = self.default_keys[key]
        elif key in self._data:
            try:
                # check if we have a custom deleter
                _DA(self, key)
                # delete normally
                del self._data[key]
        else:
            try:
                # check if we have custom deleter, otherwise ignore
                pass

        """Debug-friendly representation of this Trait."""
        return "{}({{{}}})".format(
            type(self).__name__,
            ", ".join(
                [
                    "'{}': {!r}".format(k, self._data[k])
                    for k in self.default_keys
                    if k in self._data
                ]
            ),
        )

        return f"<Trait {self.name}: {self._data['value']}>"

    # access properties

    @property
    def name(self):
        """Display name for the trait."""
        return self._data["name"]

    key = name

    # Numeric operations

        """Support equality comparison between Traits or Trait and numeric.

        Note:
            This class uses the @functools.total_ordering() decorator to
            complete the rich comparison implementation, therefore only
            `__eq__` and `__lt__` are implemented.
        """
        if inherits_from(other, Trait):
            return self.value == other.value
        elif type(other) in (float, int):
            return self.value == other
        else:

    def __lt__(self, other):
        """Support less than comparison between `Trait`s or `Trait` and numeric."""
            return self.value < other.value
            return self.value < other
        else:

    def __pos__(self):
        """Access `value` property through unary `+` operator."""
        return self.value

    def __add__(self, other):
        """Support addition between `Trait`s or `Trait` and numeric"""
            return self.value + other.value
            return self.value + other
        else:

    def __sub__(self, other):
        """Support subtraction between `Trait`s or `Trait` and numeric"""
            return self.value - other.value
            return self.value - other
        else:

    def __mul__(self, other):
        """Support multiplication between `Trait`s or `Trait` and numeric"""
            return self.value * other.value
            return self.value * other
        else:

    def __floordiv__(self, other):
        """Support floor division between `Trait`s or `Trait` and numeric"""
            return self.value // other.value
            return self.value // other
        else:

    # commutative property
    __radd__ = __add__
    __rmul__ = __mul__

    def __rsub__(self, other):
            return other.value - self.value
            return other - self.value
        else:

    def __rfloordiv__(self, other):
            return other.value // self.value
            return other // self.value
        else:

    # Public members

    @property
    def value(self):
        """Store a value"""
        return self._data["value"]

    @value.setter
    def value(self, value):
        """Get value"""
        self._data["value"] = value

# Implementation of the respective Trait types

class StaticTrait(Trait):
    """
    Static Trait. This is a single value with a modifier,
    multiplier, and no concept of a 'current' value or min/max etc.

    value = (base + mod) * mult

    """

    trait_type = "static"

    default_keys = {"base": 0, "mod": 0, "mult": 1.0}

        status = "{value:11}".format(value=self.value)
        return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(
            name=self.name, status=status, mod=self.mod, mult=self.mult
        )

    # Helpers
    @property
    def base(self):
        return self._data["base"]

    @base.setter
    def base(self, value):
            self._data["base"] = self.default_keys["base"]
        if type(value) in (int, float):
            self._data["base"] = value

    @property
    def mod(self):
        """The trait's modifier."""
        return self._data["mod"]

    @mod.setter
    def mod(self, amount):
        if type(amount) in (int, float):
            self._data["mod"] = amount

    @property
    def mult(self):
        """The trait's multiplier."""
        return self._data["mult"]

    @mult.setter
    def mult(self, amount):
            self._data["mult"] = amount

    @mult.deleter
        self._data["mult"] = 1.0

    @property
        "The value of the Trait."
        return (self.base + self.mod) * self.mult

class CounterTrait(Trait):
    """
    Counter Trait.

    This includes modifications and min/max limits as well as the notion of a
    current value. The value can also be reset to the base value.

    min/unset     base  (base+mod)*mult                  max/unset
     |--------------|--------|---------X--------X------------|
                                    current   value
                                              = (current
                                              + mod)
                                              * mult

    - value = (current + mod) * mult, starts at (base + mod) * mult
    - if min or max is None, there is no upper/lower bound (default)
    - if max is set to "base", max will be equal ot base+mod
    - descs are used to optionally describe each value interval.
      The desc of the current `value` value can then be retrieved
      with .desc(). The property is set as {lower_bound_inclusive:desc}
      and should be given smallest-to-biggest. For example, for
      a skill rating between 0 and 10:
            {0: "unskilled",
             1: "neophyte",
             5: "traited",
             7: "expert",
             9: "master"}
    - rate/ratetarget are optional settings to include a rate-of-change
      of the current value. This is calculated on-demand and allows for
      describing a value that is gradually growing smaller/bigger. The
      increase will stop when either reaching a boundary (if set) or
      ratetarget. Setting the rate to 0 (default) stops any change.

    """

    trait_type = "counter"

    # current starts equal to base.
        "base": 0,
        "mod": 0,
        "mult": 1.0,
        "min": None,
        "max": None,
        "descs": None,
        "rate": 0,
        "ratetarget": None,
    }

        """Add extra validation for descs"""
        trait_data = Trait.validate_input(cls, trait_data)
        # validate descs
        descs = trait_data["descs"]
        if isinstance(descs, dict):
            if any(
                not (isinstance(key, (int, float)) and isinstance(value, str))
                for key, value in descs.items()
            ):
                    "Trait descs must be defined on the "
                    f"form {{number:str}} (instead found {descs})."
                )
        # set up rate
        if trait_data["rate"] != 0:
            trait_data["last_update"] = time()
        else:
            trait_data["last_update"] = None

        status = "{current:4} / {base:4}".format(current=self.current, base=self.base)
        )

    # Helpers

    def _within_boundaries(self, value):
        """Check if given value is within boundaries"""
        return not (
            (self.min is not None and value <= self.min)
            or (self.max is not None and value >= self.max)
        )

    def _enforce_boundaries(self, value):
        """Ensures that incoming value falls within boundaries"""
        if self.min is not None and value <= self.min:
            return self.min
        if self.max is not None and value >= self.max:
            return self.max

    # timer component

    def _passed_ratetarget(self, value):
        """Check if we passed the ratetarget in either direction."""
        ratetarget = self._data["ratetarget"]
        return ratetarget is not None and (
            (self.rate < 0 and value <= ratetarget) or (self.rate > 0 and value >= ratetarget)
        )

    def _stop_timer(self):
        """Stop rate-timer component."""
        if self.rate != 0 and self._data["last_update"] is not None:
            self._data["last_update"] = None

    def _check_and_start_timer(self, value):
        """Start timer if we are not at a boundary."""
        if self.rate != 0 and self._data["last_update"] is None:
            if self._within_boundaries(value) and not self._passed_ratetarget(value):
                # we are not at a boundary [anymore].
                self._data["last_update"] = time()

    def _update_current(self, current):
        """Update current value by scaling with rate and time passed."""
        rate = self.rate
        if rate != 0 and self._data["last_update"] is not None:
            now = time()
            tdiff = now - self._data["last_update"]
            current += rate * tdiff
            value = current + self.mod

            # we must make sure so we don't overstep our bounds
            # even if .mod is included

            if self._passed_ratetarget(value):
                current = self._data["ratetarget"] - self.mod
                self._stop_timer()
            elif not self._within_boundaries(value):
                current = self._enforce_boundaries(value) - self.mod
            else:
                self._data["last_update"] = now

            self._data["current"] = current

        if self.base is not None and isinstance(self.base, int):
            return round(current)
        return current

    @property

            if self.min is not None and value + self.mod < self.min:
                value = self.min - self.mod
            if self.max is not None and value + self.mod > self.max:
                value = self.max - self.mod

    @property

    def mod(self, value):
            # unsetting the boundary to default
            self._data["mod"] = self.default_keys["mod"]
        elif type(value) in (int, float):
            if self.min is not None and value + self.base < self.min:
                value = self.min - self.base
            if self.max is not None and value + self.base > self.max:
                value = self.max - self.base
            self._data["mod"] = value

    @property

    @property
    def min(self):
        return self._data["min"]

    @min.setter
    def min(self, value):
            # unsetting the boundary
            self._data["min"] = value
            if self.max is not None:
                value = min(self.max, value)
            self._data["min"] = min(value, self.base + self.mod)

    @property
    def max(self):
        return self._data["max"]

    @max.setter
    def max(self, value):
            self._data["max"] = value
            if self.min is not None:
                value = max(self.min, value)
            self._data["max"] = max(value, self.base + self.mod)

    @property
    def current(self):
        """The `current` value of the `Trait`. This does not have .mod added and is not .mult-iplied."""
        return self._update_current(self._data.get("current", self.base))

    @current.setter
    def current(self, value):
            self._data["current"] = self._check_and_start_timer(self._enforce_boundaries(value))

    @current.deleter
        """reset back to base"""
        self._data["current"] = self.base

    @property
        "The value of the Trait. (current + mod) * mult"
        return self._enforce_boundaries((self.current + self.mod) * self.mult)

    @property
    def ratetarget(self):
        return self._data["ratetarget"]

    @ratetarget.setter
    def ratetarget(self, value):
        self._data["ratetarget"] = self._enforce_boundaries(value)
        self._check_and_start_timer(self.value)

    def percent(self, formatting="{:3.1f}%"):
        """
        Return the current value as a percentage.

        Args:
            formatting (str, optional): Should contain a
               format-tag which will receive the value. If
               this is set to None, the raw float will be
               returned.
        Returns:
            float or str: Depending of if a `formatting` string
                is supplied or not.
        """
        return percent(self.value, self.min, self.max, formatting=formatting)

        """Resets `current` property equal to `base` value."""
        del self.current

    def desc(self):
        """
        Retrieve descriptions of the current value, if available.

        This must be a mapping {upper_bound_inclusive: text},
        ordered from small to big. Any value above the highest
        upper bound will be included as being in the highest bound.
        rely on Python3.7+ dicts retaining ordering to let this
        describe the interval.

        Returns:
            str: The description describing the `value` value.
                If not found, returns the empty string.
        """
        descs = self._data["descs"]
        if descs is None:
            return ""
        value = self.value
        # we rely on Python3.7+ dicts retaining ordering
        highest = ""
        for bound, txt in descs.items():
            highest = txt
            if value <= bound:
                return txt
        # if we get here we are above the highest bound so
        # we return the latest bound specified.
        return highest

class GaugeTrait(CounterTrait):
    """
    Gauge Trait.

    This emulates a gauge-meter that empties from a (base+mod) * mult value.

    min/0                                       max=(base+mod)*mult
                           value
                          = current

    - min defaults to 0
    - max value is always (base + mod) * mult
    - .max is an alias of .base
    - value = current and varies from min to max.
    - descs is a mapping {upper_bound_inclusive: desc}. These
        are checked with .desc() and can be retrieve a text
        description for a given current value.

        For example, this could be used to describe health
        values between 0 and 100:
            {0: "Dead"
             10: "Badly hurt",
             30: "Bleeding",
             50: "Hurting",
             90: "Healthy"}

    """

    trait_type = "gauge"

    # same as Counter, here for easy reference
    # current starts out equal to base
        "base": 0,
        "mod": 0,
        "min": 0,
        "rate": 0,
    }

            value = current

            # we don't worry about .mod for gauges

                current = self._data["ratetarget"]
                current = self._enforce_boundaries(value)
            else:

        """Ensures that incoming value falls within trait's range."""
        return min((self.mod + self.base) * self.mult, value)

        status = "{value:4} / {base:4}".format(value=self.value, base=self.base)
        )

    @property

        """Limit so base+mod can never go below min."""
            if value + self.mod < self.min:

    @property

            if value + self.base < self.min:

    @property

    @property
        val = self._data["min"]
        return self.default_keys["min"] if val is None else val

        """Limit so min can never be greater than (base+mod)*mult."""
            self._data["min"] = self.default_keys["min"]
            self._data["min"] = min(value, (self.base + self.mod) * self.mult)

    @property
        "The max is always (base + mod) * mult."

        raise TraitException(
            "The .max property is not settable on GaugeTraits. Set .mod and .base instead."
        )

    @max.deleter
            "The .max property cannot be reset on GaugeTraits. Reset .mod and .base instead."
        )

    @property
        """The `current` value of the gauge."""
        return self._update_current(
            self._enforce_boundaries(self._data.get("current", (self.base + self.mod) * self.mult))
        )

        "Resets current back to 'full'"
        self._data["current"] = (self.base + self.mod) * self.mult

    @property
        "The value of the trait"
        return self.current

        """

        Args:
               returned.
        Returns:
        """
        return percent(self.current, self.min, self.max, formatting=formatting)

        """
        Fills the gauge to its maximum allowed by base + mod
        """

"""
Script example

Griatch - 2012

Example script for testing. This adds a simple timer that has your
character make observations and notices at irregular intervals.

To test, use
  script me = tutorial_examples.bodyfunctions.BodyFunctions

The script will only send messages to the object it is stored on, so
make sure to put it on yourself or you won't see any messages!

"""

class BodyFunctions(DefaultScript):
    """
    This class defines the script itself

    """

        self.key = "bodyfunction"
        self.desc = "Adds various timed events to a character."
        self.interval = 20  # seconds
        # self.repeats = 5  # repeat only a certain number of times
        self.start_delay = True  # wait self.interval until first call
        # self.persistent = True

        """
        This gets called every self.interval seconds. We make
        a random check here so as to only return 33% of the time.
        """
        if random.random() < 0.66:
            # no message this time
            return
        self.send_random_message()

    def send_random_message(self):
        rand = random.random()
        # return a random message
        if rand < 0.1:
            string = "You tap your foot, looking around."
        elif rand < 0.2:
            string = "You have an itch. Hard to reach too."
        elif rand < 0.3:
            string = (
                "You think you hear someone behind you. ... but when you look there's noone there."
            )
        elif rand < 0.4:
            string = "You inspect your fingernails. Nothing to report."
        elif rand < 0.5:
            string = "You cough discreetly into your hand."
        elif rand < 0.6:
            string = "You scratch your head, looking around."
        elif rand < 0.7:
            string = "You blink, forgetting what it was you were going to do."
        elif rand < 0.8:
            string = "You feel lonely all of a sudden."
        elif rand < 0.9:
            string = "You get a great idea. Of course you won't tell anyone."
        else:
            string = "You suddenly realize how much you love Evennia!"

        # echo the message to the object
        self.obj.msg(string)

"""
Tests for the bodyfunctions.

"""

from .bodyfunctions import BodyFunctions

@patch("evennia.contrib.tutorials.bodyfunctions.bodyfunctions.random")
class TestBodyFunctions(BaseEvenniaTest):
    script_typeclass = BodyFunctions

        self.script.obj = self.char1

        # if we forget to stop the script, DirtyReactorAggregateError will be raised
        self.script.stop()

    def test_at_repeat(self, mock_random):
        """test that no message will be sent when below the 66% threshold"""
        mock_random.random = Mock(return_value=0.5)
        old_func = self.script.send_random_message
        self.script.send_random_message = Mock()
        self.script.at_repeat()
        self.script.send_random_message.assert_not_called()
        # test that random message will be sent
        mock_random.random = Mock(return_value=0.7)
        self.script.send_random_message.assert_called()
        self.script.send_random_message = old_func

    def test_send_random_message(self, mock_random):
        """Test that correct message is sent for each random value"""
        old_func = self.char1.msg
        # test each of the values
        mock_random.random = Mock(return_value=0.05)
        self.script.send_random_message()
        self.char1.msg.assert_called_with("You tap your foot, looking around.")
        mock_random.random = Mock(return_value=0.15)
        self.char1.msg.assert_called_with("You have an itch. Hard to reach too.")
        mock_random.random = Mock(return_value=0.25)
        self.char1.msg.assert_called_with(
            "You think you hear someone behind you. ... " "but when you look there's noone there."
        )
        mock_random.random = Mock(return_value=0.35)
        self.char1.msg.assert_called_with("You inspect your fingernails. Nothing to report.")
        mock_random.random = Mock(return_value=0.45)
        self.char1.msg.assert_called_with("You cough discreetly into your hand.")
        mock_random.random = Mock(return_value=0.55)
        self.char1.msg.assert_called_with("You scratch your head, looking around.")
        mock_random.random = Mock(return_value=0.65)
        self.char1.msg.assert_called_with("You blink, forgetting what it was you were going to do.")
        mock_random.random = Mock(return_value=0.75)
        self.char1.msg.assert_called_with("You feel lonely all of a sudden.")
        mock_random.random = Mock(return_value=0.85)
        self.char1.msg.assert_called_with("You get a great idea. Of course you won't tell anyone.")
        mock_random.random = Mock(return_value=0.95)
        self.char1.msg.assert_called_with("You suddenly realize how much you love Evennia!")
        self.char1.msg = old_func

"""
NPC AI module for EvAdventure (WIP)

This implements a state machine for the NPCs, where it uses inputs from the game to determine what
to do next. The AI works on the concept of being 'ticks', at which point, the AI will decide to move
between different 'states', performing different 'actions' within each state until changing to
another state. The odds of changing between states and performing actions are weighted, allowing for
an AI agent to be more or less likely to perform certain actions.

The state machine is fed a dictionary of states and their transitions, and a dictionary of available
actions to choose between.
::

    {
        "states": {
            "state1": {"action1": odds, "action2": odds, ...},
            "state2": {"action1": odds, "action2": odds, ...}, ...
        }
        "transition": {
            "state1": {"state2": "odds, "state3": odds, ...},
            "state2": {"state1": "odds, "state3": odds, ...}, ...
        }
    }

The NPC class needs to look like this:
::

    class NPC(DefaultCharacter):

        # ...

        def ai(self):
            return AIHandler(self)

        def ai_roam(self, action):
            # perform the action within the current state ai.state

        def ai_hunt(self, action):
            # etc

"""

# Some example AI structures

EMOTIONAL_AI = {
    # Non-combat AI that has different moods for conversations
    "states": {
        "neutral": {"talk_neutral": 0.9, "change_state": 0.1},
        "happy": {"talk_happy": 0.9, "change_state": 0.1},
        "sad": {"talk_sad": 0.9, "change_state": 0.1},
        "angry": {"talk_angry": 0.9, "change_state": 0.1},
    }
}

STATIC_AI = {
    # AI that just hangs around until attacked
        "idle": {"do_nothing": 1.0},
        "combat": {"attack": 0.9, "stunt": 0.1},
    }
}

ROAM_AI = {
    # AI that roams around randomly, now and then stopping.
        "idle": {"do_nothing": 0.9, "change_state": 0.1},
        "roam": {
            "move_north": 0.1,
            "move_south": 0.1,
            "move_east": 0.1,
            "move_west": 0.1,
            "wait": 0.4,
            "change_state": 0.2,
        },
        "combat": {"attack": 0.9, "stunt": 0.05, "flee": 0.05},
    },
    "transitions": {
        "idle": {"roam": 0.5, "idle": 0.5},
        "roam": {"idle": 0.1, "roam": 0.9},
    },
}

HUNTER_AI = {
        "hunt_roam": {
            "move_north": 0.2,
            "move_south": 0.2,
            "move_east": 0.2,
            "move_west": 0.2,
        },
        "hunt_track": {
            "track_and_move": 0.9,
            "change_state": 0.1,
        },
        "combat": {"attack": 0.8, "stunt": 0.1, "other": 0.1},
    },
        # add a chance of the hunter losing its trail
        "hunt_track": {"hunt_roam": 1.0},
    },
}

class AIHandler:
    """
    AIHandler class. This should be placed on the NPC object, and will handle the state machine,
    including transitions and actions.

    Add to typeclass with @lazyproperty:

        class NPC(DefaultCharacter):

            ai_states = {...}

            # ...

            @lazyproperty
            def ai(self):
                return AIHandler(self)

    """

        if hasattr(self, "ai_states"):
            # since we're not setting `force=True` here, we won't overwrite any existing /
            # customized dicts.
            self.add_aidict(self.ai_states)

        return f"AIHandler for {self.obj}. Current state: {self.state}"

    def _normalize_odds(odds):
        """
        Normalize odds to 1.0.

        Args:
            odds (list): List of odds to normalize.
        Returns:
            list: Normalized list of odds.

        """
        return [float(i) / sum(odds) for i in odds]

    def _weighted_choice(choices, odds):
        """
        Choose a random element from a list of choices, with odds.

        Args:
            choices (list): List of choices to choose from. Unordered.
            odds (list): List of odds to choose from, matching the choices list. This
                can be a list of integers or floats, indicating priority. Have odds sum
                up to 100 or 1.0 to properly represent predictable odds.
        Returns:
            object: Randomly chosen element from choices.

        """
        if choices:
            return random.choices(choices, odds)[0]

    def _weighted_choice_dict(choices):
        """
        Choose a random element from a dictionary of choices, with odds.

        Args:
            choices (dict): Dictionary of choices to choose from, with odds as values.
        Returns:

        """
        return AIHandler._weighted_choice(list(choices.keys()), list(choices.values()))

    def _validate_ai_dict(aidict):
        """
        Validate and normalize an AI dictionary.

        Args:
            aidict (dict): AI dictionary to normalize.
        Returns:
            dict: Normalized AI dictionary.

        """
        if "states" not in aidict:
            raise ValueError("AI dictionary must contain a 'states' key.")

        if "transitions" not in aidict:
            aidict["transitions"] = {}

        # if we have no transitions, make sure we have a transition for each state set to 0
        for state in aidict["states"]:
            if state not in aidict["transitions"]:
                aidict["transitions"][state] = {}
            for state2 in aidict["states"]:
                if state2 not in aidict["transitions"][state]:
                    aidict["transitions"][state][state2] = 0.0

        # normalize odds
        for state, actions in aidict["states"].items():
            aidict["states"][state] = AIHandler._normalize_odds(list(actions.values()))
        for state, transitions in aidict["transitions"].items():
            aidict["transitions"][state] = AIHandler._normalize_odds(list(transitions.values()))

        return aidict

    @property
        """
        Return the current state of the AI.

        Returns:
            str: Current state of the AI.

        """
        return self.obj.attributes.get("ai_state", category="ai", default="idle")

    @state.setter
    def state(self, value):
        """
        Set the current state of the AI. This allows to force a state change, e.g. when starting
        combat.

        Args:
            value (str): New state of the AI.

        """
        return self.obj.attributes.add("ai_state", category="ai")

    @property
    def states(self):
        """
        Return the states dictionary for the AI.

        Returns:
            dict: States dictionary for the AI.

        """
        return self.obj.attributes.get("ai_states", category="ai", default={"idle": {}})

    @states.setter
    def states(self, value):
        """
        Set the states dictionary for the AI.

        Args:
            value (dict): New states dictionary for the AI.

        """
        return self.obj.attributes.add("ai_states", value, category="ai")

    @property
    def transitions(self):
        """
        Return the transitions dictionary for the AI.

        Returns:
            dict: Transitions dictionary for the AI.

        """
        return self.obj.attributes.get("ai_transitions", category="ai", default={"idle": []})

    @transitions.setter
    def transitions(self, value):
        """
        Set the transitions dictionary for the AI.

        Args:
            value (dict): New transitions dictionary for the AI. This will be automatically
            normalized.

        """
        for state in value.keys():
            value[state] = dict(
                zip(value[state].keys(), self._normalize_odds(value[state].values()))
            )
        return self.obj.attributes.add("ai_transitions", value, category="ai")

    def add_aidict(self, aidict, force=False):
        """
        Add an AI dictionary to the AI handler, if one doesn't already exist.

        Args:
            aidict (dict): AI dictionary to add.
            force (bool, optional): Force adding the AI dictionary, even if one already exists on
            this handler.

        """
        if not force and self.states and self.transitions:
            return

        aidict = self._validate_ai_dict(aidict)
        self.states = aidict["states"]
        self.transitions = aidict["transitions"]

    def adjust_transition_probability(self, state_start, state_end, odds):
        """
        Adjust the transition probability between two states.

        Args:
            state_start (str): State to start from.
            state_end (str): State to end at.
            odds (int): New odds for the transition.

        Note:
            This will normalize the odds across the other transitions from the starting state.

        """
        transitions = deserialize(self.transitions)
        transitions[state_start][state_end] = odds
        transitions[state_start] = dict(
            zip(
                transitions[state_start].keys(),
                self._normalize_odds(transitions[state_start].values()),
            )
        )
        self.transitions = transitions

    def get_next_state(self):
        """
        Get the next state for the AI.

        Returns:
            str: Next state for the AI.

        """
        return self._weighted_choice_dict(self.transitions[self.state])

    def get_next_action(self):
        """
        Get the next action for the AI within the current state.

        Returns:
            str: Next action for the AI.

        """
        return self._weighted_choice_dict(self.states[self.state])

    def execute_ai(self):
        """
        Execute the next ai action in the current state.

        This assumes that each available state exists as a method on the object, named
        ai_<state_name>, taking an optional argument of the next action to perform. The method
        will itself update the state or transition weights through this handler.

        Some states have in-built state transitions, via the special "change_state" action.

        """
        next_action = self.get_next_action()
        statechange = 0
        while next_action == "change_state":
            self.state = self.get_next_state()
            next_action = self.get_next_action()
            if statechange > 5:
                logger.log_err(f"AIHandler: {self.obj} got stuck in a state-change loop.")
                return

        # perform the action
        try:
            getattr(self.obj, f"ai_{self.state}")(next_action)
            logger.log_err(f"AIHandler: {self.obj} has no ai_{self.state} method.")

"""
Character class.

"""

from evennia.utils.evform import EvForm

from . import rules
from .equipment import EquipmentError, EquipmentHandler
from .quests import EvAdventureQuestHandler

class LivingMixin:
    """
    Mixin class to use for all living things.

    """

    is_pc = False

    @property
    def hurt_level(self):
        """
        String describing how hurt this character is.
        """
        percent = max(0, min(100, 100 * (self.hp / self.hp_max)))
        if 95 < percent <= 100:
            return "|gPerfect|n"
        elif 80 < percent <= 95:
            return "|gScraped|n"
        elif 60 < percent <= 80:
            return "|GBruised|n"
        elif 45 < percent <= 60:
            return "|yHurt|n"
        elif 30 < percent <= 45:
            return "|yWounded|n"
        elif 15 < percent <= 30:
            return "|rBadly wounded|n"
        elif 1 < percent <= 15:
            return "|rBarely hanging on|n"
        elif percent == 0:
            return "|RCollapsed!|n"

    def heal(self, hp, healer=None):
        """
        Heal by a certain amount of HP.

        """
        damage = self.hp_max - self.hp
        healed = min(damage, hp)
        self.hp += healed

        if healer is self:
            self.msg(f"|gYou heal yourself for {healed} health.|n")
        elif healer:
            self.msg(f"|g{healer.key} heals you for {healed} health.|n")
        else:
            self.msg(f"You are healed for {healed} health.")

    def at_attacked(self, attacker, **kwargs):
        """
        Called when being attacked / combat starts.

        """
        pass

    def at_damage(self, damage, attacker=None):
        """
        Called when attacked and taking damage.

        """
        self.hp -= damage

    def at_defeat(self):
        """
        Called when this living thing reaches HP 0.

        """
        # by default, defeat means death
        self.at_death()

    def at_death(self):
        """
        Called when this living thing dies.

        """
        pass

    def at_pay(self, amount):
        """
        Get coins, but no more than we actually have.

        """
        amount = min(amount, self.coins)
        self.coins -= amount
        return amount

    def at_looted(self, looter):
        """
        Called when being looted (after defeat).

        Args:
            looter (Object): The one doing the looting.

        """
        max_steal = rules.dice.roll("1d10")
        stolen = self.at_pay(max_steal)

        looter.coins += stolen

        self.location.msg_contents(
            f"$You(looter) loots $You() for {stolen} coins!",
            from_obj=self,
            mapping={"looter": looter},
        )

    def pre_loot(self, defeated_enemy):
        """
        Called just before looting an enemy.

        Args:
            defeated_enemy (Object): The enemy soon to loot.

        Returns:
            bool: If False, no looting is allowed.

        """
        pass

    def at_do_loot(self, defeated_enemy):
        """
        Called when looting another entity.

        Args:
            defeated_enemy: The thing to loot.

        """
        defeated_enemy.at_looted(self)

    def post_loot(self, defeated_enemy):
        """
        Called just after having looted an enemy.

        Args:
            defeated_enemy (Object): The enemy just looted.

        """
        pass

class EvAdventureCharacter(LivingMixin, DefaultCharacter):
    """
    A Character for use with EvAdventure.

    """

    is_pc = True

    # these are the ability bonuses. Defense is always 10 higher
    strength = AttributeProperty(default=1)
    dexterity = AttributeProperty(default=1)
    constitution = AttributeProperty(default=1)
    intelligence = AttributeProperty(default=1)
    wisdom = AttributeProperty(default=1)
    charisma = AttributeProperty(default=1)

    hp = AttributeProperty(default=4)
    hp_max = AttributeProperty(default=4)
    level = AttributeProperty(default=1)
    coins = AttributeProperty(default=0)  # copper coins

    xp = AttributeProperty(default=0)
    xp_per_level = 1000

    def equipment(self):
        """Allows to access equipment like char.equipment.worn"""
        return EquipmentHandler(self)

    def quests(self):
        """Access and track quests"""
        return EvAdventureQuestHandler(self)

    @property
    def weapon(self):
        return self.equipment.weapon

    @property
    def armor(self):
        return self.equipment.armor

    def at_pre_object_receive(self, moved_object, source_location, **kwargs):
        """
        Hook called by Evennia before moving an object here. Return False to abort move.

        Args:
            moved_object (Object): Object to move into this one (that is, into inventory).
            source_location (Object): Source location moved from.
            **kwargs: Passed from move operation; the `move_type` is useful; if someone is giving
                us something (`move_type=='give'`) we want to ask first.

        Returns:
            bool: If move should be allowed or not.

        """
        # this will raise EquipmentError if inventory is full
        return self.equipment.validate_slot_usage(moved_object)

    def at_object_receive(self, moved_object, source_location, **kwargs):
        """
        Hook called by Evennia as an object is moved here. We make sure it's added
        to the equipment handler.

        Args:
            **kwargs: Passed from move operation; unused here.

        """
        try:
            self.equipment.add(moved_object)
        except EquipmentError as err:
            log_trace(f"at_object_receive error: {err}")

    def at_pre_object_leave(self, leaving_object, destination, **kwargs):
        """
        Hook called when dropping an item. We don't allow to drop weilded/worn items
        (need to unwield/remove them first). Return False to

        """

    def at_object_leave(self, moved_object, destination, **kwargs):
        """
        Called just before an object leaves from inside this object

        Args:
            destination (Object): Where `moved_obj` is going.

        """
        self.equipment.remove(moved_object)

        """
        This happens when character drops <= 0 HP. For Characters, this means rolling on
        the death table.

        """
        if self.location.allow_death:
            rules.dice.roll_death(self)
        else:
            self.location.msg_contents("|y$You() $conj(yield), beaten and out of the fight.|n")
            self.hp = self.hp_max

        """
        Called when character dies.

        """
            "|r$You() $conj(collapse) in a heap.\nDeath embraces you ...|n",
        )

    def at_pre_loot(self):
        """
        Called before allowing to loot. Return False to block enemy looting.
        """
        # don't allow looting in pvp
        return not self.location.allow_pvp

        """
        Called when being looted.

        """
        pass

    def add_xp(self, xp):
        """
        Add new XP.

        Args:
            xp (int): The amount of gained XP.

        Returns:
            bool: If a new level was reached or not.

        Notes:
            level 1 -> 2 = 1000 XP
            level 2 -> 3 = 2000 XP etc

        """
        self.xp += xp
        next_level_xp = self.level * self.xp_per_level
        return self.xp >= next_level_xp

    def level_up(self, *abilities):
        """
        Perform the level-up action.

        Args:
            *abilities (str): A set of abilities (like 'strength', 'dexterity' (normally 3)
                to upgrade by 1. Max is usually +10.
        Notes:
            We block increases above a certain value, but we don't raise an error here, that
            will need to be done earlier, when the user selects the ability to increase.

        """

        self.level += 1
        for ability in set(abilities[:3]):
            # limit to max amount allowed, each one unique
            try:
                # set at most to the max bonus
                current_bonus = getattr(self, ability)
                setattr(
                    self,
                    ability,
                    min(10, current_bonus + 1),
                )
                pass

        # update hp
        self.hp_max = max(self.max_hp + 1, rules.dice.roll(f"{self.level}d8"))

# character sheet visualization

_SHEET = """
 +----------------------------------------------------------------------------+
 | Name: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
 | STR: x2xxxxx  DEX: x3xxxxx  CON: x4xxxxx  WIS: x5xxxxx  CHA: x6xxxxx       |
 | HP: x7xxxxx                                      XP: x8xxxxx  Level: x9x   |
 | Desc: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
 | cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc |
 | cccccccccccccccccccccccccccccccccc1ccccccccccccccccccccccccccccccccccccccc |
    """

def get_character_sheet(character):
    """
    Generate a character sheet. This is grouped in a class in order to make
    it easier to override the look of the sheet.

    """

    def get(character):
        """
        Generate a character sheet from the character's stats.

        """
        equipment = character.equipment.all()
        # divide into chunks of max 10 length (to go into two columns)
        equipment_table = EvTable(
            table=[equipment[i : i + 10] for i in range(0, len(equipment), 10)]
        )
        form = EvForm({"FORMCHAR": "x", "TABLECHAR": "c", "SHEET": _SHEET})
        form.map(
            cells={
                1: character.key,
                2: f"+{character.strength}({character.strength + 10})",
                3: f"+{character.dexterity}({character.dexterity + 10})",
                4: f"+{character.constitution}({character.constitution + 10})",
                5: f"+{character.wisdom}({character.wisdom + 10})",
                6: f"+{character.charisma}({character.charisma + 10})",
                7: f"{character.hp}/{character.hp_max}",
                8: character.xp,
                9: character.level,
                "A": character.db.desc,
            },
            tables={
                1: equipment_table,
            },
        )
        return str(form)

"""
EvAdventure character generation.

"""

from .characters import EvAdventureCharacter
from .random_tables import chargen_tables
from .rules import dice

_ABILITIES = {
    "STR": "strength",
    "DEX": "dexterity",
    "CON": "constitution",
    "INT": "intelligence",
    "WIS": "wisdom",
    "CHA": "charisma",
}

_TEMP_SHEET = """
{name}

STR +{strength}
DEX +{dexterity}
CON +{constitution}
INT +{intelligence}
WIS +{wisdom}
CHA +{charisma}

{description}

Your belongings:
{equipment}
"""

class TemporaryCharacterSheet:
    """
    This collects all the rules for generating a new character. An instance of this class is used
    to pass around the current character state during character generation and also applied to
    the character at the end. This class instance can also be saved on the menu to make sure a user
    is not losing their half-created character.

    Note:
        In standard Knave, the character's attribute bonus is rolled randomly and will give a
        value 1-6; and there is no guarantee for 'equal' starting characters.

        Knave uses a d8 roll to get the initial hit points. We will follow the recommendation
        from the rule that we will use a minimum of 5 HP.

        We *will* roll random start equipment though. Contrary to standard Knave, we'll also
        randomly assign the starting weapon among a small selection of equal-dmg weapons (since
        there is no GM to adjudicate a different choice).

    """

    def _random_ability(self):
        return min(dice.roll("1d6"), dice.roll("1d6"), dice.roll("1d6"))

        # name will likely be modified later
        self.name = dice.roll_random_table("1d282", chargen_tables["name"])

        # base attribute values
        self.strength = self._random_ability()
        self.dexterity = self._random_ability()
        self.constitution = self._random_ability()
        self.intelligence = self._random_ability()
        self.wisdom = self._random_ability()
        self.charisma = self._random_ability()

        # physical attributes (only for rp purposes)
        physique = dice.roll_random_table("1d20", chargen_tables["physique"])
        face = dice.roll_random_table("1d20", chargen_tables["face"])
        skin = dice.roll_random_table("1d20", chargen_tables["skin"])
        hair = dice.roll_random_table("1d20", chargen_tables["hair"])
        clothing = dice.roll_random_table("1d20", chargen_tables["clothing"])
        speech = dice.roll_random_table("1d20", chargen_tables["speech"])
        virtue = dice.roll_random_table("1d20", chargen_tables["virtue"])
        vice = dice.roll_random_table("1d20", chargen_tables["vice"])
        background = dice.roll_random_table("1d20", chargen_tables["background"])
        misfortune = dice.roll_random_table("1d20", chargen_tables["misfortune"])
        alignment = dice.roll_random_table("1d20", chargen_tables["alignment"])

        self.ability_changes = 0
        self.desc = (
            f"You are {physique} with a {face} face, {skin} skin, {hair} hair, {speech} speech, and"
            f" {clothing} clothing. You were a {background.title()}, but you were {misfortune} and"
            f" ended up a knave. You are {virtue} but also {vice}. You tend towards {alignment}."
        )

        # same for all
        self.hp_max = max(5, dice.roll("1d8"))
        self.hp = self.hp_max

        # random equipment
        self.armor = dice.roll_random_table("1d20", chargen_tables["armor"])

        _helmet_and_shield = dice.roll_random_table("1d20", chargen_tables["helmets and shields"])
        self.helmet = "helmet" if "helmet" in _helmet_and_shield else "none"
        self.shield = "shield" if "shield" in _helmet_and_shield else "none"

        self.weapon = dice.roll_random_table("1d20", chargen_tables["starting weapon"])

        self.backpack = [
            "ration",
            "ration",
            dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
            dice.roll_random_table("1d20", chargen_tables["general gear 1"]),
            dice.roll_random_table("1d20", chargen_tables["general gear 2"]),
        ]

    def show_sheet(self):
        """
        Show a temp character sheet, a compressed version of the real thing.

        """
        equipment = (
            str(item)
            for item in [self.armor, self.helmet, self.shield, self.weapon] + self.backpack
            if item
        )

        return _TEMP_SHEET.format(
            name=self.name,
            strength=self.strength,
            dexterity=self.dexterity,
            constitution=self.constitution,
            intelligence=self.intelligence,
            wisdom=self.wisdom,
            charisma=self.charisma,
            description=self.desc,
            equipment=", ".join(equipment),
        )

    def apply(self, account):
        """
        Once the chargen is complete, call this create and set up the character.

        """

        start_location = ObjectDB.objects.get_id(settings.START_LOCATION)
        default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
        permissions = settings.PERMISSION_ACCOUNT_DEFAULT
        # creating character with given abilities
        new_character = create_object(
            EvAdventureCharacter,
            key=self.name,
            location=start_location,
            home=default_home,
            attributes=(
                ("strength", self.strength),
                ("dexterity", self.dexterity),
                ("constitution", self.constitution),
                ("intelligence", self.intelligence),
                ("wisdom", self.wisdom),
                ("charisma", self.wisdom),
                ("hp", self.hp),
                ("hp_max", self.hp_max),
                ("desc", self.desc),
            ),
        )

        new_character.locks.add(
            "puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer);delete:id(%i) or"
            " perm(Admin)" % (new_character.id, account.id, account.id)
        )
        # spawn equipment
        if self.weapon:
            weapon = spawn(self.weapon)
            new_character.equipment.move(weapon[0])
        if self.armor:
            armor = spawn(self.armor)
            new_character.equipment.move(armor[0])
        if self.shield:
            shield = spawn(self.shield)
            new_character.equipment.move(shield[0])
        if self.helmet:
            helmet = spawn(self.helmet)
            new_character.equipment.move(helmet[0])

        for item in self.backpack:
            item = spawn(item)
            new_character.equipment.move(item[0])

        return new_character

# chargen menu

def node_chargen(caller, raw_string, **kwargs):
    """
    This node is the central point of chargen. We return here to see our current
    sheet and break off to edit different parts of it.

    In Knave, not so much can be changed.
    """
    tmp_character = kwargs["tmp_character"]

    text = tmp_character.show_sheet()

    options = [{"desc": "Change your name", "goto": ("node_change_name", kwargs)}]
    if tmp_character.ability_changes <= 0:
        options.append(
            {
                "desc": "Swap two of your ability scores (once)",
                "goto": ("node_swap_abilities", kwargs),
            }
        )
    options.append(
        {"desc": "Accept and create character", "goto": ("node_apply_character", kwargs)},
    )

def _update_name(caller, raw_string, **kwargs):
    """
    Used by node_change_name below to check what user entered and update the name if appropriate.

    """
    if raw_string:
        tmp_character = kwargs["tmp_character"]
        tmp_character.name = raw_string.lower().capitalize()

    return "node_chargen", kwargs

def node_change_name(caller, raw_string, **kwargs):
    """
    Change the random name of the character.

    """

    text = (
        f"Your current name is |w{tmp_character.name}|n. Enter a new name or leave empty to abort."
    )

    options = {"key": "_default", "goto": (_update_name, kwargs)}

def _swap_abilities(caller, raw_string, **kwargs):
    """
    Used by node_swap_abilities to parse the user's input and swap ability
    values.

    """
        abi1, *abi2 = raw_string.split(" ", 1)
        if not abi2:
            caller.msg("That doesn't look right.")
            return None, kwargs
        abi2 = abi2[0]
        abi1, abi2 = abi1.upper().strip(), abi2.upper().strip()
        if abi1 not in _ABILITIES or abi2 not in _ABILITIES:
            caller.msg("Not a familiar set of abilites.")

        # looks okay = swap values. We need to convert STR to strength etc
        abi1 = _ABILITIES[abi1]
        abi2 = _ABILITIES[abi2]
        abival1 = getattr(tmp_character, abi1)
        abival2 = getattr(tmp_character, abi2)

        setattr(tmp_character, abi1, abival2)
        setattr(tmp_character, abi2, abival1)

        tmp_character.ability_changes += 1

def node_swap_abilities(caller, raw_string, **kwargs):
    """
    One is allowed to swap the values of two abilities around, once.

    """

    text = f"""
Your current abilities:

STR +{tmp_character.strength}
DEX +{tmp_character.dexterity}
CON +{tmp_character.constitution}
INT +{tmp_character.intelligence}
WIS +{tmp_character.wisdom}
CHA +{tmp_character.charisma}

You can swap the values of two abilities around.
You can only do this once, so choose carefully!

To swap the values of e.g.  STR and INT, write |wSTR INT|n. Empty to abort.
"""

    options = {"key": "_default", "goto": (_swap_abilities, kwargs)}

def node_apply_character(caller, raw_string, **kwargs):
    """
    End chargen and create the character. We will also puppet it.

    """
    new_character = tmp_character.apply(caller)
    caller.characters.add(new_character)

    text = "Character created!"

    return text, None

def start_chargen(caller, session=None):
    """
    This is a start point for spinning up the chargen from a command later.

    """

        "node_chargen": node_chargen,
        "node_change_name": node_change_name,
        "node_swap_abilities": node_swap_abilities,
        "node_apply_character": node_apply_character,
    }

    # this generates all random components of the character
    tmp_character = TemporaryCharacterSheet()

    EvMenu(
        caller,
        menutree,
        startnode="node_chargen",
        session=session,
        startnode_input=("sgsg", {"tmp_character": tmp_character}),
    )

"""
EvAdventure Base combat utilities.

This establishes the basic building blocks for combat:

- `CombatFailure` - exception for combat-specific errors.
- `CombatAction` (and subclasses) - classes encompassing all the working around an action.
  They are initialized from 'action-dicts` - dictionaries with all the relevant data for the
  particular invocation
- `CombatHandler` - base class for running a combat. Exactly how this is used depends on the
  type of combat intended (twitch- or turn-based) so many details of this will be implemented
  in child classes.

----

"""

from evennia.utils import evtable

class CombatFailure(RuntimeError):
    """
    Some failure during combat actions.

    """

class CombatAction:
    """
    Parent class for all actions.

    This represents the executable code to run to perform an action. It is initialized from an
    'action-dict', a set of properties stored in the action queue by each combatant.

    """

    def __init__(self, combathandler, combatant, action_dict):
        """
        Each key-value pair in the action-dict is stored as a property on this class
        for later access.

        Args:
            combatant (EvAdventureCharacter, EvAdventureNPC): The combatant performing
                the action.
            action_dict (dict): A dict containing all properties to initialize on this
                class. This should not be any keys with `_` prefix, since these are
                used internally by the class.

        """
        self.combathandler = combathandler
        self.combatant = combatant

        # store the action dicts' keys as properties accessible as e.g. action.target etc
        for key, val in action_dict.items():
            if not key.startswith("_"):
                setattr(self, key, val)

    def msg(self, message, broadcast=True):
        """
        Convenience route to the combathandler msg-sender mechanism.

        Args:
            message (str): Message to send; use `$You()` and `$You(other.key)` to refer to
                the combatant doing the action and other combatants, respectively.

        """
        self.combathandler.msg(message, combatant=self.combatant, broadcast=broadcast)

    def can_use(self):
        """
        Called to determine if the action is usable with the current settings. This does not
        actually perform the action.

        Returns:
            bool: If this action can be used at this time.

        """

    def execute(self):
        """
        Perform the action as the combatant. Should normally make use of the properties
        stored on the class during initialization.

        """
        pass

    def post_execute(self):
        """
        Called after execution.
        """
        pass

class CombatActionHold(CombatAction):
    """
    Action that does nothing.
    ::
        action_dict = {
                "key": "hold"
            }
    """

class CombatActionAttack(CombatAction):
    """
    A regular attack, using a wielded weapon.
    ::
        action-dict = {
                "key": "attack",
                "target": Character/Object
            }
    """

        attacker = self.combatant
        weapon = attacker.weapon

        if weapon.at_pre_use(attacker, target):
            weapon.use(
                attacker, target, advantage=self.combathandler.has_advantage(attacker, target)
            )
            weapon.at_post_use(attacker, target)

class CombatActionStunt(CombatAction):
    """
    Perform a stunt the grants a beneficiary (can be self) advantage on their next action against a
    target. Whenever performing a stunt that would affect another negatively (giving them
    disadvantage against an ally, or granting an advantage against them, we need to make a check
    first. We don't do a check if giving an advantage to an ally or ourselves.
    ::
               "key": "stunt",
               "recipient": Character/NPC,
               "target": Character/NPC,
               "advantage": bool,  # if False, it's a disadvantage
               "stunt_type": Ability,  # what ability (like STR, DEX etc) to use to perform this stunt.
               "defense_type": Ability, # what ability to use to defend against (negative) effects of
                this stunt.
            }

    """

        combathandler = self.combathandler
        recipient = self.recipient  # the one to receive the effect of the stunt
        target = self.target  # the affected by the stunt (can be the same as recipient/combatant)
        txt = ""

        if recipient == target:
            # grant another entity dis/advantage against themselves
            defender = recipient
        else:
            # recipient not same as target; who will defend depends on disadvantage or advantage
            # to give.
            defender = target if self.advantage else recipient

        # trying to give advantage to recipient against target. Target defends against caller
        is_success, _, txt = rules.dice.opposed_saving_throw(
            attacker,
            defender,
            attack_type=self.stunt_type,
            defense_type=self.defense_type,
            advantage=combathandler.has_advantage(attacker, defender),
            disadvantage=combathandler.has_disadvantage(attacker, defender),
        )

        self.msg(f"$You() $conj(attempt) stunt on $You({defender.key}). {txt}")

        # deal with results
        if is_success:
            if self.advantage:
                combathandler.give_advantage(recipient, target)
            else:
                combathandler.give_disadvantage(recipient, target)
            if recipient == self.combatant:
                self.msg(
                    f"$You() $conj(gain) {'advantage' if self.advantage else 'disadvantage'} "
                    f"against $You({target.key})!"
                )
            else:
                self.msg(
                    f"$You() $conj(cause) $You({recipient.key}) "
                    f"to gain {'advantage' if self.advantage else 'disadvantage'} "
                )
        else:
            self.msg(f"$You({defender.key}) $conj(resist)! $You() $conj(fail) the stunt.")

class CombatActionUseItem(CombatAction):
    """
    Use an item in combat. This is meant for one-off or limited-use items (so things like
    scrolls and potions, not swords and shields). If this is some sort of weapon or spell rune,
    we refer to the item to determine what to use for attack/defense rolls.
    ::
                "key": "use",
                "item": Object
                "target": Character/NPC/Object/None
            }
    """

        item = self.item
        user = self.combatant

        if item.at_pre_use(user, target):
            item.use(
                user,
                target,
                advantage=self.combathandler.has_advantage(user, target),
                disadvantage=self.combathandler.has_disadvantage(user, target),
            )
            item.at_post_use(user, target)

class CombatActionWield(CombatAction):
    """
    Wield a new weapon (or spell) from your inventory. This will swap out the one you are currently
    wielding, if any.
    ::
                "key": "wield",
            }
    """

        self.combatant.equipment.move(self.item)
        self.msg(f"$You() $conj(wield) $You({self.item.key}).")

# main combathandler

class EvAdventureCombatBaseHandler(DefaultScript):
    """
    This script is created when a combat starts. It 'ticks' the combat and tracks
    all sides of it.

    """

    # available actions in combat
    action_classes = {
        "hold": CombatActionHold,
        "attack": CombatActionAttack,
        "stunt": CombatActionStunt,
        "use": CombatActionUseItem,
        "wield": CombatActionWield,
    }

    # fallback action if not selecting anything
    fallback_action_dict = AttributeProperty({"key": "hold"}, autocreate=False)

    def get_or_create_combathandler(cls, obj, **kwargs):
        """
        Get or create a combathandler on `obj`.

        Args:
            obj (any): The Typeclassed entity to store the CombatHandler Script on. This could be
                a location (for turn-based combat) or a Character (for twitch-based combat).
            combathandler_key (str): They key name for the script. Will be 'combathandler' by
                default.
            **kwargs: Arguments to the Script, if it is created.

        """
        if not obj:
            raise CombatFailure("Cannot start combat without a place to do it!")

        combathandler_key = kwargs.pop("key", "combathandler")
        combathandler = obj.ndb.combathandler
        if not combathandler or not combathandler.id:
            combathandler = obj.scripts.get(combathandler_key).first()
            if not combathandler:
                # have to create from scratch
                persistent = kwargs.pop("persistent", True)
                combathandler = create_script(
                    cls,
                    key=combathandler_key,
                    obj=obj,
                    persistent=persistent,
                    autostart=False,
                    **kwargs,
                )
            obj.ndb.combathandler = combathandler
        return combathandler

    def msg(self, message, combatant=None, broadcast=True, location=None):
        """
        Central place for sending messages to combatants. This allows
        for adding any combat-specific text-decoration in one place.

        Args:
            combatant (Object): The 'You' in the message, if any.
            broadcast (bool): If `False`, `combatant` must be included and
                will be the only one to see the message. If `True`, send to
                everyone in the location.
            location (Object, optional): If given, use this as the location to
                send broadcast messages to. If not, use `self.obj` as that
                location.

        Notes:
            If `combatant` is given, use `$You/you()` markup to create
            a message that looks different depending on who sees it. Use
            `$You(combatant_key)` to refer to other combatants.

        """
            location = self.obj

        location_objs = location.contents

        exclude = []
        if not broadcast and combatant:
            exclude = [obj for obj in location_objs if obj is not combatant]

        location.msg_contents(
            message,
            exclude=exclude,
            from_obj=combatant,
            mapping={locobj.key: locobj for locobj in location_objs},
        )

    def get_combat_summary(self, combatant):
        """
        Get a 'battle report' - an overview of the current state of combat from the perspective
        of one of the sides.

        Args:
            combatant (EvAdventureCharacter, EvAdventureNPC): The combatant to get.

        Returns:
            EvTable: A table representing the current state of combat.

        Example:
        ::

                                        Goblin shaman (Perfect)
        Gregor (Hurt)                   Goblin brawler(Hurt)
        Bob (Perfect)         vs        Goblin grunt 1 (Hurt)
                                        Goblin grunt 2 (Perfect)
                                        Goblin grunt 3 (Wounded)

        """
        allies, enemies = self.get_sides(combatant)
        nallies, nenemies = len(allies), len(enemies)

        # prepare colors and hurt-levels
        allies = [f"{ally} ({ally.hurt_level})" for ally in allies]
        enemies = [f"{enemy} ({enemy.hurt_level})" for enemy in enemies]

        # the center column with the 'vs'
        vs_column = ["" for _ in range(max(nallies, nenemies))]
        vs_column[len(vs_column) // 2] = "|wvs|n"

        # the two allies / enemies columns should be centered vertically
        diff = abs(nallies - nenemies)
        top_empty = diff // 2
        bot_empty = diff - top_empty
        topfill = ["" for _ in range(top_empty)]
        botfill = ["" for _ in range(bot_empty)]

        if nallies >= nenemies:
            enemies = topfill + enemies + botfill
        else:
            allies = topfill + allies + botfill

        # make a table with three columns
        return evtable.EvTable(
            table=[
                evtable.EvColumn(*allies, align="l"),
                evtable.EvColumn(*vs_column, align="c"),
                evtable.EvColumn(*enemies, align="r"),
            ],
            border=None,
            maxwidth=78,
        )

    def get_sides(self, combatant):
        """
        Get a listing of the two 'sides' of this combat, from the perspective of the provided
        combatant. The sides don't need to be balanced.

        Args:
            combatant (Character or NPC): The one whose sides are to determined.

        Returns:
            tuple: A tuple of lists `(allies, enemies)`, from the perspective of `combatant`.

        Note:
            The sides are found by checking PCs vs NPCs. PCs can normally not attack other PCs, so
            are naturally allies. If the current room has the `allow_pvp` Attribute set, then _all_
            other combatants (PCs and NPCs alike) are considered valid enemies (one could expand
            this with group mechanics).

        """
        raise NotImplementedError

    def give_advantage(self, recipient, target):
        """
        Let a benefiter gain advantage against the target.

        Args:
            recipient (Character or NPC): The one to gain the advantage. This may or may not
                be the same entity that creates the advantage in the first place.
            target (Character or NPC): The one against which the target gains advantage. This
                could (in principle) be the same as the benefiter (e.g. gaining advantage on
                some future boost)

        """

    def give_disadvantage(self, recipient, target):
        """
        Let an affected party gain disadvantage against a target.

        Args:
            recipient (Character or NPC): The one to get the disadvantage.
            target (Character or NPC): The one against which the target gains disadvantage, usually
            an enemy.

        """

    def has_advantage(self, combatant, target):
        """
        Check if a given combatant has advantage against a target.

        Args:
            combatant (Character or NPC): The one to check if they have advantage
            target (Character or NPC): The target to check advantage against.

        """

    def has_disadvantage(self, combatant, target):
        """
        Check if a given combatant has disadvantage against a target.

        Args:
            combatant (Character or NPC): The one to check if they have disadvantage
            target (Character or NPC): The target to check disadvantage against.

        """

    def queue_action(self, action_dict, combatant=None):
        """
        Queue an action by adding the new actiondict.

        Args:
            action_dict (dict): A dict describing the action class by name along with properties.
            combatant (EvAdventureCharacter, EvAdventureNPC, optional): A combatant queueing the
                action.

        """

    def execute_next_action(self, combatant):
        """
        Perform a combatant's next action.

        Args:
            combatant (EvAdventureCharacter, EvAdventureNPC): The combatant performing and action.

        """

    def start_combat(self):
        """
        Start combat.

        """

    def check_stop_combat(self):
        """
        Check if this combat should be aborted, whatever this means for the particular
        the particular combat type.

            kwargs: Any extra keyword args used.

        Returns:
            bool: If `True`, the `stop_combat` method should be called.

        """

    def stop_combat(self):
        """
        Stop combat. This should also do all cleanup.
        """

"""
EvAdventure Turn-based combat

This implements a turn-based (Final Fantasy, etc) style of MUD combat.

In this variation, all combatants are sharing the same combat handler, sitting on the current room.
The user will receive a menu of combat options and each combatat has a certain time time (e.g. 30s)
to select their next action or do nothing. To speed up play, as soon as everyone in combat selected
their next action, the next turn runs immediately, regardless of the timeout.

With this example, all chosen combat actions are considered to happen at the same time (so you are
able to kill and be killed in the same turn).

Unlike in twitch-like combat, there is no movement while in turn-based combat. Fleeing is a select
action that takes several vulnerable turns to complete.

----

"""

from evennia import AttributeProperty, CmdSet, Command, EvMenu
from evennia.utils import inherits_from, list_to_string

from .combat_base import (
    CombatAction,
    CombatActionAttack,
    CombatActionHold,
    CombatActionStunt,
    CombatActionUseItem,
    CombatActionWield,
    EvAdventureCombatBaseHandler,
)
from .enums import Ability

# turnbased-combat needs the flee action too
class CombatActionFlee(CombatAction):
    """
    Start (or continue) fleeing/disengaging from combat.

    action_dict = {
           "key": "flee",
        }

    Note:
        Refer to as 'flee'.

    """

        if self.combatant not in combathandler.fleeing_combatants:
            # we record the turn on which we started fleeing
            combathandler.fleeing_combatants[self.combatant] = self.combathandler.turn

        # show how many turns until successful flight
        current_turn = combathandler.turn
        started_fleeing = combathandler.fleeing_combatants[self.combatant]
        flee_timeout = combathandler.flee_timeout
        time_left = flee_timeout - (current_turn - started_fleeing) - 1

        if time_left > 0:
            self.msg(
                "$You() $conj(retreat), being exposed to attack while doing so (will escape in "
                f"{time_left} $pluralize(turn, {time_left}))."
            )

class EvAdventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
    """
    A version of the combathandler, handling turn-based combat.

    """

        "flee": CombatActionFlee,
    }

    # how many turns you must be fleeing before escaping
    flee_timeout = AttributeProperty(1, autocreate=False)

    # persistent storage

    turn = AttributeProperty(0)
    # who is involved in combat, and their queued action
    # as {combatant: actiondict, ...}
    combatants = AttributeProperty(dict)

    # who has advantage against whom
    advantage_matrix = AttributeProperty(defaultdict(dict))
    disadvantage_matrix = AttributeProperty(defaultdict(dict))

    fleeing_combatants = AttributeProperty(dict)
    defeated_combatants = AttributeProperty(list)

    # usable script properties
    # .is_active - show if timer is running

    def give_advantage(self, combatant, target):
        """

        Args:
            combatant (Character or NPC): The one to gain the advantage. This may or may not

        """
        self.advantage_matrix[combatant][target] = True

    def give_disadvantage(self, combatant, target, **kwargs):
        """

        Args:
                an enemy.

        """
        self.disadvantage_matrix[combatant][target] = True

    def has_advantage(self, combatant, target, **kwargs):
        """

        Args:

        """
        return target in self.fleeing_combatants or bool(
            self.advantage_matrix[combatant].pop(target, False)
        )

        """

        Args:

        """
        return bool(self.disadvantage_matrix[combatant].pop(target, False))

    def add_combatant(self, combatant):
        """
        Add a new combatant to the battle. Can be called multiple times safely.

        Args:
            combatant (EvAdventureCharacter, EvAdventureNPC): Any number of combatants to add to
                the combat.
        Returns:
            bool: If this combatant was newly added or not (it was already in combat).

        """
        if combatant not in self.combatants:
            self.combatants[combatant] = self.fallback_action_dict

    def remove_combatant(self, combatant):
        """
        Remove a combatant from the battle. This removes their queue.

        Args:
            combatant (EvAdventureCharacter, EvAdventureNPC): A combatant to add to

        """
        self.combatants.pop(combatant, None)
        # clean up menu if it exists
        if combatant.ndb._evmenu:
            combatant.ndb._evmenu.close_menu()

    def start_combat(self, **kwargs):
        """
        This actually starts the combat. It's safe to run this multiple times
        since it will only start combat if it isn't already running.

        """
        if not self.is_active:
            self.start(**kwargs)

        """
        Stop the combat immediately.

        """
        for combatant in self.combatants:
            self.remove_combatant(combatant)
        self.stop()

        """Add your next queued action to summary"""
        summary = super().get_combat_summary(combatant)
        next_action = self.get_next_action_dict(combatant) or {"key": "hold"}
        next_repeat = self.time_until_next_repeat()

        summary = (
            f"{summary}\n Your queued action: [|b{next_action['key']}|n] (|b{next_repeat}s|n until"
            " next round,\n or until all combatants have chosen their next action)."
        )
        return summary

        """

        Args:

        Returns:

        Note:

        """
        if self.obj.allow_pvp:
            # in pvp, everyone else is an ememy
            allies = [combatant]
            enemies = [comb for comb in self.combatants if comb != combatant]
        else:
            # otherwise, enemies/allies depend on who combatant is
            pcs = [comb for comb in self.combatants if inherits_from(comb, EvAdventureCharacter)]
            npcs = [comb for comb in self.combatants if comb not in pcs]
            if combatant in pcs:
                # combatant is a PC, so NPCs are all enemies
                allies = pcs
                enemies = npcs
            else:
                # combatant is an NPC, so PCs are all enemies
                allies = npcs
                enemies = pcs
        return allies, enemies

    def queue_action(self, combatant, action_dict):
        """

        Args:
            combatant (EvAdventureCharacter, EvAdventureNPC): A combatant queueing the action.

        """
        self.combatants[combatant] = action_dict

        # track who inserted actions this turn (non-persistent)
        did_action = set(self.ndb.did_action or set())
        did_action.add(combatant)
        if len(did_action) >= len(self.combatants):
            # everyone has inserted an action. Start next turn without waiting!
            self.force_repeat()

    def get_next_action_dict(self, combatant):
        """
        Give the action_dict for the next action that will be executed.

        Args:
            combatant (EvAdventureCharacter, EvAdventureNPC): The combatant to get the action for.

        Returns:
            dict: The next action-dict in the queue.

        """
        return self.combatants.get(combatant, self.fallback_action_dict)

        """
        Perform a combatant's next queued action. Note that there is _always_ an action queued,
        even if this action is 'hold', which means the combatant will do nothing.

        Args:

        """
        # this gets the next dict and rotates the queue
        action_dict = self.combatants.get(combatant, self.fallback_action_dict)

        # use the action-dict to select and create an action from an action class
        action_class = self.action_classes[action_dict["key"]]
        action = action_class(self, combatant, action_dict)

        action.execute()
        action.post_execute()

        if action_dict.get("repeat", False):
            # queue the action again *without updating the *.ndb.did_action list* (otherwise
            # we'd always auto-end the turn if everyone used repeating actions and there'd be
            # no time to change it before the next round)
            self.combatants[combatant] = action_dict
        else:
            # if not a repeat, set the fallback action

        """Check if it's time to stop combat"""

        # check if anyone is defeated
        for combatant in list(self.combatants.keys()):
            if combatant.hp <= 0:
                # PCs roll on the death table here, NPCs die. Even if PCs survive, they
                # are still out of the fight.
                combatant.at_defeat()
                self.combatants.pop(combatant)
                self.defeated_combatants.append(combatant)
                self.msg("|r$You() $conj(fall) to the ground, defeated.|n", combatant=combatant)

        # check if anyone managed to flee
        flee_timeout = self.flee_timeout
        for combatant, started_fleeing in self.fleeing_combatants.items():
            if self.turn - started_fleeing >= flee_timeout - 1:
                # if they are still alive/fleeing and have been fleeing long enough, escape
                self.msg("|y$You() successfully $conj(flee) from combat.|n", combatant=combatant)
                self.remove_combatant(combatant)

        # check if one side won the battle
        if not self.combatants:
            # noone left in combat - maybe they killed each other or all fled
            surviving_combatant = None
            allies, enemies = (), ()
        else:
            # grab a random survivor and check if they have any living enemies.
            surviving_combatant = random.choice(list(self.combatants.keys()))
            allies, enemies = self.get_sides(surviving_combatant)

        if not enemies:
            # if one way or another, there are no more enemies to fight
            still_standing = list_to_string(f"$You({comb.key})" for comb in allies)
            knocked_out = list_to_string(comb for comb in self.defeated_combatants if comb.hp > 0)
            killed = list_to_string(comb for comb in self.defeated_combatants if comb.hp <= 0)

            if still_standing:
                txt = [f"The combat is over. {still_standing} are still standing."]
            else:
                txt = ["The combat is over. No-one stands as the victor."]
            if knocked_out:
                txt.append(f"{knocked_out} were taken down, but will live.")
            if killed:
                txt.append(f"{killed} were killed.")
            self.msg(txt)
            self.stop_combat()

        """
        This method is called every time Script repeats (every `interval` seconds). Performs a full
        turn of combat, performing everyone's actions in random order.

        """
        self.turn += 1
        # random turn order
        combatants = list(self.combatants.keys())
        random.shuffle(combatants)  # shuffles in place

        # do everyone's next queued combat action
        for combatant in combatants:
            self.execute_next_action(combatant)

        self.ndb.did_action = set()

        self.check_stop_combat()

# -----------------------------------------------------------------------------------
#
# Turn-based combat (Final Fantasy style), using a menu
#
# Activate by adding the CmdTurnCombat command to Character cmdset, then
# use it to attack a target.
#

def _get_combathandler(caller, turn_timeout=30, flee_time=3, combathandler_key="combathandler"):
    """
    Get the combat handler for the caller's location. If it doesn't exist, create it.

    Args:
        caller (EvAdventureCharacter or EvAdventureNPC): The character/NPC to get the
            combat handler for.
        turn_timeout (int): After this time, the turn will roll around.
        flee_time (int): How many turns it takes to flee.

    """
    return EvAdventureTurnbasedCombatHandler.get_or_create_combathandler(
        caller.location,
        interval=turn_timeout,
        attributes=[("flee_time", flee_time)],
        key=combathandler_key,
    )

def _queue_action(caller, raw_string, **kwargs):
    """
    Goto-function that queue the action with the CombatHandler. This always returns
    to the top-level combat menu "node_combat"
    """
    action_dict = kwargs["action_dict"]
    _get_combathandler(caller).queue_action(caller, action_dict)
    return "node_combat"

def _rerun_current_node(caller, raw_string, **kwargs):
    return None, kwargs

def _get_default_wizard_options(caller, **kwargs):
    """
    Get the standard wizard options for moving back/forward/abort. This can be appended to
    the end of other options.

    """

    return [
        {"key": ("back", "b"), "goto": (_step_wizard, {**kwargs, **{"step": "back"}})},
        {"key": ("abort", "a"), "goto": "node_combat"},
        {
            "key": "_default",
            "goto": (_rerun_current_node, kwargs),
        },
    ]

def _step_wizard(caller, raw_string, **kwargs):
    """
    Many options requires stepping through several steps, wizard style. This
    will redirect back/forth in the sequence.

    E.g. Stunt boost -> Choose ability to boost -> Choose recipient -> Choose target -> queue

    """
    steps = kwargs.get("steps", [])
    nsteps = len(steps)
    istep = kwargs.get("istep", -1)
    # one of abort, back, forward
    step_direction = kwargs.get("step", "forward")

    if step_direction == "back":
        # step back in wizard
        if istep <= 0:
            return "node_combat"
        istep = kwargs["istep"] = istep - 1
        return steps[istep], kwargs
    else:
        # step to the next step in wizard
        if istep >= nsteps - 1:
            # we are already at end of wizard - queue action!
            return _queue_action(caller, raw_string, **kwargs)
        else:
            # step forward
            istep = kwargs["istep"] = istep + 1
            return steps[istep], kwargs

def node_choose_enemy_target(caller, raw_string, **kwargs):
    """
    Choose an enemy as a target for an action
    """
    text = "Choose an enemy to target."

    combathandler = _get_combathandler(caller)

    _, enemies = combathandler.get_sides(caller)

    options = [
        {
            "desc": target.get_display_name(caller),
            "goto": (
                _step_wizard,
                {**kwargs, **{"action_dict": {**action_dict, **{"target": target}}}},
            ),
        }
        for target in enemies
    ]
    options.extend(_get_default_wizard_options(caller, **kwargs))

def node_choose_enemy_recipient(caller, raw_string, **kwargs):
    """
    Choose an enemy as a 'recipient' for an action.
    """
    text = "Choose an enemy as a recipient."

    options = [
        {
            "goto": (
                {**kwargs, **{"action_dict": {**action_dict, **{"recipient": target}}}},
            ),
        }
    ]

def node_choose_allied_target(caller, raw_string, **kwargs):
    """
    """
    text = "Choose an ally to target."

    allies, _ = combathandler.get_sides(caller)

    options.extend(
        [
            {
                "desc": target.get_display_name(caller),
                "goto": (
                    _step_wizard,
                    {
                        **kwargs,
                        **{"action_dict": {**action_dict, **{"target": target}}},
                    },
                ),
            }
            for target in allies
        ]
    )

def node_choose_allied_recipient(caller, raw_string, **kwargs):
    """
    Choose an allied recipient for an action
    """
    text = "Choose an ally as a recipient."

        [
            {
                "goto": (
                    {
                        **kwargs,
                        **{
                            "action_dict": {
                                **action_dict,
                                **{"recipient": target},
                            }
                        },
                    },
                ),
            }
        ]
    )

def node_choose_ability(caller, raw_string, **kwargs):
    """
    Select an ability to use/boost etc.
    """
    text = "Choose the ability to apply"

    options = [
        {
            "desc": abi.value,
            "goto": (
                {
                    **kwargs,
                    **{
                        "action_dict": {**action_dict, **{"stunt_type": abi, "defense_type": abi}},
                    },
                },
            ),
        }
        for abi in (
            Ability.STR,
            Ability.DEX,
            Ability.CON,
            Ability.INT,
            Ability.WIS,
            Ability.CHA,
        )
    ]

def node_choose_use_item(caller, raw_string, **kwargs):
    """
    Choose item to use.

    """
    text = "Select the item"

    options = [
        {
            "desc": item.get_display_name(caller),
            "goto": (
                {**kwargs, **{"action_dict": {**action_dict, **{"item": item}}}},
            ),
        }
        for item in caller.equipment.get_usable_objects_from_backpack()
    ]
    if not options:
        text = "There are no usable items in your inventory!"

def node_choose_wield_item(caller, raw_string, **kwargs):
    """

    """

    options = [
        {
            "goto": (
            ),
        }
        for item in caller.equipment.get_wieldable_objects_from_backpack()
    ]
        text = "There are no items in your inventory that you can wield!"

def node_combat(caller, raw_string, **kwargs):
    """Base combat menu"""

    text = combathandler.get_combat_summary(caller)
    options = [
        {
            "desc": "attack an enemy",
            "goto": (
                {
                    "steps": ["node_choose_enemy_target"],
                    "action_dict": {"key": "attack", "target": None, "repeat": True},
                },
            ),
        },
        {
            "desc": "Stunt - gain a later advantage against a target",
            "goto": (
                {
                    "steps": [
                        "node_choose_ability",
                        "node_choose_enemy_target",
                        "node_choose_allied_recipient",
                    ],
                    "action_dict": {"key": "stunt", "advantage": True},
                },
            ),
        },
        {
            "desc": "Stunt - give an enemy disadvantage against yourself or an ally",
            "goto": (
                {
                    "steps": [
                        "node_choose_enemy_recipient",
                        "node_choose_allied_target",
                    ],
                    "action_dict": {"key": "stunt", "advantage": False},
                },
            ),
        },
        {
            "desc": "Use an item on yourself or an ally",
            "goto": (
                {
                    "steps": ["node_choose_use_item", "node_choose_allied_target"],
                    "action_dict": {"key": "use", "item": None, "target": None},
                },
            ),
        },
        {
            "desc": "Use an item on an enemy",
            "goto": (
                {
                    "steps": ["node_choose_use_item", "node_choose_enemy_target"],
                },
            ),
        },
        {
            "desc": "Wield/swap with an item from inventory",
            "goto": (
                {
                    "steps": ["node_choose_wield_item"],
                    "action_dict": {"key": "wield", "item": None},
                },
            ),
        },
        {
            "desc": "flee!",
            "goto": (_queue_action, {"action_dict": {"key": "flee", "repeat": True}}),
        },
        {
            "desc": "hold, doing nothing",
            "goto": (_queue_action, {"action_dict": {"key": "hold"}}),
        },
        {
            "goto": "node_combat",
        },
    ]

# Add this command to the Character cmdset to make turn-based combat available.

class CmdTurnAttack(Command):
    """
    Start or join combat.

    Usage:
      attack [<target>]

    """

    aliases = ["hit", "turnbased combat"]

    turn_timeout = 30  # seconds
    flee_time = 3  # rounds

            self.msg("What are you attacking?")
            return

            return

        if not hasattr(target, "hp"):
            self.msg("You can't attack that.")
            return
        elif target.hp <= 0:
            self.msg(f"{target.get_display_name(self.caller)} is already down.")
            return

        if target.is_pc and not target.location.allow_pvp:
            self.msg("PvP combat is not allowed here!")
            return

        combathandler = _get_combathandler(self.caller, self.turn_timeout, self.flee_time)

        # add combatants to combathandler. this can be done safely over and over
        combathandler.add_combatant(self.caller)
        combathandler.queue_action(self.caller, {"key": "attack", "target": target})
        combathandler.add_combatant(target)
        target.msg("|rYou are attacked by {self.caller.get_display_name(self.caller)}!|n")
        combathandler.start_combat()

        # build and start the menu
        EvMenu(
            {
                "node_choose_enemy_target": node_choose_enemy_target,
                "node_choose_allied_target": node_choose_allied_target,
                "node_choose_enemy_recipient": node_choose_enemy_recipient,
                "node_choose_allied_recipient": node_choose_allied_recipient,
                "node_choose_ability": node_choose_ability,
                "node_choose_use_item": node_choose_use_item,
                "node_choose_wield_item": node_choose_wield_item,
                "node_combat": node_combat,
            },
            startnode="node_combat",
            combathandler=combathandler,
            auto_look=False,
            # cmdset_mergetype="Union",
        )

class TurnCombatCmdSet(CmdSet):
    """
    CmdSet for the turn-based combat.
    """

        self.add(CmdTurnAttack())

"""
EvAdventure Twitch-based combat

This implements a 'twitch' (aka DIKU or other traditional muds) style of MUD combat.

----

"""
from evennia import AttributeProperty, CmdSet, default_cmds
    pad,
    repeat,
    unrepeat,
)

)
from .enums import ABILITY_REVERSE_MAP

class EvAdventureCombatTwitchHandler(EvAdventureCombatBaseHandler):
    """
    This is created on the combatant when combat starts. It tracks only the combatants
    side of the combat and handles when the next action will happen.

    """

    # fixed properties
    }

    # dynamic properties

    advantage_against = AttributeProperty(dict)
    disadvantage_against = AttributeProperty(dict)

    action_dict = AttributeProperty(dict)
    fallback_action_dict = AttributeProperty({"key": "hold", "dt": 0})

    # stores the current ticker reference, so we can manipulate it later
    current_ticker_ref = AttributeProperty(None)

    def msg(self, message, broadcast=True, **kwargs):
        """

        Args:
                location.

        Notes:
        """
        super().msg(message, combatant=self.obj, broadcast=broadcast, location=self.obj.location)

        self.obj.cmdset.add(TwitchLookCmdSet, persistent=False)

        """

        Args:

        Returns:
                Note that combatant itself is not included in either of these.

        """
        # get all entities involved in combat by looking up their combathandlers
        combatants = [
            comb
            for comb in self.obj.location.contents
            if hasattr(comb, "scripts") and comb.scripts.has(self.key)
        ]
        location = self.obj.location

        if hasattr(location, "allow_pvp") and location.allow_pvp:
            # in pvp, everyone else is an enemy
            enemies = [comb for comb in combatants if comb != combatant]
        else:
            pcs = [comb for comb in combatants if inherits_from(comb, EvAdventureCharacter)]
            npcs = [comb for comb in combatants if comb not in pcs]
            else:

        """

        Args:

        """
        self.advantage_against[target] = True

        """

        Args:
                an enemy.

        """
        self.disadvantage_against[target] = True

        """

        Args:

        """
        return self.advantage_against.get(target, False)

        """

        Args:

        """
        return self.disadvantage_against.get(target, False)

        """
        Schedule the next action to fire.

        Args:
            action_dict (dict): The new action-dict to initialize.
            combatant: Unused.

        """
        if action_dict["key"] not in self.action_classes:
            self.obj.msg("This is an unkown action!")
            return

        # store action dict and schedule it to run in dt time
        self.action_dict = action_dict
        dt = action_dict.get("dt", 0)

        if self.current_ticker_ref:
            # we already have a current ticker going - abort it
            unrepeat(self.current_ticker_ref)
        if dt <= 0:
            # no repeat
            self.current_ticker_ref = None
        else:
            # always schedule the task to be repeating, cancel later otherwise. We store
            # the tickerhandler's ref to make sure we can remove it later
            self.current_ticker_ref = repeat(dt, self.execute_next_action, id_string="combat")

    def execute_next_action(self):
        """
        Triggered after a delay by the command
        """
        combatant = self.obj
        action_dict = self.action_dict

        if action.can_use():
            action.execute()
            action.post_execute()

        if not action_dict.get("repeat", True):
            # not a repeating action, use the fallback (normally the original attack)
            self.action_dict = self.fallback_action_dict
            self.queue_action(self.fallback_action_dict)

        """
        Check if the combat is over.
        """

        allies, enemies = self.get_sides(self.obj)

        # only keep combatants that are alive and still in the same room
        allies = [comb for comb in allies if comb.hp > 0 and comb.location == location]
        enemies = [comb for comb in enemies if comb.hp > 0 and comb.location == location]

        if not allies and not enemies:
            self.msg("Noone stands after the dust settles.", broadcast=False)
            return

        if not allies or not enemies:
            if allies + enemies == [self.obj]:
                self.msg("The combat is over.")
            else:
                still_standing = list_to_string(f"$You({comb.key})" for comb in allies + enemies)
                self.msg(
                    f"The combat is over. Still standing: {still_standing}.",
                    broadcast=False,
                )

        """
        Stop combat immediately.
        """
        self.queue_action({"key": "hold", "dt": 0})  # make sure ticker is killed
        del self.obj.ndb.combathandler
        self.obj.cmdset.remove(TwitchLookCmdSet)

class _BaseTwitchCombatCommand(Command):
    """
    Parent class for all twitch-combat commnads.

    """

    def at_pre_command(self):
        """
        Called before parsing.

        """
        if not self.caller.location or not self.caller.location.allow_combat:
            self.msg("Can't fight here!")

        """
        Handle parsing of most supported combat syntaxes (except stunts).

        <action> [<target>|<item>]
        or
        <action> <item> [on] <target>

        Use 'on' to differentiate if names/items have spaces in the name.

        """
        self.args = args = self.args.strip()
        self.lhs, self.rhs = "", ""

            return

        if " on " in args:
            lhs, rhs = args.split(" on ", 1)
        else:
            lhs, *rhs = args.split(None, 1)
            rhs = " ".join(rhs)
        self.lhs, self.rhs = lhs.strip(), rhs.strip()

    def get_or_create_combathandler(self, target=None, combathandler_key="combathandler"):
        """
        Get or create the combathandler assigned to this combatant.

        """
        if target:
            # add/check combathandler to the target
            if target.hp_max is None:
                self.msg("You can't attack that!")
                raise InterruptCommand()

            EvAdventureCombatTwitchHandler.get_or_create_combathandler(
                target, key=combathandler_key
            )
        return EvAdventureCombatTwitchHandler.get_or_create_combathandler(self.caller)

class CmdAttack(_BaseTwitchCombatCommand):
    """
    Attack a target. Will keep attacking the target until
    combat ends or another combat action is taken.

    Usage:
        attack/hit <target>

    """

    aliases = ["hit"]

        target = self.caller.search(self.lhs)
            return

        combathandler = self.get_or_create_combathandler(target)
        # we use a fixed dt of 3 here, to mimic Diku style; one could also picture
        # attacking at a different rate, depending on skills/weapon etc.
        combathandler.queue_action({"key": "attack", "target": target, "dt": 3, "repeat": True})
        combathandler.msg(f"$You() $conj(attack) $You({target.key})!", self.caller)

class CmdLook(default_cmds.CmdLook, _BaseTwitchCombatCommand):
        # get regular look, followed by a combat summary
            combathandler = self.get_or_create_combathandler()
            txt = str(combathandler.get_combat_summary(self.caller))
            maxwidth = max(display_len(line) for line in txt.strip().split("\n"))
            self.msg(f"|r{pad(' Combat Status ', width=maxwidth, fillchar='-')}|n\n{txt}")

class CmdHold(_BaseTwitchCombatCommand):
    """
    Hold back your blows, doing nothing.

    Usage:
        hold

    """

    key = "hold"

        combathandler = self.get_or_create_combathandler()
        combathandler.queue_action({"key": "hold"})
        combathandler.msg("$You() $conj(hold) back, doing nothing.", self.caller)

class CmdStunt(_BaseTwitchCombatCommand):
    """
    Perform a combat stunt, that boosts an ally against a target, or
    foils an enemy, giving them disadvantage against an ally.

    Usage:
        boost [ability] <recipient> <target>
        foil [ability] <recipient> <target>
        boost [ability] <target>       (same as boost me <target>)
        foil [ability] <target>        (same as foil <target> me)

    Example:
        boost STR me Goblin
        boost DEX Goblin
        foil STR Goblin me
        foil INT Goblin
        boost INT Wizard Goblin

    """

    key = "stunt"
    aliases = (
        "boost",
        "foil",
    )

        if not args or " " not in args:
            self.msg("Usage: <ability> <recipient> <target>")

        advantage = self.cmdname != "foil"

        # extract data from the input

        stunt_type, recipient, target = None, None, None

        stunt_type, *args = args.split(None, 1)
        if stunt_type:
            stunt_type = stunt_type.strip().lower()

        args = args[0] if args else ""

        recipient, *args = args.split(None, 1)
        target = args[0] if args else None

        # validate input and try to guess if not given

        # ability is requried
        if not stunt_type or stunt_type not in ABILITY_REVERSE_MAP:
            self.msg(
                f"'{stunt_type}' is not a valid ability. Pick one of"
                f" {', '.join(ABILITY_REVERSE_MAP.keys())}."
            )

        if not recipient:
            self.msg("Must give at least a recipient or target.")

            # something like `boost str target`
            target = recipient if advantage else "me"
            recipient = "me" if advantage else recipient

        # if we still have None:s at this point, we can't continue
        if None in (stunt_type, recipient, target):
            self.msg("Both ability, recipient and  target of stunt must be given.")

        # save what we found so it can be accessed from func()
        self.advantage = advantage
        self.stunt_type = ABILITY_REVERSE_MAP[stunt_type]
        self.recipient = recipient.strip()
        self.target = target.strip()

        target = self.caller.search(self.target)
            return
        recipient = self.caller.search(self.recipient)
            return

        combathandler.queue_action(
            {
                "key": "stunt",
                "recipient": recipient,
                "target": target,
                "advantage": self.advantage,
                "stunt_type": self.stunt_type,
                "defense_type": self.stunt_type,
                "dt": 3,
            },
        )
        combathandler.msg("$You() prepare a stunt!", self.caller)

class CmdUseItem(_BaseTwitchCombatCommand):
    """
    Use an item in combat. The item must be in your inventory to use.

    Usage:
        use <item>
        use <item> [on] <target>

    Examples:
        use potion
        use throwing knife on goblin
        use bomb goblin

    """

    key = "use"

            self.msg("What do you want to use?")

        self.item = self.lhs
        self.target = self.rhs or "me"

        item = self.caller.search(
            self.item, candidates=self.caller.equipment.get_usable_objects_from_backpack()
        )
            self.msg("(You must carry the item to use it.)")
            return
        if self.target:
            target = self.caller.search(self.target)
                return

        combathandler.queue_action({"key": "use", "item": item, "target": target, "dt": 3})
        combathandler.msg(
            f"$You() prepare to use {item.get_display_name(self.caller)}!", self.caller
        )

class CmdWield(_BaseTwitchCombatCommand):
    """
    Wield a weapon or spell-rune. You will the wield the item, swapping with any other item(s) you
    were wielded before.

    Usage:
      wield <weapon or spell>

    Examples:
      wield sword
      wield shield
      wield fireball

    Note that wielding a shield will not replace the sword in your hand, while wielding a two-handed
    weapon (or a spell-rune) will take two hands and swap out what you were carrying.

    """

            self.msg("What do you want to wield?")

            self.args, candidates=self.caller.equipment.get_wieldable_objects_from_backpack()
        )
            self.msg("(You must carry the item to wield it.)")
            return
        combathandler.queue_action({"key": "wield", "item": item, "dt": 3})
        combathandler.msg(f"$You() reach for {item.get_display_name(self.caller)}!", self.caller)

class TwitchCombatCmdSet(CmdSet):
    """
    Add to character, to be able to attack others in a twitch-style way.
    """

        self.add(CmdHold())
        self.add(CmdStunt())
        self.add(CmdUseItem())

class TwitchLookCmdSet(CmdSet):
    """
    This will be added/removed dynamically when in combat.
    """

"""
EvAdventure commands and cmdsets. We don't need that many stand-alone new
commands since a lot of functionality is managed in menus. These commands
are in additional to normal Evennia commands and should be added
to the CharacterCmdSet

New commands:
    inventory
    wield/wear <item>
    unwield/remove <item>
    give <item or coin> to <character>
    talk <npc>

To install, add the `EvAdventureCmdSet` from this module to the default character cmdset:

```python
    # in mygame/commands/default_cmds.py

    from evennia.contrib.tutorials.evadventure.commands import EvAdventureCmdSet  # <---

    # ...

    class CharacterCmdSet(CmdSet):
            # ...
            self.add(EvAdventureCmdSet)   # <-----

```
"""

from evennia import CmdSet, Command, InterruptCommand

from .enums import WieldLocation
from .equipment import EquipmentError
from .npcs import EvAdventureTalkativeNPC
from .utils import get_obj_stats

class EvAdventureCommand(Command):
    """
    Base EvAdventure command. This is on the form

        command <args>

    where whitespace around the argument(s) are stripped.

    """

class CmdInventory(EvAdventureCommand):
    """
    View your inventory

    Usage:
      inventory

    """

    aliases = ("i", "inv")

        loadout = self.caller.equipment.display_loadout()
        backpack = self.caller.equipment.display_backpack()
        slot_usage = self.caller.equipment.display_slot_usage()

        self.caller.msg(f"{loadout}\n{backpack}\nYou use {slot_usage} equipment slots.")

class CmdWieldOrWear(EvAdventureCommand):
    """
    Wield a weapon/shield, or wear a piece of armor or a helmet.

    Usage:
      wield <item>
      wear <item>

    The item will automatically end up in the suitable spot, replacing whatever
    was there previously.

    """

    aliases = ("wear",)

    out_txts = {
        WieldLocation.BACKPACK: "You shuffle the position of {key} around in your backpack.",
        WieldLocation.TWO_HANDS: "You hold {key} with both hands.",
        WieldLocation.WEAPON_HAND: "You hold {key} in your strongest hand, ready for action.",
        WieldLocation.SHIELD_HAND: "You hold {key} in your off hand, ready to protect you.",
        WieldLocation.BODY: "You strap {key} on yourself.",
        WieldLocation.HEAD: "You put {key} on your head.",
    }

        # find the item among those in equipment
        item = self.caller.search(self.args, candidates=self.caller.equipment.all(only_objs=True))
            # An 'item not found' error will already have been reported; we add another line
            # here for clarity.
            self.caller.msg("You must carry the item you want to wield or wear.")
            return

        use_slot = getattr(item, "inventory_use_slot", WieldLocation.BACKPACK)

        # check what is currently in this slot
        current = self.caller.equipment.slots[use_slot]

        if current == item:
            self.caller.msg(f"You are already using {item.key}.")
            return

        # move it to the right slot based on the type of object
        self.caller.equipment.move(item)

        # inform the user of the change (and potential swap)
        if current:
            self.caller.msg(f"Returning {current.key} to the backpack.")
        self.caller.msg(self.out_txts[use_slot].format(key=item.key))

class CmdRemove(EvAdventureCommand):
    """
    Remove a remove a weapon/shield, armor or helmet.

    Usage:
      remove <item>
      unwield <item>
      unwear <item>

    To remove an item from the backpack, use |wdrop|n instead.

    """

    aliases = ("unwield", "unwear")

        item = caller.search(self.args, candidates=caller.equipment.all(only_objs=True))
            # An 'item not found' error will already have been reported
            return

        current_slot = caller.equipment.get_current_slot(item)

        if current_slot is WieldLocation.BACKPACK:
            # we don't allow dropping this way since it may be unexepected by users who forgot just
            # where their item currently is.
                f"You already stashed away {item.key} in your backpack. Use 'drop' if "
                "you want to get rid of it."
            )
            return

        caller.equipment.remove(item)
        caller.equipment.add(item)
        caller.msg(f"You stash {item.key} in your backpack.")

# give / accept menu

def _rescind_gift(caller, raw_string, **kwargs):
    """
    Called when giver rescinds their gift in `node_give` below.
    It means they entered 'cancel' on the gift screen.

    """
    # kill the gift menu for the receiver immediately
    receiver = kwargs["receiver"]
    receiver.ndb._evmenu.close_menu()
    receiver.msg("The offer was rescinded.")
    return "node_end"

def node_give(caller, raw_string, **kwargs):
    """
    This will show to the giver until receiver accepts/declines. It allows them
    to rescind their offer.

    The `caller` here is the one giving the item. We also make sure to feed
    the 'item' and 'receiver' into the Evmenu.

    """
    item = kwargs["item"]
    text = f"""
You are offering {item.key} to {receiver.get_display_name(looker=caller)}.
|wWaiting for them to accept or reject the offer ...|n

    options = {
        "key": ("cancel", "abort"),
        "desc": "Rescind your offer.",
        "goto": (_rescind_gift, kwargs),
    }

def _accept_or_reject_gift(caller, raw_string, **kwargs):
    """
    Called when receiver enters yes/no in `node_receive` below. We first need to
    figure out which.

    """
    giver = kwargs["giver"]
    if raw_string.lower() in ("yes", "y"):
        # they accepted - move the item!
        item = giver.equipment.remove(item)
        if item:
            try:
                # this will also add them to the equipment backpack, if possible
                item.move_to(caller, quiet=True, move_type="give")
            except EquipmentError:
                    (
                        f"$You({giver.key.key}) $conj(try) to give "
                        f"{item.key} to $You({caller.key}), but they can't accept it since their "
                        "inventory is full."
                    ),
                    mapping={giver.key: giver, caller.key: caller},
                )
            else:
                    (
                        f"$You({giver.key}) $conj(give) {item.key} to $You({caller.key}), "
                        "and they accepted the offer."
                    ),
                )
        giver.ndb._evmenu.close_menu()
        return "node_end"

def node_receive(caller, raw_string, **kwargs):
    """
    Will show to the receiver and allow them to accept/decline the offer for
    as long as the giver didn't rescind it.

    The `caller` here is the one receiving the item. We also make sure to feed
    the 'item' and 'giver' into the EvMenu.

    """
    text = f"""
{giver.get_display_name()} is offering you {item.key}:

{get_obj_stats(item)}

[Your inventory usage: {caller.equipment.display_slot_usage()}]
|wDo you want to accept the given item? Y/[N]
    """
    options = ({"key": "_default", "goto": (_accept_or_reject_gift, kwargs)},)

def node_end(caller, raw_string, **kwargs):
    return "", None

class CmdGive(EvAdventureCommand):
    """
    Give item or money to another person. Items need to be accepted before
    they change hands. Money changes hands immediately with no wait.

    Usage:
      give <item> to <receiver>
      give <number of coins> [coins] to receiver

    If item name includes ' to ', surround it in quotes.

    Examples:
      give apple to ranger
      give "road to happiness" to sad ranger
      give 10 coins to ranger
      give 12 to ranger

    """

        """
        Parsing is a little more complex for this command.

        """
        if " to " not in args:
                "Usage: give <item> to <recevier>. Specify e.g. '10 coins' to pay money. "
                "Use quotes around the item name it if includes the substring ' to '. "
            )

        self.item_name = ""
        self.coins = 0

        # make sure we can use '...' to include items with ' to ' in the name
        if args.startswith('"') and args.count('"') > 1:
            end_ind = args[1:].index('"') + 1
            item_name = args[:end_ind]
            _, receiver_name = args.split(" to ", 1)
        elif args.startswith("'") and args.count("'") > 1:
            end_ind = args[1:].index("'") + 1
        else:
            item_name, receiver_name = args.split(" to ", 1)

        # a coin count rather than a normal name
        if " coins" in item_name:
            item_name = item_name[:-6]
        if item_name.isnumeric():
            self.coins = max(0, int(item_name))

        self.item_name = item_name
        self.receiver_name = receiver_name

        receiver = caller.search(self.receiver_name)
        if not receiver:
            return

        # giving of coins is always accepted

        if self.coins:
            current_coins = caller.coins
            if self.coins > current_coins:
                caller.msg(f"You only have |y{current_coins}|n coins to give.")
                return
            # do transaction
            caller.coins -= self.coins
            receiver.coins += self.coins
            caller.location.msg_contents(
                f"$You() $conj(give) $You({receiver.key}) {self.coins} coins.",
                from_obj=caller,
                mapping={receiver.key: receiver},
            )
            return

        # giving of items require acceptance before it happens

        item = caller.search(self.item_name, candidates=caller.equipment.all(only_objs=True))
            return

        # testing hook
        if not item.at_pre_give(caller, receiver):
            return

        # before we start menus, we must check so either part is not already in a menu,
        # that would be annoying otherwise
        if receiver.ndb._evmenu:
                f"{receiver.get_display_name(looker=caller)} seems busy talking to someone else."
            )
            return
        if caller.ndb._evmenu:
            caller.msg("Close the current menu first.")
            return

        # this starts evmenus for both parties
        EvMenu(
            receiver, {"node_receive": node_receive, "node_end": node_end}, item=item, giver=caller
        )
        EvMenu(caller, {"node_give": node_give, "node_end": node_end}, item=item, receiver=receiver)

class CmdTalk(EvAdventureCommand):
    """
    Start a conversations with shop keepers and other NPCs in the world.

    Args:
      talk <npc>

    """

            return

        if not inherits_from(target, EvAdventureTalkativeNPC):
                f"{target.get_display_name(looker=self.caller)} does not seem very talkative."
            )
            return
        target.at_talk(self.caller)

class EvAdventureCmdSet(CmdSet):
    """
    Groups all commands in one cmdset which can be added in one go to the DefaultCharacter cmdset.

    """

    key = "evadventure"

        self.add(CmdWieldOrWear())
        self.add(CmdGive())
        self.add(CmdTalk())

"""
Dungeon system

This creates a procedurally generated dungeon.

The dungone originates in an entrance room with exits that spawn a new dungeon connection every X
minutes. As long as characters go through the same exit within that time, they will all end up in
the same dungeon 'branch', otherwise they will go into separate, un-connected dungeon 'branches'.
They can always go back to the start room, but this will become a one-way exit back.

When moving through the dungeon, a new room is not generated until characters
decided to go in that direction. Each room is tagged with the specific 'instance'
id of that particular branch of dungon. When no characters remain in the branch,
the branch is deleted.

Each room in the dungeon starts with a Tag `not_clear`; while this is set, all exits out
of the room (not the one they came from) is blocked. When whatever problem the room
offers has been solved (such as a puzzle or a battle), the tag is removed and the player(s)
can choose which exit to leave through.

"""

from math import sqrt
from random import randint, random, shuffle

from .rooms import EvAdventureRoom

# aliases for cardinal directions
_AVAILABLE_DIRECTIONS = [
    "north",
    "east",
    "south",
    "west",
    # commented out to make the dungeon simpler to navigate
    # "northeast", "southeast", "southwest", "northwest",
]

_EXIT_ALIASES = {
    "north": ("n",),
    "east": ("e",),
    "south": ("s",),
    "west": ("w",),
    "northeast": ("ne",),
    "southeast": ("se",),
    "southwest": ("sw",),
    "northwest": ("nw",),
}
# finding the reverse cardinal direction
_EXIT_REVERSE_MAPPING = {
    "north": "south",
    "east": "west",
    "south": "north",
    "west": "east",
    "northeast": "southwest",
    "southeast": "northwest",
    "southwest": "northeast",
    "northwest": "southeast",
}

# how xy coordinate shifts by going in direction
_EXIT_GRID_SHIFT = {
    "north": (0, 1),
    "east": (1, 0),
    "south": (0, -1),
    "west": (-1, 0),
    "northeast": (1, 1),
    "southeast": (1, -1),
    "southwest": (-1, -1),
    "northwest": (-1, 1),
}

# --------------------------------------------------
# Dungeon orchestrator and room / exits

class EvAdventureDungeonRoom(EvAdventureRoom):
    """
    Dangerous dungeon room.

    """

    allow_combat = AttributeProperty(True, autocreate=False)
    allow_death = AttributeProperty(True, autocreate=False)

    # dungeon generation attributes; set when room is created
    back_exit = AttributeProperty(None, autocreate=False)
    dungeon_orchestrator = AttributeProperty(None, autocreate=False)
    xy_coords = AttributeProperty(None, autocreate=False)

    @property
    def is_room_clear(self):
        return not bool(self.tags.get("not_clear", category="dungeon_room"))

    def clear_room(self):
        self.tags.remove("not_clear", category="dungeon_room")

        """
        Set the `not_clear` tag on the room. This is removed when the room is
        'cleared', whatever that means for each room.

        We put this here rather than in the room-creation code so we can override
        easier (for example we may want an empty room which auto-clears).

        """
        self.tags.add("not_clear", category="dungeon_room")

    def get_display_footer(self, looker, **kwargs):
        """
        Show if the room is 'cleared' or not as part of its description.

        """
        if self.is_room_clear:
            return ""
        else:
            return "|rThe path forwards is blocked!|n"

class EvAdventureDungeonExit(DefaultExit):
    """
    Dungeon exit. This will not create the target room until it's traversed.

    """

        """
        We want to block progressing forward unless the room is clear.

        """
        self.locks.add("traverse:not objloctag(not_clear, dungeon_room)")

    def at_traverse(self, traversing_object, target_location, **kwargs):
        """
        Called when traversing. `target_location` will be None if the
        target was not yet created. It checks the current location to get the
        dungeon-orchestrator in use.

        """
        if target_location == self.location:
            self.destination = target_location = self.location.db.dungeon_orchestrator.new_room(
                self
            )
            if self.id in self.location.dungeon_orchestrator.unvisited_exits:
                self.location.dungeon_orchestrator.unvisited_exits.remove(self.id)

        super().at_traverse(traversing_object, target_location, **kwargs)

    def at_failed_traverse(self, traversing_object, **kwargs):
        """
        Called when failing to traverse.

        """
        traversing_object.msg("You can't get through this way yet!")

def room_generator(dungeon_orchestrator, depth, coords):
    """
    Plugin room generator

    This default one returns the same empty room.

    Args:
        dungeon_orchestrator (EvAdventureDungeonOrchestrator): The current orchestrator.
        depth (int): The 'depth' of the dungeon (radial distance from start room) this
            new room will be placed at.
        coords (tuple): The `(x,y)` coords that the new room will be created at.

    """
    room_typeclass = EvAdventureDungeonRoom

    # simple map of depth to name and desc of room
    name_depth_map = {
        1: ("Water-logged passage", "This earth-walled passage is dripping of water."),
        2: ("Passage with roots", "Roots are pushing through the earth walls."),
        3: ("Hardened clay passage", "The walls of this passage is of hardened clay."),
        4: ("Clay with stones", "This passage has clay with pieces of stone embedded."),
        5: ("Stone passage", "Walls are crumbling stone, with roots passing through it."),
        6: ("Stone hallway", "Walls are cut from rough stone."),
        7: ("Stone rooms", "A stone room, built from crude and heavy blocks."),
        8: ("Granite hall", "The walls are of well-fitted granite blocks."),
        9: ("Marble passages", "The walls are blank and shiny marble."),
        10: ("Furnished rooms", "The marble walls have tapestries and furnishings."),
    }
    key, desc = name_depth_map.get(depth, ("Dark rooms", "There is very dark here."))

    new_room = create.create_object(
        room_typeclass,
        key=key,
        attributes=(
            ("desc", desc),
            ("xy_coords", coords),
            ("dungeon_orchestrator", dungeon_orchestrator),
        ),
    )
    return new_room

class EvAdventureDungeonOrchestrator(DefaultScript):
    """
    One script is created per dungeon 'branch' created. The orchestrator is
    responsible for determining what is created next when a character enters an
    exit within the dungeon.

    """

    # this determines how branching the dungeon will be
    max_unexplored_exits = 2
    max_new_exits_per_room = 2

    rooms = AttributeProperty(list())
    unvisited_exits = AttributeProperty(list())
    highest_depth = AttributeProperty(0)

    last_updated = AttributeProperty(datetime.utcnow())

    # the room-generator function; copied from the same-name value on the start-room when the
    # orchestrator is first created
    room_generator = AttributeProperty(None, autocreate=False)

    # (x,y): room coordinates used up by orchestrator
    xy_grid = AttributeProperty(dict())
    start_room = AttributeProperty(None, autocreate=False)

    def register_exit_traversed(self, exit):
        """
        Tell the system the given exit was traversed. This allows us to track how many unvisited
        paths we have so as to not have it grow exponentially.

        """
        if exit.id in self.unvisited_exits:
            self.unvisited_exits.remove(exit.id)

    def create_out_exit(self, location, exit_direction="north"):
        """
        Create outgoing exit from a room. The target room is not yet created.

        """
        out_exit = create.create_object(
            EvAdventureDungeonExit,
            key=exit_direction,
            aliases=_EXIT_ALIASES[exit_direction],
        )
        self.unvisited_exits.append(out_exit.id)

        """
        Clean up the entire dungeon along with the orchestrator.

        """
        # first secure all characters in this branch back to the start room
        characters = search.search_object_by_tag(self.key, category="dungeon_character")
        start_room = self.start_room
        for character in characters:
            start_room.msg_contents(
                "Suddenly someone stumbles out of a dark exit, covered in dust!"
            )
            character.location = start_room
            character.msg(
                "|rAfter a long time of silence, the room suddenly rumbles and then collapses! "
                "All turns dark ...|n\n\nThen you realize you are back where you started."
            )
            character.tags.remove(self.key, category="dungeon_character")
        # next delete all rooms in the dungeon (this will also delete exits)
        rooms = search.search_object_by_tag(self.key, category="dungeon_room")
        for room in rooms:
            room.delete()
        # finally delete the orchestrator itself

    def new_room(self, from_exit):
        """
        Create a new Dungeon room leading from the provided exit.

        Args:
            from_exit (Exit): The exit leading to this new room.

        """
        self.last_updated = datetime.utcnow()
        # figure out coordinate of old room and figure out what coord the
        # new one would get
        source_location = from_exit.location
        x, y = source_location.attributes.get("xy_coords", default=(0, 0))
        dx, dy = _EXIT_GRID_SHIFT.get(from_exit.key, (0, 1))
        new_x, new_y = (x + dx, y + dy)

        # the dungeon's depth acts as a measure of the current difficulty level. This is the radial
        # distance from the (0, 0) (the entrance). The Orchestrator also tracks the highest
        # depth achieved.
        depth = int(sqrt(new_x**2 + new_y**2))

        new_room = self.room_generator(self, depth, (new_x, new_y))

        self.xy_grid[(new_x, new_y)] = new_room

        # always make a return exit back to where we came from
        back_exit_key = _EXIT_REVERSE_MAPPING.get(from_exit.key, "back")
        create.create_object(
            key=back_exit_key,
            aliases=_EXIT_ALIASES.get(back_exit_key, ()),
            location=new_room,
            destination=from_exit.location,
                (
                    "desc",
                    "A dark passage.",
                ),
            ),
            # we default to allowing back-tracking (also used for fleeing)
            locks=("traverse: true()",),
        )

        # figure out what other exits should be here, if any
        n_unexplored = len(self.unvisited_exits)

        if n_unexplored < self.max_unexplored_exits:
            # we have a budget of unexplored exits to open
            n_exits = min(self.max_new_exits_per_room, self.max_unexplored_exits)
            if n_exits > 1:
                n_exits = randint(1, n_exits)
            available_directions = [
                direction for direction in _AVAILABLE_DIRECTIONS if direction != back_exit_key
            ]
            # randomize order of exits
            shuffle(available_directions)
            for _ in range(n_exits):
                while available_directions:
                    # get a random direction and check so there isn't a room already
                    # created in that direction
                    direction = available_directions.pop(0)
                    dx, dy = _EXIT_GRID_SHIFT[direction]
                    target_coord = (new_x + dx, new_y + dy)
                    if target_coord not in self.xy_grid and target_coord != (0, 0):
                        # no room there (and not back to start room) - make an exit to it
                        self.create_out_exit(new_room, direction)
                        # we create this to avoid other rooms linking here, but don't create the
                        # room yet
                        self.xy_grid[target_coord] = None
                        break

        self.highest_depth = max(self.highest_depth, depth)

        return new_room

# Start room

class EvAdventureDungeonStartRoomExit(DefaultExit):
    """
    Traversing this exit will either lead to an existing dungeon branch or create
    a new one.

    Since exits need to have a destination, we start out having them loop back to
    the same location and change this whenever someone actually traverse them. The
    act of passing through creates a room on the other side.

    """

    def reset_exit(self):
        """
        Flush the exit, so next traversal creates a new dungeon branch.

        """
        self.destination = self.location

        """
        When traversing create a new orchestrator if one is not already assigned.

        """
            # make a global orchestrator script for this dungeon branch
            self.location.room_generator
            dungeon_orchestrator = create.create_script(
                EvAdventureDungeonOrchestrator,
                key=f"dungeon_orchestrator_{self.key}_{datetime.utcnow()}",
                attributes=(
                    ("start_room", self.location),
                    ("room_generator", self.location.room_generator),
                ),
            )
            self.destination = target_location = dungeon_orchestrator.new_room(self)
            # make sure to tag character when entering so we can find them again later
            traversing_object.tags.add(dungeon_orchestrator.key, category="dungeon_character")

class EvAdventureStartRoomResetter(DefaultScript):
    """
    Simple ticker-script. Introduces a chance of the room's exits cycling every interval.

    """

        self.key = "evadventure_dungeon_startroom_resetter"

        """
        Called every time the script repeats.

        """
        room = self.obj
        for exi in room.exits:
            if inherits_from(exi, EvAdventureDungeonStartRoomExit) and random() < 0.5:
                exi.reset_exit()

class EvAdventureDungeonBranchDeleter(DefaultScript):
    """
    Cleanup script. After some time a dungeon branch will 'collapse', forcing all players in it
    back to the start room.

    """

    # set at creation time when the start room is created
    branch_max_life = AttributeProperty(0, autocreate=False)

        self.key = "evadventure_dungeon_branch_deleter"

        """
        Go through all dungeon-orchestrators and find which ones are too old.

        """
        max_dt = timedelta(seconds=self.branch_max_life)
        max_allowed_date = datetime.utcnow() - max_dt

        for orchestrator in EvAdventureDungeonOrchestrator.objects.all():
            if orchestrator.last_updated < max_allowed_date:
                # orchestrator is too old; tell it to clean up and delete itself
                orchestrator.delete()

class EvAdventureDungeonStartRoom(EvAdventureDungeonRoom):
    """
    The start room is the only permanent part of the dungeon. Exits leading from this room (except
    one leading back outside) each create/links to a separate dungeon branch/instance.

    - A script will reset each exit every 5 mins; after that time, entering the exit will spawn
        a new branch-instance instead of leading to the one before.
    - Another script will check age of branch instance every hour; once an instance has been
        inactive for a week, it will 'collapse', forcing everyone inside back to the start room.

    The actual exits should be created in the build script.

    """

    recycle_time = 60 * 5  # 5 mins
    branch_check_time = 60 * 60  # one hour
    branch_max_life = 60 * 60 * 24 * 7  # 1 week

    # allow for a custom room_generator function
    room_generator = AttributeProperty(lambda: room_generator, autocreate=False)

        return (
            "|yYou sense that if you want to team up, "
            "you must all pick the same path from here ... or you'll quickly get separated.|n"
        )

        # want to set the script interval on creation time, so we use create_script with obj=self
        # instead of self.scripts.add() here
        create.create_script(
            EvAdventureStartRoomResetter, obj=self, interval=self.recycle_time, autostart=True
        )
            EvAdventureDungeonBranchDeleter,
            obj=self,
            interval=self.branch_check_time,
            autostart=True,
            attributes=(("branch_max_life", self.branch_max_life),),
        )

    def at_object_receive(self, obj, source_location, **kwargs):
        """
        Make sure to clean the dungeon branch-tag from characters when leaving a dungeon branch.

        """
        obj.tags.remove(category="dungeon_character")

"""
Enums are constants representing different things in EvAdventure. The advantage
of using an Enum over, say, a string is that if you make a typo using an unknown
enum, Python will give you an error while a typo in a string may go through silently.

It's used as a direct reference:
::

    from enums import Ability

    if abi is Ability.STR:
        # ...

To get the `value` of an enum (must always be hashable, useful for Attribute lookups), use
`Ability.STR.value` (which would return 'strength' in our case).

----

"""
from enum import Enum

class Ability(Enum):
    """
    The six base abilities (defense is always bonus + 10)

    """

    STR = "strength"
    DEX = "dexterity"
    CON = "constitution"
    INT = "intelligence"
    WIS = "wisdom"
    CHA = "charisma"

    ARMOR = "armor"

    CRITICAL_FAILURE = "critical_failure"
    CRITICAL_SUCCESS = "critical_success"

    ALLEGIANCE_HOSTILE = "hostile"
    ALLEGIANCE_NEUTRAL = "neutral"
    ALLEGIANCE_FRIENDLY = "friendly"

ABILITY_REVERSE_MAP = {
    "str": Ability.STR,
    "dex": Ability.DEX,
    "con": Ability.CON,
    "int": Ability.INT,
    "wis": Ability.WIS,
    "cha": Ability.CHA,
}

class WieldLocation(Enum):
    """
    Wield (or wear) locations.

    """

    # wield/wear location
    BACKPACK = "backpack"
    WEAPON_HAND = "weapon_hand"
    SHIELD_HAND = "shield_hand"
    TWO_HANDS = "two_handed_weapons"
    BODY = "body"  # armor
    HEAD = "head"  # helmets

class ObjType(Enum):
    """
    Object types

    """

    WEAPON = "weapon"
    SHIELD = "shield"
    HELMET = "helmet"
    CONSUMABLE = "consumable"
    GEAR = "gear"
    THROWABLE = "throwable"
    MAGIC = "magic"
    QUEST = "quest"
    TREASURE = "treasure"

"""
Knave has a system of Slots for its inventory.

"""

from .enums import Ability, WieldLocation
from .objects import EvAdventureObject, get_bare_hands

class EquipmentError(TypeError):
    pass

class EquipmentHandler:
    """
    _Knave_ puts a lot of emphasis on the inventory. You have CON_DEFENSE inventory
    slots. Some things, like torches can fit multiple in one slot, other (like
    big weapons and armor) use more than one slot. The items carried and wielded has a big impact
    on character customization - even magic requires carrying a runestone per spell.

    The inventory also doubles as a measure of negative effects. Getting soaked in mud
    or slime could gunk up some of your inventory slots and make the items there unusuable
    until you clean them.

    """

    save_attribute = "inventory_slots"

        self._load()

    def _load(self):
        """
        Load or create a new slot storage.

        """
        self.slots = self.obj.attributes.get(
            self.save_attribute,
            category="inventory",
            default={
                WieldLocation.WEAPON_HAND: None,
                WieldLocation.SHIELD_HAND: None,
                WieldLocation.TWO_HANDS: None,
                WieldLocation.BODY: None,
                WieldLocation.HEAD: None,
                WieldLocation.BACKPACK: [],
            },
        )
        self.slots[WieldLocation.BACKPACK] = [
            obj for obj in self.slots[WieldLocation.BACKPACK] if obj and obj.id
        ]

        """
        Save slot to storage.

        """
        self.obj.attributes.add(self.save_attribute, self.slots, category="inventory")

    def count_slots(self):
        """
        Count slot usage. This is fetched from the .size Attribute of the
        object. The size can also be partial slots.

        """
        slots = self.slots
        wield_usage = sum(
            getattr(slotobj, "size", 0) or 0
            for slot, slotobj in slots.items()
            if slot is not WieldLocation.BACKPACK
        )
        backpack_usage = sum(
            getattr(slotobj, "size", 0) or 0 for slotobj in slots[WieldLocation.BACKPACK]
        )
        return wield_usage + backpack_usage

    @property
    def max_slots(self):
        """
        The max amount of equipment slots ('carrying capacity') is based on
        the constitution defense.

        """
        return getattr(self.obj, Ability.CON.value, 1) + 10

    def validate_slot_usage(self, obj):
        """
        Check if obj can fit in equipment, based on its size.

        Args:
            obj (EvAdventureObject): The object to add.

        """
        if not inherits_from(obj, EvAdventureObject):
            raise EquipmentError(f"{obj.key} is not something that can be equipped.")

        size = obj.size
        max_slots = self.max_slots
        current_slot_usage = self.count_slots()

        if current_slot_usage + size > max_slots:
            slots_left = max_slots - current_slot_usage
            raise EquipmentError(
                f"Equipment full ($int2str({slots_left}) slots "
                f"remaining, {obj.key} needs $int2str({size}) "
                f"$pluralize(slot, {size}))."
            )

    def get_current_slot(self, obj):
        """
        Check which slot-type the given object is in.

        Args:
            obj (EvAdventureObject): The object to check.

        Returns:
            WieldLocation: A location the object is in. None if the object
            is not in the inventory at all.

        """
        for equipment_item, slot in self.all():
            if obj == equipment_item:
                return slot

    @property
        """
        Armor provided by actually worn equipment/shield. For body armor
        this is a base value, like 12, for shield/helmet, it's a bonus, like +1.
        We treat values and bonuses equal and just add them up. This value
        can thus be 0, the 'unarmored' default should be handled by the calling
        method.

        Returns:
            int: Armor from equipment. Note that this is the +bonus of Armor, not the
                'defense' (to get that one adds 10).

        """
        return sum(
            (
                # armor is listed using its defense, so we remove 10 from it
                # (11 is base no-armor value in Knave)
                getattr(slots[WieldLocation.BODY], "armor", 1),
                # shields and helmets are listed by their bonus to armor
                getattr(slots[WieldLocation.SHIELD_HAND], "armor", 0),
                getattr(slots[WieldLocation.HEAD], "armor", 0),
            )
        )

    @property
        """
        Conveniently get the currently active weapon or rune stone.

        Returns:
            obj or None: The weapon. None if unarmored.

        """
        # first checks two-handed wield, then one-handed; the two
        # should never appear simultaneously anyhow (checked in `move` method).
        weapon = slots[WieldLocation.TWO_HANDS]
            weapon = slots[WieldLocation.WEAPON_HAND]
            weapon = get_bare_hands()
        return weapon

    def display_loadout(self):
        """
        Get a visual representation of your current loadout.

        Returns:
            str: The current loadout.

        """
        weapon_str = "You are fighting with your bare fists"
        shield_str = " and have no shield."
        armor_str = "You wear no armor"
        helmet_str = " and no helmet."

        two_hands = slots[WieldLocation.TWO_HANDS]
        if two_hands:
            weapon_str = f"You wield {two_hands} with both hands"
            shield_str = " (you can't hold a shield at the same time)."
        else:
            one_hands = slots[WieldLocation.WEAPON_HAND]
            if one_hands:
                weapon_str = f"You are wielding {one_hands} in one hand."
            shield = slots[WieldLocation.SHIELD_HAND]
            if shield:
                shield_str = f"You have {shield} in your off hand."

        armor = slots[WieldLocation.BODY]
        if armor:
            armor_str = f"You are wearing {armor}"

        helmet = slots[WieldLocation.BODY]
        if helmet:
            helmet_str = f" and {helmet} on your head."

        return f"{weapon_str}{shield_str}\n{armor_str}{helmet_str}"

    def display_backpack(self):
        """
        Get a visual representation of the backpack's contents.

        """
        backpack = self.slots[WieldLocation.BACKPACK]
        if not backpack:
            return "Backpack is empty."
        out = []
        for item in backpack:
            out.append(f"{item.key} [|b{item.size}|n] slot(s)")
        return "\n".join(out)

    def display_slot_usage(self):
        """
        Get a slot usage/max string for display.

        Returns:
            str: The usage string.

        """
        return f"|b{self.count_slots()}/{self.max_slots}|n"

    def move(self, obj):
        """
        Moves item to the place it things it should be in - this makes use of the object's wield
        slot to decide where it goes. The item is assumed to already be in the backpack.

        Args:
            obj (EvAdventureObject): Thing to use.

        Raises:
            EquipmentError: If there's no room in inventory. It will contains the details
                of the error, suitable to echo to user.

        Notes:
            This will cleanly move any 'colliding' items to the backpack to
            make the use possible (such as moving sword + shield to backpack when wielding
            a two-handed weapon). If wanting to warn the user about this, it needs to happen
            before this call.

        """
        # make sure to remove from backpack first, if it's there, since we'll be re-adding it
        self.remove(obj)

        self.validate_slot_usage(obj)
        use_slot = getattr(obj, "inventory_use_slot", WieldLocation.BACKPACK)

        to_backpack = []
        if use_slot is WieldLocation.TWO_HANDS:
            # two-handed weapons can't co-exist with weapon/shield-hand used items
            to_backpack = [slots[WieldLocation.WEAPON_HAND], slots[WieldLocation.SHIELD_HAND]]
            slots[WieldLocation.WEAPON_HAND] = slots[WieldLocation.SHIELD_HAND] = None
            slots[use_slot] = obj
        elif use_slot in (WieldLocation.WEAPON_HAND, WieldLocation.SHIELD_HAND):
            # can't keep a two-handed weapon if adding a one-handed weapon or shield
            to_backpack = [slots[WieldLocation.TWO_HANDS]]
            slots[WieldLocation.TWO_HANDS] = None
        elif use_slot is WieldLocation.BACKPACK:
            # it belongs in backpack, so goes back to it
            to_backpack = [obj]
        else:
            # for others (body, head), just replace whatever's there and put the old
            # thing in the backpack
            to_backpack = [slots[use_slot]]

        for to_backpack_obj in to_backpack:
            # put stuff in backpack
            if to_backpack_obj:
                slots[WieldLocation.BACKPACK].append(to_backpack_obj)

        # store new state

    def add(self, obj):
        """
        Put something in the backpack specifically (even if it could be wield/worn).

        Args:

        Notes:
            This will not change the object's `.location`, this must be done
            by the calling code.

        """
        # check if we have room
        self.slots[WieldLocation.BACKPACK].append(obj)

    def remove(self, obj_or_slot):
        """
        Remove specific object or objects from a slot.

        Args:
            obj_or_slot (EvAdventureObject or WieldLocation): The specific object or
                location to empty. If this is WieldLocation.BACKPACK, all items
                in the backpack will be emptied and returned!
        Returns:
            list: A list of 0, 1 or more objects emptied from the inventory.

        Notes:
            This will not change the object's `.location`, this must be done separately

        """
        ret = []
        if isinstance(obj_or_slot, WieldLocation):
            if obj_or_slot is WieldLocation.BACKPACK:
                # empty entire backpack
                ret.extend(slots[obj_or_slot])
                slots[obj_or_slot] = []
            else:
                ret.append(slots[obj_or_slot])
                slots[obj_or_slot] = None
        elif obj_or_slot in self.slots.values():
            # obj in use/wear slot
            for slot, objslot in slots.items():
                if objslot is obj_or_slot:
                    slots[slot] = None
                    ret.append(objslot)
        elif obj_or_slot in slots[WieldLocation.BACKPACK]:
            # obj in backpack slot
            try:
                slots[WieldLocation.BACKPACK].remove(obj_or_slot)
                ret.append(obj_or_slot)
                pass
        if ret:
            self._save()
        return ret

    def get_wieldable_objects_from_backpack(self):
        """
        Get all wieldable weapons (or spell runes) from backpack. This is useful in order to
        have a list to select from when swapping your wielded loadout.

        Returns:
            list: A list of objects with a suitable `inventory_use_slot`. We don't check
            quality, so this may include broken items (we may want to visually show them
            in the list after all).

        """
        return [
            obj
            for obj in self.slots[WieldLocation.BACKPACK]
            if obj
            and obj.id
            and obj.inventory_use_slot
            in (WieldLocation.WEAPON_HAND, WieldLocation.TWO_HANDS, WieldLocation.SHIELD_HAND)
        ]

    def get_wearable_objects_from_backpack(self):
        """
        Get all wearable items (armor or helmets) from backpack. This is useful in order to
        have a list to select from when swapping your worn loadout.

        Returns:

        """
        return [
            obj
            if obj and obj.id and obj.inventory_use_slot in (WieldLocation.BODY, WieldLocation.HEAD)
        ]

    def get_usable_objects_from_backpack(self):
        """
        Get all 'usable' items (like potions) from backpack. This is useful for getting a
        list to select from.

        Returns:
            list: A list of objects that are usable.

        """
        character = self.obj
        return [
            obj for obj in self.slots[WieldLocation.BACKPACK] if obj and obj.at_pre_use(character)
        ]

    def all(self, only_objs=False):
        """
        Get all objects in inventory, regardless of location.

            only_objs (bool): Only return a flat list of objects, not tuples.

        Returns:
            list: A list of item tuples `[(item, WieldLocation),...]`
            starting with the wielded ones, backpack content last. If `only_objs` is set,
            this will just be a flat list of objects.

        """
        lst = [
            (slots[WieldLocation.WEAPON_HAND], WieldLocation.WEAPON_HAND),
            (slots[WieldLocation.SHIELD_HAND], WieldLocation.SHIELD_HAND),
            (slots[WieldLocation.TWO_HANDS], WieldLocation.TWO_HANDS),
            (slots[WieldLocation.BODY], WieldLocation.BODY),
            (slots[WieldLocation.HEAD], WieldLocation.HEAD),
        ] + [(item, WieldLocation.BACKPACK) for item in slots[WieldLocation.BACKPACK]]
        if only_objs:
            # remove any None-results from empty slots
            return [tup[0] for tup in lst if tup[0]]
        # keep empty slots
        return [tup for tup in lst]

"""
EvAdventure NPCs. This includes both friends and enemies, only separated by their AI.

"""

from evennia.typeclasses.tags import TagProperty

from .characters import LivingMixin
from .objects import get_bare_hands

class EvAdventureNPC(LivingMixin, DefaultCharacter):
    """
    This is the base class for all non-player entities, including monsters. These
    generally don't advance in level but uses a simplified, abstract measure of how
    dangerous or competent they are - the 'hit dice' (HD).

    HD indicates how much health they have and how hard they hit. In _Knave_, HD also
    defaults to being the bonus for all abilities. HP is 4 x Hit die (this can then be
    customized per-entity of course).

    Morale is set explicitly per-NPC, usually between 7 and 9.

    Monsters don't use equipment in the way PCs do, instead they have a fixed armor
    value, and their Abilities are dynamically generated from the HD (hit_dice).

    If wanting monsters or NPCs that can level and work the same as PCs, base them off the
    EvAdventureCharacter class instead.

    The weapon of the npc is stored as an Attribute instead of implementing a full
    inventory/equipment system. This means that the normal inventory can be used for
    non-combat purposes (or for loot to get when killing an enemy).

    """

    hit_dice = AttributeProperty(default=1, autocreate=False)
    armor = AttributeProperty(default=1, autocreate=False)  # +10 to get armor defense
    morale = AttributeProperty(default=9, autocreate=False)
    hp_multiplier = AttributeProperty(default=4, autocreate=False)  # 4 default in Knave
    hp = AttributeProperty(default=None, autocreate=False)  # internal tracking, use .hp property
    allegiance = AttributeProperty(default=Ability.ALLEGIANCE_HOSTILE, autocreate=False)

    is_idle = AttributeProperty(default=False, autocreate=False)

    weapon = AttributeProperty(default=get_bare_hands, autocreate=False)  # instead of inventory
    coins = AttributeProperty(default=1, autocreate=False)  # coin loot

    # if this npc is attacked, everyone with the same tag in the current location will also be
    # pulled into combat.
    group = TagProperty("npcs")

    @property
    def strength(self):
        return self.hit_dice

    @property
    def dexterity(self):

    @property
    def constitution(self):

    @property
    def intelligence(self):

    @property
    def wisdom(self):

    @property
    def charisma(self):

    @property
    def hp_max(self):
        return self.hit_dice * self.hp_multiplier

        """
        Start with max health.

        """
        self.tags.add("npcs", category="group")

        """
        Called when being attacked and combat starts.

        """
        pass

    def ai_next_action(self, **kwargs):
        """
        The combat engine should ask this method in order to
        get the next action the npc should perform in combat.

        """
        pass

class EvAdventureTalkativeNPC(EvAdventureNPC):
    """
    Talkative NPCs can be addressed by `talk [to] <npc>`. This opens a chat menu with
    communication options. The menu is created with the npc and we override the .create
    to allow passing in the menu nodes.

    """

    menudata = AttributeProperty(dict(), autocreate=False)
    menu_kwargs = AttributeProperty(dict(), autocreate=False)
    # text shown when greeting at the start of a conversation. If this is an
    # iterable, a random reply will be chosen by the menu
    hi_text = AttributeProperty("Hi!", autocreate=False)

        """
        Talkative NPCs are generally immortal (we don't deduct HP here by default)."

        """
        attacker.msg(f'{self.key} dodges the damage and shouts "|wHey! What are you doing?|n"')

    def create(cls, key, account=None, **kwargs):
        """
        Overriding the creation of the NPC, allowing some extra `**kwargs`.

        Args:
            key (str): Name of the new object.
            account (Account, optional): Account to attribute this object to.

            description (str): Brief description for this object (same as default Evennia)
            ip (str): IP address of creator (for object auditing) (same as default Evennia).
            menudata (dict or str): The `menudata` argument to `EvMenu`. This is either a dict of
                `{"nodename": <node_callable>,...}` or the python-path to a module containing
                such nodes (see EvMenu docs). This will be used to generate the chat menu
                chat menu for the character that talks to the NPC (which means the `at_talk` hook
                is called (by our custom `talk` command).
            menu_kwargs (dict): This will be passed as `**kwargs` into `EvMenu` when it
                is created. Make sure this dict can be pickled to an Attribute.

        Returns:
            tuple: `(new_character, errors)`. On error, the `new_character` is `None` and
            `errors` is a `list` of error strings (an empty list otherwise).

        """
        menudata = kwargs.pop("menudata", None)
        menu_kwargs = kwargs.pop("menu_kwargs", {})

        # since this is a @classmethod we can't use super() here
        new_object, errors = EvAdventureNPC.create(
            key, account=account, attributes=(("menudata", menudata), ("menu_kwargs", menu_kwargs))
        )

        return new_object, errors

    def at_talk(self, talker, startnode="node_start", session=None, **kwargs):
        """
        Called by the `talk` command when another entity addresses us.

        Args:
            talker (Object): The one talking to us.
            startnode (str, optional): Allows to start in a different location in the menu tree.
                The given node must exist in the tree.
            session (Session, optional): The talker's current session, allows for routing
                correctly in multi-session modes.
            **kwargs: This will be passed into the `EvMenu` creation and appended and `menu_kwargs`
                given to the NPC at creation.

        Notes:
            We pass `npc=self` into the EvMenu for easy back-reference. This will appear in the
            `**kwargs` of the start node.

        """
        menu_kwargs = {**self.menu_kwargs, **kwargs}
        EvMenu(talker, self.menudata, startnode=startnode, session=session, npc=self, **menu_kwargs)

    """
    This is the intended start menu node for the Talkative NPC interface. It will
    use on-npc Attributes to build its message and will also pick its options
    based on nodes named `node_start_*` are available in the node tree.

    """
    # we presume a back-reference to the npc this is added when the menu is created
    npc = kwargs["npc"]

    # grab a (possibly random) welcome text
    text = choice(make_iter(npc.hi_text))

    # determine options based on `node_start_*` nodes available
    toplevel_node_keys = [
        node_key for node_key in caller.ndb._evmenu._menutree if node_key.startswith("node_start_")
    ]
    options = []
    for node_key in toplevel_node_keys:
        option_name = node_key[11:].replace("_", " ").capitalized()

        # we let the menu number the choices, so we don't use key here
        options.append({"desc": option_name, "goto": node_key})

class EvAdventureQuestGiver(EvAdventureTalkativeNPC):
    """
    An NPC that acts as a dispenser of quests.

    """

class EvAdventureShopKeeper(EvAdventureTalkativeNPC):
    """
    ShopKeeper NPC.

    """

    # how much extra the shopkeeper adds on top of the item cost
    upsell_factor = AttributeProperty(1.0, autocreate=False)
    # how much of the raw cost the shopkeep is willing to pay when buying from character
    miser_factor = AttributeProperty(0.5, autocreate=False)
    # prototypes of common wares
    common_ware_prototypes = AttributeProperty([], autocreate=False)

        """
        Immortal - we don't deduct any damage here.

        """
        attacker.msg(
            f"{self.key} brushes off the hit and shouts "
            '"|wHey! This is not the way to get a discount!|n"'
        )

class EvAdventureMob(EvAdventureNPC):
    """
    Mob (mobile) NPC; this is usually an enemy.

    """

    # chance (%) that this enemy will loot you when defeating you
    loot_chance = AttributeProperty(75, autocreate=False)

        """
        Called to get the next action in combat.

        Args:
            combathandler (EvAdventureCombatHandler): The currently active combathandler.

        Returns:
            tuple: A tuple `(str, tuple, dict)`, being the `action_key`, and the `*args` and
            `**kwargs` for that action. The action-key is that of a CombatAction available to the
            combatant in the current combat handler.

        """
        from .combat import CombatActionAttack, CombatActionDoNothing

        if self.is_idle:
            # mob just stands around
            return CombatActionDoNothing.key, (), {}

        target = choice(combathandler.get_enemy_targets(self))

        # simply randomly decide what action to take
        action = choice(
            (
                CombatActionAttack,
                CombatActionDoNothing,
            )
        )
        return action.key, (target,), {}

        """
        Mobs die right away when defeated, no death-table rolls.

        """

    def at_do_loot(self, looted):
        """
        Called when mob gets to loot a PC.

        """
        if dice.roll("1d100") > self.loot_chance:
            # don't loot
            return

        if looted.coins:
            # looter prefer coins
            loot = dice.roll("1d20")
            if looted.coins < loot:
                self.location.msg_location(
                    "$You(looter) loots $You() for all coin!",
                    from_obj=looted,
                    mapping={"looter": self},
                )
            else:
                    "$You(looter) loots $You() for |y{loot}|n coins!",
                )
        elif hasattr(looted, "equipment"):
            # go through backpack, first usable, then wieldable, wearable items
            # and finally stuff wielded
            stealable = looted.equipment.get_usable_objects_from_backpack()
            if not stealable:
                stealable = looted.equipment.get_wieldable_objects_from_backpack()
                stealable = looted.equipment.get_wearable_objects_from_backpack()
                stealable = [looted.equipment.slots[WieldLocation.SHIELD_HAND]]
                stealable = [looted.equipment.slots[WieldLocation.HEAD]]
                stealable = [looted.equipment.slots[WieldLocation.ARMOR]]
                stealable = [looted.equipment.slots[WieldLocation.WEAPON_HAND]]
                stealable = [looted.equipment.slots[WieldLocation.TWO_HANDS]]

            stolen = looted.equipment.remove(choice(stealable))
            stolen.location = self

            self.location.msg_location(
                "$You(looter) steals {stolen.key} from $You()!",
                from_obj=looted,
                mapping={"looter": self},
            )

"""
All items in the game inherit from a base object. The properties (what you can do
with an object, such as wear, wield, eat, drink, kill etc) are all controlled by
Tags.

Every object has one of a few `obj_type`-category tags:
- weapon
- armor
- shield
- helmet
- consumable  (potions, torches etc)
- magic (runestones, magic items)
- quest (quest-items)
- treasure  (valuable to sell)

It's possible for an item to have more than one tag, such as a golden helmet (helmet+treasure) or
rune sword (weapon+quest).

"""

from evennia import AttributeProperty, create_object, search_object
from evennia.objects.objects import DefaultObject

from .enums import Ability, ObjType, WieldLocation

_BARE_HANDS = None

class EvAdventureObject(DefaultObject):
    """
    Base in-game entity.

    """

    # inventory management
    inventory_use_slot = AttributeProperty(WieldLocation.BACKPACK)
    # how many inventory slots it uses (can be a fraction)
    size = AttributeProperty(1)
    value = AttributeProperty(0)

    # can also be an iterable, for adding multiple obj-type tags
    obj_type = ObjType.GEAR

        for obj_type in make_iter(self.obj_type):
            self.tags.add(obj_type.value, category="obj_type")

    def get_display_header(self, looker, **kwargs):
        return ""  # this is handled by get_obj_stats

        return get_obj_stats(self, owner=looker)

    def has_obj_type(self, objtype):
        """
        Check if object is of a particular type.

        typeobj_enum (enum.ObjType): A type to check, like enums.TypeObj.TREASURE.

        """
        return objtype.value in make_iter(self.obj_type)

    def get_help(self):
        """
        Get help text for the item.

        Returns:
            str: The help text, by default taken from the `.help_text` property.

        """
        return "No help for this item."

    def at_pre_use(self, *args, **kwargs):
        """
        Called before use. If returning False, usage should be aborted.
        """

    def use(self, *args, **kwargs):
        """
        Use this object, whatever that may mean.

        """

    def at_post_use(self, *args, **kwargs):
        """
        Called after use happened.
        """
        pass

class EvAdventureObjectFiller(EvAdventureObject):
    """
    In _Knave_, the inventory slots act as an extra measure of how you are affected by
    various averse effects. For example, mud or water could fill up some of your inventory
    slots and make the equipment there unusable until you cleaned it. Inventory is also
    used to track how long you can stay under water etc - the fewer empty slots you have,
    the less time you can stay under water due to carrying so much stuff with you.

    This class represents such an effect filling up an empty slot. It has a quality of 0,
    meaning it's unusable.

    """

    obj_type = ObjType.QUEST.value  # can't be sold
    quality = AttributeProperty(0)

class EvAdventureQuestObject(EvAdventureObject):
    """
    A quest object. These cannot be sold and only be used for quest resolution.

    """

    obj_type = ObjType.QUEST

class EvAdventureTreasure(EvAdventureObject):
    """
    A 'treasure' is mainly useful to sell for coin.

    """

    obj_type = ObjType.TREASURE
    value = AttributeProperty(100, autocreate=False)

class EvAdventureConsumable(EvAdventureObject):
    """
    Item that can be 'used up', like a potion or food. Weapons, armor etc does not
    have a limited usage in this way.

    """

    obj_type = ObjType.CONSUMABLE
    size = AttributeProperty(0.25, autocreate=False)
    uses = AttributeProperty(1, autocreate=False)

    def at_pre_use(self, user, target=None, *args, **kwargs):
        if target and user.location != target.location:
            user.msg("You are not close enough to the target!")

        if self.uses <= 0:
            user.msg(f"|w{self.key} is used up.|n")

        return super().at_pre_use(user, target=target, *args, **kwargs)

    def use(self, user, target=None, *args, **kwargs):
        """
        Use the consumable.

        """

        if user.location:
            user.location.msg_contents(
                f"$You() $conj(use) {self.get_display_name(user)}.", from_obj=user
            )

    def at_post_use(self, user, *args, **kwargs):
        """
        Called after this item was used.

        Args:
            user (Object): The one using the item.
            *args, **kwargs: Optional arguments.

        """
        self.uses -= 1
            user.msg(f"|w{self.key} was used up.|n")

class EvAdventureWeapon(EvAdventureObject):
    """
    Base weapon class for all EvAdventure weapons.

    """

    obj_type = ObjType.WEAPON
    inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND)
    quality = AttributeProperty(3)

    # what ability used to attack with this weapon
    attack_type = AttributeProperty(Ability.STR)
    # what defense stat of the enemy it must defeat
    defense_type = AttributeProperty(Ability.ARMOR)
    damage_roll = AttributeProperty("1d6")

    def get_display_name(self, looker=None, **kwargs):
        quality = self.quality

        quality_txt = ""
        if quality <= 0:
            quality_txt = "|r(broken!)|n"
        elif quality < 2:
            quality_txt = "|y(damaged)|n"
        elif quality < 3:
            quality_txt = "|Y(chipped)|n"

        return super().get_display_name(looker=looker, **kwargs) + quality_txt

            # we assume weapons can only be used in the same location

        if self.quality is not None and self.quality <= 0:
            user.msg(f"{self.get_display_name(user)} is broken and can't be used!")

    def use(self, attacker, target, *args, advantage=False, disadvantage=False, **kwargs):
        """When a weapon is used, it attacks an opponent"""

        location = attacker.location

        is_hit, quality, txt = rules.dice.opposed_saving_throw(
            attacker,
            target,
            attack_type=self.attack_type,
            advantage=advantage,
            disadvantage=disadvantage,
        )
            f"$You() $conj(attack) $You({target.key}) with {self.key}: {txt}",
            from_obj=attacker,
            mapping={target.key: target},
        )
        if is_hit:
            # enemy hit, calculate damage
            dmg = rules.dice.roll(self.damage_roll)

            if quality is Ability.CRITICAL_SUCCESS:
                # doble damage roll for critical success
                dmg += rules.dice.roll(self.damage_roll)
                message = (
                    f" $You() |ycritically|n $conj(hit) $You({target.key}) for |r{dmg}|n damage!"
                )
            else:
                message = f" $You() $conj(hit) $You({target.key}) for |r{dmg}|n damage!"

            location.msg_contents(message, from_obj=attacker, mapping={target.key: target})
            # call hook
            target.at_damage(dmg, attacker=attacker)

        else:
            # a miss
            message = f" $You() $conj(miss) $You({target.key})."
            if quality is Ability.CRITICAL_FAILURE:
                message += ".. it's a |rcritical miss!|n, damaging the weapon."
                if self.quality is not None:
                    self.quality -= 1
                location.msg_contents(message, from_obj=attacker, mapping={target.key: target})

            user.msg(f"|r{self.get_display_name(user)} breaks and can no longer be used!")

class EvAdventureThrowable(EvAdventureWeapon, EvAdventureConsumable):
    """
    Something you can throw at an enemy to harm them once, like a knife or exploding potion/grenade.

    Note: In Knave, ranged attacks are done with WIS (representing the stillness of your mind?)

    """

    obj_type = (ObjType.THROWABLE, ObjType.WEAPON, ObjType.CONSUMABLE)

    attack_type = AttributeProperty(Ability.WIS)
    defense_type = AttributeProperty(Ability.DEX)

class EvAdventureRunestone(EvAdventureWeapon, EvAdventureConsumable):
    """
    Base class for magic runestones. In _Knave_, every spell is represented by a rune stone
    that takes up an inventory slot. It is wielded as a weapon in order to create the specific
    magical effect provided by the stone. Normally each stone can only be used once per day but
    they are quite powerful (and scales with caster level).

    """

    obj_type = (ObjType.WEAPON, ObjType.MAGIC)
    inventory_use_slot = WieldLocation.TWO_HANDS

    attack_type = AttributeProperty(Ability.INT)
    damage_roll = AttributeProperty("1d8")

        """Called after the spell was cast"""
        # the rune stone is not deleted after use, but
        # it needs to be refreshed after resting.

    def refresh(self):
        self.uses = 1

class EvAdventureArmor(EvAdventureObject):
    """
    Base class for all wearable Armors.

    """

    obj_type = ObjType.ARMOR
    inventory_use_slot = WieldLocation.BODY

    armor = AttributeProperty(1)

class EvAdventureShield(EvAdventureArmor):
    """
    Base class for all Shields.

    """

    obj_type = ObjType.SHIELD
    inventory_use_slot = WieldLocation.SHIELD_HAND

class EvAdventureHelmet(EvAdventureArmor):
    """
    Base class for all Helmets.

    """

    obj_type = ObjType.HELMET
    inventory_use_slot = WieldLocation.HEAD

class WeaponBareHands(EvAdventureWeapon):
    """
    This is a dummy-class loaded when you wield no weapons. We won't create any db-object for it.

    """

    key = "Bare hands"
    inventory_use_slot = WieldLocation.WEAPON_HAND
    attack_type = Ability.STR
    defense_type = Ability.ARMOR
    damage_roll = "1d4"
    quality = None  # let's assume fists are always available ...

def get_bare_hands():
    """
    Get the bare-hands singleton object.

    Returns:
        WeaponBareHands
    """
    global _BARE_HANDS

    if not _BARE_HANDS:
        _BARE_HANDS = search_object("Bare hands", typeclass=WeaponBareHands).first()
        _BARE_HANDS = create_object(WeaponBareHands, key="Bare hands")
    return _BARE_HANDS

"""
A simple quest system for EvAdventure.

A quest is represented by a quest-handler sitting as
.quest on a Character. Individual Quests are objects
that track the state and can have multiple steps, each
of which are checked off during the quest's progress.

The player can use the quest handler to track the
progress of their quests.

A quest ending can mean a reward or the start of
another quest.

"""

from copy import copy, deepcopy

from evennia.utils import dbserialize

class EvAdventureQuest:
    """
    This represents a single questing unit of quest.

        name (str): Main identifier for the quest.
        category (str, optional): This + name must be globally unique.
    it ends - it then pauses after the last completed step.

    Each step of the quest is represented by a `.step_<stepname>` method. This should check
    the status of the quest-step and update the `.current_step` or call `.complete()`. There
    are also `.help_<stepname>` which is either a class-level help string or a method
    returning a help text. All properties should be stored on the quester.

    Example:
    ```py
    class MyQuest(EvAdventureQuest):
        '''A quest with two steps that ar'''

        start_step = "A"

        help_A = "You need a '_quest_A_flag' on yourself to finish this step!"
        help_B = "Finally, you need more than 4 items in your inventory!"

        def step_A(self, *args, **kwargs):
            if self.quester.db._quest_A_flag == True:
                self.quester.msg("Completed the first step of the quest.")
                self.current_step = "end"
                self.progress()

        def step_end(self, *args, **kwargs):
            if len(self.quester.contents) > 4:
                self.quester.msg("Quest complete!")
                self.complete()
    ```
    """

    key = "basequest"
    desc = "This is the base quest class"
    start_step = "start"

    completed_text = "This quest is completed!"
    abandoned_text = "This quest is abandoned."

    # help entries for quests (could also be methods)
    help_start = "You need to start first"
    help_end = "You need to end the quest"

    def __init__(self, quester, start_step=None):
        if " " in self.key:
            raise TypeError("The Quest name must not have spaces in it.")

        self.quester = quester
        self._current_step = start_step or self.start_step
        self.is_completed = False
        self.is_abandoned = False

    def __serialize_dbobjs__(self):
        self.quester = dbserialize.dbserialize(self.quester)

    def __deserialize_dbobjs__(self):
        if isinstance(self.quester, bytes):
            self.quester = dbserialize.dbunserialize(self.quester)

    @property
    def questhandler(self):
        return self.quester.quests

    @property
    def current_step(self):
        return self._current_step

    @current_step.setter
    def current_step(self, step_name):
        self._current_step = step_name
        self.questhandler.do_save = True

    def abandon(self):
        """
        Call when quest is abandoned.

        """
        self.is_abandoned = True

    def complete(self):
        """
        Call this to end the quest.

        """
        self.is_completed = True

    def progress(self, *args, **kwargs):
        """
        This is called whenever the environment expects a quest may need stepping. This will
        determine which quest-step we are on and run `step_<stepname>`, which in turn will figure
        out if the step is complete or not.

        Args:
            *args, **kwargs: Will be passed into the step method.

        """
        if not (self.is_completed or self.is_abandoned):
            getattr(self, f"step_{self.current_step}")(*args, **kwargs)

    def help(self):
        """
        This is used to get help (or a reminder) of what needs to be done to complete the current
        quest-step.

        Returns:
            str: The help text for the current step.

        """
        if self.is_completed:
            return self.completed_text
        if self.is_abandoned:
            return self.abandoned_text

        help_resource = (
            getattr(self, f"help_{self.current_step}", None)
            or "You need to {self.current_step} ..."
        )
        if callable(help_resource):
            # the help_<current_step> can be a method to call
            return help_resource()
        else:
            # normally it's just a string
            return str(help_resource)

    # step methods and hooks

    def step_start(self, *args, **kwargs):
        """
        Example step that completes immediately.

        """
        self.complete()

        """
        This is called both when completing the quest, or when it is abandoned prematurely.
        Make sure to cleanup any quest-related data stored when following the quest.

        """
        pass

class EvAdventureQuestHandler:
    """
    This sits on the Character, as `.quests`.

    It's initiated using a lazy property on the Character:

    ```
    ```

    """

    quest_storage_attribute_key = "_quests"
    quest_storage_attribute_category = "evadventure"

        self.do_save = False

        self.storage = self.obj.attributes.get(
            self.quest_storage_attribute_key,
            category=self.quest_storage_attribute_category,
            default={},
        )

        self.obj.attributes.add(
            self.storage,
        )
        self._load()  # important

    def has(self, quest_key):
        """
        Check if a given quest is registered with the Character.

        Args:
            quest_key (str): The name of the quest to check for.
            quest_category (str, optional): Quest category, if any.

        Returns:
            bool: If the character is following this quest or not.

        """
        return bool(self.storage.get(quest_key))

    def get(self, quest_key):
        """
        Get the quest stored on character, if any.

        Args:

        Returns:
            EvAdventureQuest or None: The quest stored, or None if
                Character is not on this quest.

        """
        return self.storage.get(quest_key)

    def add(self, quest):
        """
        Add a new quest

        Args:
            quest (EvAdventureQuest): The quest class to start.

        """
        self.storage[quest.key] = quest(self.obj)

    def remove(self, quest_key):
        """
        Remove a quest. If not complete, it will be abandoned.

        Args:
            quest_key (str): The quest to remove.

        """
        quest = self.storage.pop(quest_key, None)
        if not quest.is_completed:
            # make sure to cleanup
            quest.abandon()

    def get_help(self, quest_key=None):
        """
        Get help text for a quest or for all quests. The help text is
        a combination of the description of the quest and the help-text
        of the current step.

        Args:
            quest_key (str, optional): The quest-key. If not given, get help for all
                quests in handler.

        Returns:
            list: Help texts, one for each quest, or only one if `quest_key` is given.

        """
        help_texts = []
        if quest_key in self.storage:
            quests = [self.storage[quest_key]]
        else:
            quests = self.storage.values()

        for quest in quests:
            help_texts.append(f"|c{quest.key}|n\n {quest.desc}\n\n - {quest.help()}")
        return help_texts

    def progress(self, quest_key=None, *args, **kwargs):
        """
        Check progress of a given quest or all quests.

        Args:
            quest_key (str, optional): If given, check the progress of this quest (if we have it),
                otherwise check progress on all quests.
            *args, **kwargs: Will be passed into each quest's `progress` call.

        """
        else:

            quest.progress(*args, **kwargs)

        if self.do_save:
            # do_save is set by the quest

"""
EvAdventure rooms.

The base EvAdventure room has a modified display header that shows a little mini-map.

All EvAdventure rooms inherit from this room, and it is integral to combat as well as
the dungeon generation. But one can also mix with other non-EvAdventure rooms (you will
just not be able to fight in them).

"""

from copy import deepcopy

from evennia import AttributeProperty, DefaultCharacter, DefaultRoom

CHAR_SYMBOL = "|w@|n"
CHAR_ALT_SYMBOL = "|w>|n"
ROOM_SYMBOL = "|bo|n"
LINK_COLOR = "|B"

_MAP_GRID = [
    [" ", " ", " ", " ", " "],
    [" ", " ", "@", " ", " "],
]
    "north": (0, 1, "||"),
    "east": (1, 0, "-"),
    "south": (0, -1, "||"),
    "west": (-1, 0, "-"),
    "northeast": (1, 1, "/"),
    "southeast": (1, -1, "\\"),
    "southwest": (-1, -1, "/"),
    "northwest": (-1, 1, "\\"),
}

class EvAdventureRoom(DefaultRoom):
    """
    Simple room supporting some EvAdventure-specifics.

    """

    allow_combat = AttributeProperty(False, autocreate=False)
    allow_pvp = AttributeProperty(False, autocreate=False)
    allow_death = AttributeProperty(False, autocreate=False)

    def format_appearance(self, appearance, looker, **kwargs):
        """Don't left-strip the appearance string"""
        return appearance.rstrip()

        """
        Display the current location as a mini-map.

        """
        # make sure to not show make a map for users of screenreaders.
        # for optimization we also don't show it to npcs/mobs
        if not inherits_from(looker, DefaultCharacter) or (
            looker.account and looker.account.uses_screenreader()
        ):
            return ""

        # build a map
        map_grid = deepcopy(_MAP_GRID)
        dx0, dy0 = 2, 2
        map_grid[dy0][dx0] = CHAR_SYMBOL
        for exi in self.exits:
            dx, dy, symbol = _EXIT_GRID_SHIFT.get(exi.key, (None, None, None))
            if symbol is None:
                # we have a non-cardinal direction to go to - indicate this
                map_grid[dy0][dx0] = CHAR_ALT_SYMBOL
                continue
            map_grid[dy0 + dy][dx0 + dx] = f"{LINK_COLOR}{symbol}|n"
            if exi.destination != self:
                map_grid[dy0 + dy + dy][dx0 + dx + dx] = ROOM_SYMBOL

        # Note that on the grid, dy is really going *downwards* (origo is
        # in the top left), so we need to reverse the order at the end to mirror it
        # vertically and have it come out right.
        return "  " + "\n  ".join("".join(line) for line in reversed(map_grid))

class EvAdventurePvPRoom(EvAdventureRoom):
    """
    Room where PvP can happen, but noone gets killed.

    """

    allow_pvp = AttributeProperty(True, autocreate=False)

        """
        Customize footer of description.
        """
        return "|yNon-lethal PvP combat is allowed here!|n"

"""
MUD ruleset based on the _Knave_ OSR tabletop RPG by Ben Milton (modified for MUD use).

The center of the rule system is the "RollEngine", which handles all rolling of dice
and determining what the outcome is.

----

"""

from .random_tables import death_and_dismemberment as death_table

# Basic rolls

class EvAdventureRollEngine:
    """
    This groups all dice rolls of EvAdventure. These could all have been normal functions, but we
    are group them in a class to make them easier to partially override and replace later.

    """

    def roll(self, roll_string, max_number=10):
        """
        NOTE: In evennia/contribs/rpg/dice/ is a more powerful dice roller with
        more features, such as modifiers, secret rolls etc. This is much simpler and only
        gets a simple sum of normal rpg-dice.

        Args:
            roll_string (str): A roll using standard rpg syntax, <number>d<diesize>, like
                1d6, 2d10 etc. Max die-size is 1000.
            max_number (int): The max number of dice to roll. Defaults to 10, which is usually
                more than enough.

        Returns:
            int: The rolled result - sum of all dice rolled.

        Raises:
            TypeError: If roll_string is not on the right format or otherwise doesn't validate.

        Notes:
            Since we may see user input to this function, we make sure to validate the inputs (we
            wouldn't bother much with that if it was just for developer use).

        """
        max_diesize = 1000
        roll_string = roll_string.lower()
        if "d" not in roll_string:
                f"Dice roll '{roll_string}' was not recognized. Must be `<number>d<dicesize>`."
            )
        number, diesize = roll_string.split("d", 1)
        try:
            number = int(number)
            diesize = int(diesize)
            raise TypeError(f"The number and dice-size of '{roll_string}' must be numerical.")
        if 0 < number > max_number:
            raise TypeError(f"Invalid number of dice rolled (must be between 1 and {max_number})")
        if 0 < diesize > max_diesize:
            raise TypeError(f"Invalid die-size used (must be between 1 and {max_diesize} sides)")

        # At this point we know we have valid input - roll and add dice together
        return sum(randint(1, diesize) for _ in range(number))

    def roll_with_advantage_or_disadvantage(self, advantage=False, disadvantage=False):
        """
        Base roll of d20, or 2d20, based on dis/advantage given.

        Args:
            bonus (int): The ability bonus to apply, like strength or charisma.
            advantage (bool): Roll 2d20 and use the bigger number.
            disadvantage (bool): Roll 2d20 and use the smaller number.

        Notes:
            Disadvantage and advantage cancel each other out.

        """
        if not (advantage or disadvantage) or (advantage and disadvantage):
            # normal roll, or advantage cancels disadvantage
            return self.roll("1d20")
        elif advantage:
            return max(self.roll("1d20"), self.roll("1d20"))
        else:
            return min(self.roll("1d20"), self.roll("1d20"))

    def saving_throw(
        self,
        character,
        bonus_type=Ability.STR,
        target=15,
        advantage=False,
        disadvantage=False,
        modifier=0,
    ):
        """
        A saving throw without a clear enemy to beat. In _Knave_ all unopposed saving
        throws always tries to beat 15, so (d20 + bonus + modifier) > 15.

        Args:
            character (Object): The one attempting to save themselves.
            bonus_type (enum.Ability): The ability bonus to apply, like strength or
                charisma.
            target (int, optional): Used for opposed throws (in Knave any regular
                saving through must always beat 15).
            advantage (bool, optional): Roll 2d20 and use the bigger number.
            disadvantage (bool, optional): Roll 2d20 and use the smaller number.
            modifier (int, optional): An additional +/- modifier to the roll.

        Returns:
            tuple: A tuple `(bool, str, str)`. The bool indicates if the save was passed or not.
                The second element is the quality of the roll - None (normal),
                "critical fail" and "critical success". Last element is a text detailing
                the roll, for display purposes.
        Notes:
            Advantage and disadvantage cancel each other out.

        Example:
            Trying to overcome the effects of poison, roll d20 + Constitution-bonus above 15.

        """
        bonus = getattr(character, bonus_type.value, 1)
        dice_roll = self.roll_with_advantage_or_disadvantage(advantage, disadvantage)
        if dice_roll == 1:
            quality = Ability.CRITICAL_FAILURE
        elif dice_roll == 20:
            quality = Ability.CRITICAL_SUCCESS
        else:
            quality = None
        result = dice_roll + bonus + modifier > target

        # determine text output
        rolltxt = "d20 "
        if advantage and disadvantage:
            rolltxt = "d20 (advantage canceled by disadvantage)"
            rolltxt = "|g2d20|n (advantage: picking highest) "
        elif disadvantage:
            rolltxt = "|r2d20|n (disadvantage: picking lowest) "
        bontxt = f"(+{bonus})"
        modtxt = ""
            modtxt = f"+ {modifier}" if modifier > 0 else f" - {abs(modifier)}"
        qualtxt = f" ({quality.value}!)" if quality else ""

        txt = (
            f" rolled {dice_roll} on {rolltxt} "
            f"+ {bonus_type.value}{bontxt}{modtxt} vs "
            f"{target} -> |w{'|GSuccess|w' if result else '|RFail|w'}{qualtxt}|n"
        )

        return (dice_roll + bonus + modifier) > target, quality, txt

    def opposed_saving_throw(
        self,
        attacker,
        defender,
        attack_type=Ability.STR,
        defense_type=Ability.ARMOR,
    ):
        """
        An saving throw that tries to beat an active opposing side.

        Args:
            attacker (Character): The attacking party.
            defender (Character): The one defending.
            attack_type (str): Which ability to use in the attack, like 'strength' or 'willpower'.
                Minimum is always 1.
            defense_type (str): Which ability to defend with, in addition to 'armor'.
                Minimum is always 11 (bonus + 10 is always the defense in _Knave_).
            modifier (int): An additional +/- modifier to the roll.

        Returns:
            tuple: (bool, str, str): If the attack succeed or not. The second element is the
                quality of the roll - None (normal), "critical fail" and "critical success". Last
                element is a text that summarizes the details of the roll.
        Notes:

        """
        # what is stored on the character/npc is the bonus; we add 10 to get the defense target
        defender_defense = getattr(defender, defense_type.value, 1) + 10

        result, quality, txt = self.saving_throw(
            attacker,
            bonus_type=attack_type,
            target=defender_defense,
            modifier=modifier,
        )
        txt = f"Roll vs {defense_type.value}({defender_defense}):\n{txt}"

        return result, quality, txt

    def roll_random_table(self, dieroll, table_choices):
        """
        Make a roll on a random table.

        Args:
            dieroll (str): The dice to roll, like 1d6, 1d20, 3d6 etc).
            table_choices (iterable): If a list of single elements, the die roll
                should fully encompass the table, like a 1d20 roll for a table
                with 20 elements. If each element is a tuple, the first element
                of the tuple is assumed to be a string 'X-Y' indicating the
                range of values that should match the roll.

        Returns:
            Any: The result of the random roll.

        Example:
            `roll table_choices = [('1-5', "Blue"), ('6-9': "Red"), ('10', "Purple")]`

        Notes:
            If the roll is outside of the listing, the closest edge value is used.

        """
        roll_result = self.roll(dieroll)
        if not table_choices:

        if isinstance(table_choices[0], (tuple, list)):
            # tuple with range conditional, like ('1-5', "Blue") or ('10', "Purple")
            max_range = -1
            min_range = 10**6
            for valrange, choice in table_choices:
                minval, *maxval = valrange.split("-", 1)
                minval = abs(int(minval))
                maxval = abs(int(maxval[0]) if maxval else minval)

                # we store the largest/smallest values so far in case we need to use them
                max_range = max(max_range, maxval)
                min_range = min(min_range, minval)

                if minval <= roll_result <= maxval:

            # if we have no result, we are outside of the range, we pick the edge values. It is also
            # possible the range contains 'gaps', but that'd be an error in the random table itself.
            if roll_result > max_range:
                return table_choices[-1][1]
            else:
                return table_choices[0][1]
        else:
            # regular list - one line per value.
            roll_result = max(1, min(len(table_choices), roll_result))
            return table_choices[roll_result - 1]

    # specific rolls / actions

    def morale_check(self, defender):
        """
        A morale check is done for NPCs/monsters. It's done with a 2d6 against
        their morale.

        Args:
            defender (NPC): The entity trying to defend its morale.

        Returns:
            bool: False if morale roll failed, True otherwise.

        """
        return self.roll("2d6") <= defender.morale

    def heal_from_rest(self, character):
        """
        A meal and a full night's rest allow for regaining 1d8 + Const bonus HP.

        Args:
            character (Character): The one resting.

        """
        character.heal(self.roll("1d8") + character.constitution)

    death_map = {
        "weakened": "strength",
        "unsteady": "dexterity",
        "sickly": "constitution",
        "addled": "intelligence",
        "rattled": "wisdom",
        "disfigured": "charisma",
    }

    def roll_death(self, character):
        """
        Happens when hitting <= 0 hp. unless dead,

        """

        result = self.roll_random_table("1d8", death_table)
        if result == "dead":
            character.at_death()
        else:
            # survives with degraded abilities (1d4 roll)
            abi = self.death_map[result]

            current_abi = getattr(character, abi)
            loss = self.roll("1d4")

            current_abi -= loss

            if current_abi < -10:
                # can't lose more - die
                character.at_death()
            else:
                # refresh health, but get permanent ability loss
                new_hp = self.roll("1d4")
                character.heal(new_hp)
                setattr(character, abi, current_abi)

                character.msg(
                    "~" * 78 + "\n|yYou survive your brush with death, "
                    f"but are |r{result.upper()}|y and permanently |rlose {loss} {abi}|y.|n\n"
                    f"|GYou recover |g{new_hp}|G health|.\n" + "~" * 78
                )

# singletons

# access rolls e.g. with rules.dice.opposed_saving_throw(...)
dice = EvAdventureRollEngine()

"""
EvAdventure Shop system.

A shop is run by an NPC. It can provide one or more of several possible services:

- Buy from a pre-set list of (possibly randomized) items. Cost is based on the item's value,
  adjusted by how stingy the shopkeeper is. When bought this way, the item is
  generated on the fly and passed to the player character's inventory. Inventory files are
  a list of prototypes, normally from a prototype-file. A random selection of items from each
  inventory file is available.
- Sell items to the shop for a certain percent of their value. One could imagine being able
  to buy back items again, but we will instead _destroy_ sold items, so as to remove them
  from circulation. In-game we can say it's because the merchants collect the best stuff
  to sell to collectors in the big city later. Each merchant buys a certain subset of items
  based on their tags.
- Buy a service. For a cost, a certain action is performed for the character; this applies
  immediately when bought. The most notable services are healing and converting coin to XP.
- Buy rumors - this is echoed to the player for a price. Different merchants could have
  different rumors (or randomized ones).
- Quest - gain or hand in a quest for a merchant.

All shops are menu-driven. One starts talking to the npc and will then end up in their shop
interface.

This is a series of menu nodes meant to be added as a mapping via
`EvAdventureShopKeeper.create(menudata={},...)`.

To make this pluggable, the shopkeeper start page will analyze the available nodes
and auto-add options to all nodes in the three named `node_start_*`. The last part of the
node name will be the name of the option capitalized, with underscores replaced by spaces, so
`node_start_sell_items` will become a top-level option `Sell items`.

"""

from evennia.prototypes.prototypes import search_prototype
from evennia.prototypes.spawner import flatten_prototype, spawn

from .enums import ObjType, WieldLocation

# ------------------------------------ Buying from an NPC

class BuyItem:
    """
    Storage container for storing generic info about an item for sale. This means it can be used
    both for real objects and for prototypes without constantly having to track which is which.

    """

    # skipping typehints here since we are not using them anywhere else

    # available for all buyable items
    key = ""
    desc = ""
    size = 1
    value = 0
    use_slot = WieldLocation.BACKPACK

    uses = None
    quality = None
    attack_type = None
    defense_type = None
    damage_roll = None

    # references the original (always only one of the two)
    obj = None

    def create_from_obj(obj, shopkeeper):
        """
        Build a new BuyItem container from a real db obj.

        Args:
            obj (EvAdventureObject): An object to analyze.
            shopkeeper (EvAdventureShopKeeper): The shopkeeper.

        Returns:
            BuyItem: A general representation of the original data.

        """
        try:
            # mandatory
            key = obj.key
            desc = obj.db.desc
            obj_type = obj.obj_type
            size = obj.size
            use_slot = obj.use_slot
            value = obj.value * shopkeeper.upsell_factor
            # not a buyable item
            log_trace("Not a buyable item")

        # getting optional properties

        return BuyItem(
            key=key,
            obj_type=obj_type,
            size=size,
            use_slot=use_slot,
            value=value,
            # optional fields
            uses=getattr(obj, "uses", None),
            quality=getattr(obj, "quality", None),
            attack_type=getattr(obj, "attack_type", None),
            defense_type=getattr(obj, "defense_type", None),
            damage_roll=getattr(obj, "damage_roll", None),
            # back-reference (don't set prototype)
            obj=obj,
        )

    def create_from_prototype(self, prototype_or_key, shopkeeper):
        """
        Build a new BuyItem container from a prototype.

        Args:
            prototype (dict or key): An Evennia prototype dict or the key of one
                registered with the system. This is assumed to be a full prototype,
                including having parsed and included parentage.

        Returns:

        """

        def _get_attr_value(key, prot, optional=True):
            """
            We want the attribute's value, which is always in the `attrs` field of
            the prototype.

            """
            attr = [tup for tup in prot.get("attrs", ()) if tup[0] == key]
            try:
                return attr[0][1]
                if optional:
                raise

        if isinstance(prototype_or_key, dict):
            prototype = prototype_or_key
        else:
            # make sure to generate a 'full' prototype with all inheritance applied ('flattened'),
            # otherwise we will not get inherited data when we analyze it.
            prototype = flatten_prototype(search_prototype(key=prototype_or_key))

            log_err(f"No valid prototype '{prototype_or_key}' found")

        try:
            # at this point we should have a full, flattened prototype ready to spawn. It must
            # contain all fields needed for buying
            key = prototype["key"]
            desc = _get_attr_value("desc", prototype, optional=False)
            obj_type = _get_attr_value("obj_type", prototype, optional=False)
            size = _get_attr_value("size", prototype, optional=False)
            use_slot = _get_attr_value("use_slot", prototype, optional=False)
            value = int(
                _get_attr_value("value", prototype, optional=False) * shopkeeper.upsell_factor
            )
        except (KeyError, IndexError):

            key=key,
            uses=_get_attr_value("uses", prototype),
            quality=_get_attr_value("quality", prototype),
            attack_type=_get_attr_value("attack_type", prototype),
            defense_type=_get_attr_value("defense_type", prototype),
            damage_roll=_get_attr_value("damage_roll", prototype),
            # back-reference (don't set obj)
            prototype=prototype,
        )

        """
        Get the short description to show in buy list.

        """
        return f"{self.key} [|y{self.value}|n coins]"

    def get_detail(self):
        """
        Get more info when looking at the item.

        """
        return f"""
|c{self.key}|n  Cost: |y{self.value}|n coins

{self.desc}

Slots: |w{self.size}|n Used from: |w{self.use_slot.value}|n
Quality: |w{self.quality}|n Uses: |wself.uses|n
Attacks using: |w{self.attack_type.value}|n against |w{self.defense_type.value}|n
Damage roll: |w{self.damage_roll}"""

    def to_obj(self):
        """
        Convert this into an actual database object that we can trade. This either means
        using the stored `.prototype` to spawn a new instance of the object, or to
        use the `.obj` reference to get the already existing object.

        """
        if self.obj:
            return self.obj
        return spawn(self.prototype)

def _get_or_create_buymap(caller, shopkeep):
    """
    Helper that fetches or creates the mapping of `{"short description": BuyItem, ...}`
    we need for the buy menu. We cache it on the `_evmenu` object on the caller.

    """
    if not caller.ndb._evmenu.buymap:
        # buymap not in cache - build it and store in memory on _evmenu object - this way
        # it will be removed automatically when the menu closes. We will need to reset this
        # when the shopkeep buys new things.
        # items carried by the shopkeep are sellable (these are items already created, such as
        # things sold to the shopkeep earlier). We
        obj_wares = [BuyItem.create_from_obj(obj) for obj in list(shopkeep.contents)]
        prototype_wares = [
            BuyItem.create_from_prototype(prototype)
            for prototype in shopkeep.common_ware_prototypes
        ]
        wares = obj_wares + prototype_wares
        caller.ndb._evmenu.buymap = {str(ware): ware for ware in wares if ware}

    return caller.ndb._evmenu.buymap

# Helper functions for building the shop listings and select a ware to buy
def _get_all_wares_to_buy(caller, raw_string, **kwargs):
    """
    This helper is used by `EvMenu.list_node` to build the list of items to buy.

    We rely on `**kwargs` being forwarded from `node_start_buy`, which in turns contains
    the `npc` kwarg pointing to the shopkeeper (`caller` is the one doing the buying).

    """
    shopkeep = kwargs["npc"]
    buymap = _get_or_create_buymap(caller, shopkeep)
    return [ware_desc for ware_desc in buymap]

def _select_ware_to_buy(caller, selected_ware_desc, **kwargs):
    """
    This helper is used by `EvMenu.list_node` to operate on what the user selected.
    We return `item` in the kwargs to the `node_select_buy` node.

    """
    kwargs["item"] = buymap[selected_ware_desc]

    return "node_confirm_buy", kwargs

def _back_to_previous_node(caller, raw_string, **kwargs):
    """
    Back to previous node is achieved by returning a node of None.

    """

def _buy_ware(caller, raw_string, **kwargs):
    """
    Complete the purchase of a ware. At this point the money is deducted
    and the item is either spawned from a prototype or simply moved from
    the sellers inventory to that of the buyer.

    We will have kwargs `item` and `npc` passed along to refer to the BuyItem we bought
    and the shopkeep selling it.

    """
    item = kwargs["item"]  # a BuyItem instance

    # exchange money
    caller.coins -= item.value
    shopkeep += item.value

    # get the item - if not enough room, dump it on the ground
    obj = item.to_obj()
    try:
        caller.equipment.add(obj)
    except EquipmentError as err:
        obj.location = caller.location
        caller.msg(err)
        caller.msg(f"|w{obj.key} ends up on the ground.|n")

    caller.msg("|gYou bought |w{obj.key}|g for |y{item.value}|g coins.|n")

@list_node(_get_all_wares_to_buy, select=_select_ware_to_buy, pagesize=40)
def node_start_buy(caller, raw_string, **kwargs):
    """
    Menu node for the caller to buy items from the shopkeep. This assumes `**kwargs` contains
    a kwarg `npc` referencing the npc/shopkeep being talked to.

    Items available to sell are a combination of items in the shopkeep's inventory and
    the list of `prototypes` stored in the Shopkeep's "common_ware_prototypes` Attribute. In the
    latter case, the properties will be extracted from the prototype when inspecting it (object will
    only spawn when bought).

    """
    coins = caller.coins
    used_slots = caller.equipment.count_slots()
    max_slots = caller.equipment.max_slots

    text = (
        f'"Seeing something you like?" [you have |y{coins}|n coins, '
        f"using |b{used_slots}/{max_slots}|n slots]"
    )
    # this will be in addition to the options generated by the list-node
    extra_options = [{"key": ("[c]ancel", "b", "c", "cancel"), "goto": "node_start"}]

    return text, extra_options

def node_confirm_buy(caller, raw_string, **kwargs):
    """
    Menu node reached when a user selects an item in the buy menu. The `item` passed
    along in `**kwargs` is the selected item (see `_select_ware_to_buy`, where this is injected).

    """
    # this was injected in _select_ware_to_buy. This is an BuyItem instance.

    text = item.get_detail()
    text += f"\n\n[You have |y{coins}|n coins] and are using |b{used_slots}/{max_slots}|n slots]"

    if caller.coins >= item.value and item.size <= (max_slots - used_slots):
        options.append({"desc": f"Buy [{item.value} coins]", "goto": (_buy_ware, kwargs)})
    options.append({"desc": "Cancel", "goto": (_back_to_previous_node, kwargs)})

# node tree to inject for buying things
node_tree_buy = {"node_start_buy": node_start_buy, "node_confirm_buy": node_confirm_buy}

# ------------------------------------------------- Selling to an NPC

def _get_or_create_sellmap(self, caller, shopkeep):
    if not caller.ndb._evmenu.sellmap:
        # no sellmap, build one anew

        sellmap = {}
        for obj, wieldlocation in caller.equipment.all():
            value = int(obj.value * shopkeep.miser_factor)
            if value > 0 and obj.obj_type is not ObjType.QUEST:
                sellmap[f"|w{key}|n [{wieldlocation.value}] - sell price |y{value}|n coins"] = (
                    obj,
                    value,
                )
        caller.ndb._evmenu.sellmap = sellmap

    sellmap = caller.ndb._evmenu.sellmap

    return sellmap

def _get_all_wares_to_sell(caller, raw_string, **kwargs):
    """
    Get all wares available to sell from caller's inventory. We need to build a
    mapping between the descriptors and the items.

    """
    sellmap = _get_or_create_sellmap(caller, shopkeep)
    return [ware_desc for ware_desc in sellmap]

def _sell_ware(caller, raw_string, **kwargs):
    """
    Complete the sale of a ware. This is were money is gained and the item is removed.

    We will have kwargs `item`, `value` and `npc` passed along to refer to the inventory item we
    sold, its (adjusted) sales cost and the shopkeep buying it.

    """
    value = kwargs["value"]

    # move item to shopkeep
    obj = caller.equipment.remove(item)
    obj.location = shopkeep

    # exchange money - shopkeep always have money to pay, so we don't deduct from them
    caller.coins += value

    caller.msg("|gYou sold |w{obj.key}|g for |y{value}|g coins.|n")

def _select_ware_to_sell(caller, selected_ware_desc, **kwargs):
    """
    Selected one ware to sell. Figure out which one it is using the sellmap.
    Store the result as "item" kwarg.

    """
    kwargs["item"], kwargs["value"] = sellmap[selected_ware_desc]

    return "node_examine_sell", kwargs

@list_node(_get_all_wares_to_sell, select=_select_ware_to_sell, pagesize=20)
def node_start_sell(caller, raw_string, **kwargs):
    """
    The start-level node for selling items from the user's inventory. This assumes
    `**kwargs` contains a kwarg `npc` referencing the npc/shopkeep being talked to.

    Items available to sell are all items in the player's equipment handler, including
    things in their hands.

    """

    text = (
        f'"Anything you want to sell?" [you have |y{coins}|n coins, '
    )

def node_confirm_sell(caller, raw_string, **kwargs):
    """
    In this node we confirm the sell by first investigating the item we are about to sell.

    We have `item` and `value` available in kwargs here, added by `_select_ware_to_sell` earler.

    """

    text = caller.equipment.get_obj_stats(item)

    options = (
        {"desc": f"Sell [{value} coins]", "goto": (_sell_ware, kwargs)},
        {"desc": "Cancel", "goto": (_back_to_previous_node, kwargs)},
    )

# node tree to inject for selling things
node_tree_sell = {"node_start_sell": node_start_sell, "node_confirm_sell": node_confirm_sell}

# Full shopkeep node tree - inject into ShopKeep NPC menu to add buy/sell submenus
node_tree_shopkeep = {**node_tree_buy, **node_tree_sell}

"""
Helpers for testing evadventure modules.

"""

from .. import enums
from ..characters import EvAdventureCharacter
from ..objects import (
    EvAdventureArmor,
    EvAdventureHelmet,
    EvAdventureObject,
    EvAdventureShield,
    EvAdventureWeapon,
)
from ..rooms import EvAdventureRoom

class EvAdventureMixin:
    """
    Provides a set of pre-made characters.

    """

        self.location = create.create_object(EvAdventureRoom, key="testroom")
        self.character = create.create_object(
            EvAdventureCharacter, key="testchar", location=self.location
        )
        self.helmet = create.create_object(
            EvAdventureHelmet,
            key="helmet",
        )
        self.shield = create.create_object(
            EvAdventureShield,
            key="shield",
        )
        self.armor = create.create_object(
            EvAdventureArmor,
            key="armor",
        )
        self.weapon = create.create_object(
            EvAdventureWeapon,
            key="weapon",
        )
        self.big_weapon = create.create_object(
            key="big_weapon",
            attributes=[("inventory_use_slot", enums.WieldLocation.TWO_HANDS)],
        )
        self.item = create.create_object(EvAdventureObject, key="backpack item")

"""
Test characters.

"""

class TestCharacters(BaseEvenniaTest):
        self.character = create.create_object(EvAdventureCharacter, key="testchar")

    def test_abilities(self):
        self.character.strength += 2
        self.assertEqual(self.character.strength, 3)

    def test_heal(self):
        """Make sure we don't heal too much"""
        self.character.hp = 0
        self.character.hp_max = 8

        self.character.heal(1)
        self.assertEqual(self.character.hp, 1)
        self.character.heal(100)
        self.assertEqual(self.character.hp, 8)

    def test_at_damage(self):
        self.character.hp = 8
        self.character.at_damage(5)
        self.assertEqual(self.character.hp, 3)

    def test_at_pay(self):
        self.character.coins = 100

        result = self.character.at_pay(60)
        self.assertEqual(result, 60)
        self.assertEqual(self.character.coins, 40)

        # can't get more coins than we have
        result = self.character.at_pay(100)
        self.assertEqual(result, 40)
        self.assertEqual(self.character.coins, 0)

"""
Test chargen.

"""

from unittest.mock import MagicMock, patch

from .. import chargen, enums, objects

class EvAdventureCharacterGenerationTest(BaseEvenniaTest):
    """
    Test the Character generator in the rule engine.

    """

    @patch("evennia.contrib.tutorials.evadventure.rules.randint")
    def setUp(self, mock_randint):
        mock_randint.return_value = 10
        self.chargen = chargen.TemporaryCharacterSheet()

    def test_base_chargen(self):
        self.assertEqual(self.chargen.strength, 10)  # not realistic, due to mock
        self.assertEqual(self.chargen.armor, "gambeson")
        self.assertEqual(self.chargen.shield, "shield")
            self.chargen.backpack, ["ration", "ration", "waterskin", "waterskin", "drill", "twine"]
        )

    def test_build_desc(self):
            self.chargen.desc,
            "You are scrawny with a broken face, pockmarked skin, greased hair, hoarse speech, and "
            "stained clothing. You were a Herbalist, but you were exiled and ended up a knave. You "
            "are honest but also irascible. You tend towards neutrality.",
        )

    @patch("evennia.contrib.tutorials.evadventure.chargen.spawn")
    def test_apply(self, mock_spawn):
        gambeson = create_object(objects.EvAdventureArmor, key="gambeson")
        mock_spawn.return_value = [gambeson]
        account = MagicMock()
        account.id = 2222

        character = self.chargen.apply(account)

        self.assertIn("Herbalist", character.db.desc)
            character.equipment.all(),
            [
                (None, enums.WieldLocation.WEAPON_HAND),
                (None, enums.WieldLocation.SHIELD_HAND),
                (None, enums.WieldLocation.TWO_HANDS),
                (gambeson, enums.WieldLocation.BODY),
                (None, enums.WieldLocation.HEAD),
            ],
        )

        gambeson.delete()
        character.delete()

"""
Test EvAdventure combat.

"""

from evennia.utils.test_resources import (
    BaseEvenniaTest,
    EvenniaCommandTestMixin,
    EvenniaTestCase,
)

from .. import combat_base, combat_turnbased, combat_twitch
from ..enums import Ability, WieldLocation
from ..npcs import EvAdventureMob
from ..objects import EvAdventureConsumable, EvAdventureRunestone, EvAdventureWeapon

class _CombatTestBase(EvenniaTestCase):
    """
    Set up common entities for testing combat:

    - `location` (key=testroom)
    - `combatant` (key=testchar)
    - `target` (key=testmonster)`

    We also mock the `.msg` method of both `combatant` and `target` so we can
    see what was sent.

    """

        self.combatant = create.create_object(
        )

        self.location.allow_combat = True
        self.location.allow_death = True

        self.target = create.create_object(
            EvAdventureMob,
            key="testmonster",
            location=self.location,
            attributes=(("is_idle", True),),
        )

        # mock the msg so we can check what they were sent later
        self.combatant.msg = Mock()
        self.target.msg = Mock()

class TestEvAdventureCombatBaseHandler(_CombatTestBase):
    """
    Test the base functionality of the base combat handler.

    """

        """This also tests the `get_or_create_combathandler` classfunc"""
        self.combathandler = combat_base.EvAdventureCombatBaseHandler.get_or_create_combathandler(
            self.location, key="combathandler"
        )

    def test_combathandler_msg(self):
        """Test sending messages to all in handler"""

        self.location.msg_contents = Mock()

        self.combathandler.msg("test_message")

        self.location.msg_contents.assert_called_with(
            "test_message",
            exclude=[],
            from_obj=None,
            mapping={"testchar": self.combatant, "testmonster": self.target},
        )

    def test_get_combat_summary(self):
        """Test combat summary"""

        self.combathandler.get_sides = Mock(return_value=([self.combatant], [self.target]))

        # as seen from one side
        result = str(self.combathandler.get_combat_summary(self.combatant))

            strip_ansi(result),
            " testchar (Perfect)  vs  testmonster (Perfect) ",
        )

        # as seen from other side
        self.combathandler.get_sides = Mock(return_value=([self.target], [self.combatant]))
        result = str(self.combathandler.get_combat_summary(self.target))

            " testmonster (Perfect)  vs  testchar (Perfect) ",
        )

class TestCombatActionsBase(_CombatTestBase):
    """
    A class for testing all subclasses of CombatAction in combat_base.py

    """

        )
        # we need to mock all NotImplemented methods
        self.combathandler.get_sides = Mock(return_value=([], [self.target]))
        self.combathandler.give_advantage = Mock()
        self.combathandler.give_disadvantage = Mock()
        self.combathandler.remove_advantage = Mock()
        self.combathandler.remove_disadvantage = Mock()
        self.combathandler.get_advantage = Mock()
        self.combathandler.get_disadvantage = Mock()
        self.combathandler.has_advantage = Mock()
        self.combathandler.has_disadvantage = Mock()
        self.combathandler.queue_action = Mock()

    def test_base_action(self):
        action = combat_base.CombatAction(
            self.combathandler, self.combatant, {"key": "hold", "foo": "bar"}
        )
        self.assertEqual(action.key, "hold")
        self.assertEqual(action.foo, "bar")
        self.assertEqual(action.combathandler, self.combathandler)
        self.assertEqual(action.combatant, self.combatant)

    @patch("evennia.contrib.tutorials.evadventure.combat_base.rules.randint")
    def test_attack__miss(self, mock_randint):
        actiondict = {"key": "attack", "target": self.target}

        mock_randint.return_value = 8  # target has default armor 11, so 8+1 str will miss
        action = combat_base.CombatActionAttack(self.combathandler, self.combatant, actiondict)
        self.assertEqual(self.target.hp, 4)

    def test_attack__success(self, mock_randint):

        mock_randint.return_value = 11  # 11 + 1 str will hit beat armor 11
        self.target.hp = 20
        self.assertEqual(self.target.hp, 9)

    def test_stunt_fail(self, mock_randint):
            "key": "stunt",
            "recipient": self.combatant,
            "target": self.target,
            "advantage": True,
            "stunt_type": Ability.STR,
            "defense_type": Ability.DEX,
        }
        mock_randint.return_value = 8  # fails 8+1 dex vs DEX 11 defence
        action = combat_base.CombatActionStunt(self.combathandler, self.combatant, action_dict)
        self.combathandler.give_advantage.assert_not_called()

    def test_stunt_advantage__success(self, mock_randint):
        }
        mock_randint.return_value = 11  # 11+1 dex vs DEX 11 defence is success
        self.combathandler.give_advantage.assert_called_with(self.combatant, self.target)

    def test_stunt_disadvantage__success(self, mock_randint):
            "recipient": self.target,
            "target": self.combatant,
            "advantage": False,
        }
        self.combathandler.give_disadvantage.assert_called_with(self.target, self.combatant)

    def test_use_item(self):
        """
        Use up a potion during combat.

        """
        item = create.create_object(
            EvAdventureConsumable, key="Healing potion", attributes=[("uses", 2)]
        )

        item.use = Mock()

            "key": "use",
            "item": item,
        }

        self.assertEqual(item.uses, 2)
        action = combat_base.CombatActionUseItem(self.combathandler, self.combatant, action_dict)
        self.assertEqual(item.uses, 1)
        self.assertEqual(item.pk, None)  # deleted, it was used up

    def test_swap_wielded_weapon_or_spell(self):
        """
        First draw a weapon (from empty fists), then swap that out to another weapon, then
        swap to a spell rune.

        """
        sword = create.create_object(EvAdventureWeapon, key="sword")
        zweihander = create.create_object(
            key="zweihander",
            attributes=(("inventory_use_slot", WieldLocation.TWO_HANDS),),
        )
        runestone = create.create_object(EvAdventureRunestone, key="ice rune")

        # check hands are empty
        self.assertEqual(self.combatant.weapon.key, "Bare hands")
        self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], None)
        self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], None)

        # swap to sword

        actiondict = {"key": "wield", "item": sword}

        action = combat_base.CombatActionWield(self.combathandler, self.combatant, actiondict)

        self.assertEqual(self.combatant.weapon, sword)
        self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], sword)

        # swap to zweihander (two-handed sword)
        actiondict["item"] = zweihander

        self.assertEqual(self.combatant.weapon, zweihander)
        self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], zweihander)

        # swap to runestone (also using two hands)
        actiondict["item"] = runestone

        self.assertEqual(self.combatant.weapon, runestone)
        self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], runestone)

        # swap back to normal one-handed sword
        actiondict["item"] = sword

class EvAdventureTurnbasedCombatHandlerTest(_CombatTestBase):
    """
    Test methods on the turn-based combat handler and actions

    """

    # make sure to mock away all time-keeping elements
    @patch(
        (
            "evennia.contrib.tutorials.evadventure."
            "combat_turnbased.EvAdventureTurnbasedCombatHandler.interval"
        ),
        new=-1,
    )
        # add target to combat
        self.combathandler = (
            combat_turnbased.EvAdventureTurnbasedCombatHandler.get_or_create_combathandler(
                self.location, key="combathandler"
            )
        )
        self.combathandler.add_combatant(self.combatant)
        self.combathandler.add_combatant(self.target)

    def _get_action(self, action_dict={"key": "hold"}):
        action_class = self.combathandler.action_classes[action_dict["key"]]
        return action_class(self.combathandler, self.combatant, action_dict)

    def _run_actions(
        self, action_dict, action_dict2={"key": "hold"}, combatant_msg=None, target_msg=None
    ):
        """
        Helper method to run an action and check so combatant saw the expected message.
        """
        self.combathandler.queue_action(self.combatant, action_dict)
        self.combathandler.queue_action(self.target, action_dict2)
        self.combathandler.at_repeat()
        if combatant_msg is not None:
            # this works because we mock combatant.msg in SetUp
            self.combatant.msg.assert_called_with(combatant_msg)
        if target_msg is not None:
            # this works because we mock target.msg in SetUp
            self.combatant.msg.assert_called_with(target_msg)

    def test_combatanthandler_setup(self):
        """Testing all is set up correctly in the combathandler"""

        chandler = self.combathandler
            dict(chandler.combatants),
            {self.combatant: {"key": "hold"}, self.target: {"key": "hold"}},
        )
            dict(chandler.action_classes),
            {
                "hold": combat_turnbased.CombatActionHold,
                "attack": combat_turnbased.CombatActionAttack,
                "stunt": combat_turnbased.CombatActionStunt,
                "use": combat_turnbased.CombatActionUseItem,
                "wield": combat_turnbased.CombatActionWield,
                "flee": combat_turnbased.CombatActionFlee,
            },
        )
        self.assertEqual(chandler.flee_timeout, 1)
        self.assertEqual(dict(chandler.advantage_matrix), {})
        self.assertEqual(dict(chandler.disadvantage_matrix), {})
        self.assertEqual(dict(chandler.fleeing_combatants), {})
        self.assertEqual(dict(chandler.defeated_combatants), {})

    def test_remove_combatant(self):
        """Remove a combatant."""

        self.combathandler.remove_combatant(self.target)
        self.assertEqual(dict(self.combathandler.combatants), {self.combatant: {"key": "hold"}})

    def test_stop_combat(self):
        """Stopping combat, making sure combathandler is deleted."""

        self.combathandler.stop_combat()
        self.assertIsNone(self.combathandler.pk)

    def test_get_sides(self):
        """Getting the sides of combat"""

        combatant2 = create.create_object(
            EvAdventureCharacter, key="testchar2", location=self.location
        )
        target2 = create.create_object(
            key="testmonster2",
        )
        self.combathandler.add_combatant(combatant2)
        self.combathandler.add_combatant(target2)

        # allies to combatant
        allies, enemies = self.combathandler.get_sides(self.combatant)
        self.assertEqual((allies, enemies), ([self.combatant, combatant2], [self.target, target2]))

        # allies to monster
        allies, enemies = self.combathandler.get_sides(self.target)
        self.assertEqual((allies, enemies), ([self.target, target2], [self.combatant, combatant2]))

    def test_queue_and_execute_action(self):
        """Queue actions and execute"""

        hold = {"key": "hold"}

        self.combathandler.queue_action(self.combatant, hold)
            dict(self.combathandler.combatants),
        )

        mock_action = Mock()
        self.combathandler.action_classes["hold"] = Mock(return_value=mock_action)

        self.combathandler.execute_next_action(self.combatant)

        self.combathandler.action_classes["hold"].assert_called_with(
            self.combathandler, self.combatant, hold
        )
        mock_action.execute.assert_called_once()

    def test_execute_full_turn(self):
        """Run a full (passive) turn"""

        self.combathandler.queue_action(self.target, hold)

        self.combathandler.execute_next_action = Mock()

        self.combathandler.execute_next_action.assert_has_calls(
            [call(self.combatant), call(self.target)], any_order=True
        )

    def test_action__action_ticks_turn(self):
        """Test that action execution ticks turns"""

        actiondict = {"key": "hold"}
        self._run_actions(actiondict, actiondict)
        self.assertEqual(self.combathandler.turn, 1)

        self.combatant.msg.assert_not_called()

    def test_attack__success__kill(self, mock_randint):
        """Test that the combathandler is deleted once there are no more enemies"""

        self._run_actions(actiondict)
        self.assertEqual(self.target.hp, -7)
        # after this the combat is over

        }
        self._run_actions(action_dict)
        self.assertEqual(self.combathandler.advantage_matrix[self.combatant], {})
        self.assertEqual(self.combathandler.disadvantage_matrix[self.combatant], {})

        """Test so the advantage matrix is updated correctly"""
        }
            bool(self.combathandler.advantage_matrix[self.combatant][self.target]), True
        )

        """Test so the disadvantage matrix is updated correctly"""
        }
            bool(self.combathandler.disadvantage_matrix[self.target][self.combatant]), True
        )

    def test_flee__success(self):
        """
        Test fleeing twice, leading to leaving combat.

        """

        self.assertEqual(self.combathandler.turn, 0)
        action_dict = {"key": "flee", "repeat": True}

        # first flee records the fleeing state
        self.combathandler.flee_timeout = 2  # to make sure
        self.assertEqual(self.combathandler.fleeing_combatants[self.combatant], 1)

        # action_dict should still be in place due to repeat
        self.assertEqual(self.combathandler.combatants[self.combatant], action_dict)

        self.combatant.msg.assert_called_with(
            text=(
                "You retreat, being exposed to attack while doing so (will escape in 1 turn).",
                {},
            ),
            from_obj=self.combatant,
        )
        # Check that enemies have advantage against you now
        action = combat_turnbased.CombatAction(self.combathandler, self.target, {"key": "hold"})
        self.assertTrue(action.combathandler.has_advantage(self.target, self.combatant))

        # second flee should remove combatant
        # this ends combat, so combathandler should be gone

class TestEvAdventureTwitchCombatHandler(EvenniaCommandTestMixin, _CombatTestBase):

        # in order to use the EvenniaCommandTestMixin we need these variables defined
        self.char1 = self.combatant
        self.account = None

        self.combatant_combathandler = (
            combat_twitch.EvAdventureCombatTwitchHandler.get_or_create_combathandler(
                self.combatant, key="combathandler"
            )
        )
        self.target_combathandler = (
                self.target, key="combathandler"
            )
        )

        sides = self.combatant_combathandler.get_sides(self.combatant)
        self.assertEqual(sides, ([self.combatant], [self.target]))

    def test_give_advantage(self):
        self.combatant_combathandler.give_advantage(self.combatant, self.target)
        self.assertTrue(self.combatant_combathandler.advantage_against[self.target])

    def test_give_disadvantage(self):
        self.combatant_combathandler.give_disadvantage(self.combatant, self.target)
        self.assertTrue(self.combatant_combathandler.disadvantage_against[self.target])

    @patch("evennia.contrib.tutorials.evadventure.combat_twitch.unrepeat", new=Mock())
    @patch("evennia.contrib.tutorials.evadventure.combat_twitch.repeat", new=Mock(return_value=999))
    def test_queue_action(self):
        """Test so the queue action cleans up tickerhandler correctly"""

        self.combatant_combathandler.queue_action(actiondict)

        self.assertIsNone(self.combatant_combathandler.current_ticker_ref)

        actiondict = {"key": "hold", "dt": 5}
        self.assertEqual(self.combatant_combathandler.current_ticker_ref, 999)

    @patch("evennia.contrib.tutorials.evadventure.combat_twitch.repeat", new=Mock())
    def test_execute_next_action(self):
        self.combatant_combathandler.action_dict = {
            "key": "hold",
            "dummy": "foo",
            "repeat": False,
        }  # to separate from fallback

        self.combatant_combathandler.execute_next_action()
        # should now be back to fallback
            self.combatant_combathandler.action_dict,
            self.combatant_combathandler.fallback_action_dict,
        )

    def test_check_stop_combat(self):
        """Test combat-stop functionality"""

        # noone remains (both combatant/target <0 hp
        # get_sides does not include the caller
        self.combatant_combathandler.get_sides = Mock(return_value=([], []))
        self.combatant_combathandler.stop_combat = Mock()

        self.combatant.hp = -1
        self.target.hp = -1

        self.combatant_combathandler.check_stop_combat()
            text=("Noone stands after the dust settles.", {}), from_obj=self.combatant
        )
        self.combatant_combathandler.stop_combat.assert_called()

        # only one side wiped out
        self.combatant.hp = 10
        self.combatant_combathandler.get_sides = Mock(return_value=([self.combatant], []))
            text=("The combat is over.", {}), from_obj=self.combatant
        )

    def test_hold(self):
        self.call(combat_twitch.CmdHold(), "", "You hold back, doing nothing")
        self.assertEqual(self.combatant_combathandler.action_dict, {"key": "hold"})

    def test_attack(self):
        """Test attack action in the twitch combathandler"""
        self.call(combat_twitch.CmdAttack(), self.target.key, "You attack testmonster!")
            {"key": "attack", "target": self.target, "dt": 3, "repeat": True},
        )

    def test_stunt(self):
        boost_result = {
            "defense_type": Ability.STR,
            "dt": 3,
        }
        foil_result = {
            "dt": 3,
        }

            combat_twitch.CmdStunt(),
            f"STR {self.target.key}",
            "You prepare a stunt!",
            cmdstring="boost",
        )
        self.assertEqual(self.combatant_combathandler.action_dict, boost_result)

            f"STR me {self.target.key}",
        )

            cmdstring="foil",
        )
        self.assertEqual(self.combatant_combathandler.action_dict, foil_result)

            f"STR {self.target.key} me",
        )

    def test_useitem(self):
            EvAdventureConsumable, key="potion", attributes=[("uses", 2)], location=self.combatant
        )

        self.call(combat_twitch.CmdUseItem(), "potion", "You prepare to use potion!")
            {"key": "use", "item": item, "target": self.combatant, "dt": 3},
        )

            combat_twitch.CmdUseItem(),
            f"potion on {self.target.key}",
            "You prepare to use potion!",
        )
            {"key": "use", "item": item, "target": self.target, "dt": 3},
        )

    def test_wield(self):
        sword = create.create_object(EvAdventureWeapon, key="sword", location=self.combatant)
        runestone = create.create_object(
            EvAdventureWeapon, key="runestone", location=self.combatant
        )

        self.call(combat_twitch.CmdWield(), "sword", "You reach for sword!")
            self.combatant_combathandler.action_dict, {"key": "wield", "item": sword, "dt": 3}
        )

        self.call(combat_twitch.CmdWield(), "runestone", "You reach for runestone!")
            self.combatant_combathandler.action_dict, {"key": "wield", "item": runestone, "dt": 3}
        )

"""
Test the EvAdventure commands.

"""

from unittest.mock import call, patch

from .. import commands
from ..npcs import EvAdventureMob, EvAdventureShopKeeper
from .mixins import EvAdventureMixin

class TestEvAdventureCommands(EvAdventureMixin, BaseEvenniaCommandTest):
        # needed for the .call mechanism
        self.char1 = self.character

            commands.CmdInventory(),
            "inventory",
            """
You are fighting with your bare fists and have no shield.
You wear no armor and no helmet.
Backpack is empty.
You use 0/11 equipment slots.
""".strip(),
        )

    def test_wield_or_wear(self):
        self.char1.equipment.add(self.helmet)
        self.char1.equipment.add(self.weapon)
        self.shield.location = self.location

        self.call(commands.CmdWieldOrWear(), "shield", "Could not find 'shield'")
        self.call(commands.CmdWieldOrWear(), "helmet", "You put helmet on your head.")
            commands.CmdWieldOrWear(),
            "weapon",
            "You hold weapon in your strongest hand, ready for action.",
        )
        self.call(commands.CmdWieldOrWear(), "helmet", "You are already using helmet.")

        self.call(commands.CmdRemove(), "helmet", "You stash helmet in your backpack.")

    def test_give__coins(self):
        recipient = create_object(EvAdventureCharacter, key="Friend", location=self.location)
        recipient.coins = 0
        self.char1.coins = 100

        self.call(commands.CmdGive(), "40 coins to friend", "You give Friend 40 coins.")
        self.assertEqual(self.char1.coins, 60)
        self.assertEqual(recipient.coins, 40)

        self.call(commands.CmdGive(), "10 to friend", "You give Friend 10 coins.")
        self.assertEqual(self.char1.coins, 50)
        self.assertEqual(recipient.coins, 50)

        self.call(commands.CmdGive(), "60 to friend", "You only have 50 coins to give.")

        recipient.delete()

    @patch("evennia.contrib.tutorials.evadventure.commands.EvMenu")
    def test_give__item(self, mock_EvMenu):

        self.call(commands.CmdGive(), "helmet to friend", "")

        mock_EvMenu.assert_has_calls(
            (
                call(
                    recipient,
                    {"node_receive": Something, "node_end": Something},
                    item=self.helmet,
                    giver=self.char1,
                ),
                call(
                    self.char1,
                    {"node_give": Something, "node_end": Something},
                    receiver=recipient,
                ),
            )
        )

    @patch("evennia.contrib.tutorials.evadventure.npcs.EvMenu")
    def test_talk(self, mock_EvMenu):
        npc = create_object(EvAdventureShopKeeper, key="shopkeep", location=self.location)

        npc.menudata = {"foo": None, "bar": None}

        self.call(commands.CmdTalk(), "shopkeep", "")

        mock_EvMenu.assert_called_with(
            self.char1,
            {"foo": None, "bar": None},
            startnode="node_start",
            session=None,
            npc=npc,
        )

"""
Test Dungeon orchestrator / procedurally generated dungeon rooms.

"""

from unittest.mock import MagicMock

from .. import dungeon

class TestDungeon(EvAdventureMixin, BaseEvenniaTest):
    """
    Test with a starting room and a character moving through the dungeon,
    generating more and more rooms as they go.

    """

        """
        Create a start room with exits leading away from it

        """
        droomclass = dungeon.EvAdventureDungeonStartRoom
        droomclass.recycle_time = 0  # disable the tick
        droomclass.branch_check_time = 0

        self.start_room = create_object(droomclass, key="bottom of well")

            self.start_room.scripts.get("evadventure_dungeon_startroom_resetter")[0].interval, -1
        )
        self.start_north = create_object(
            dungeon.EvAdventureDungeonStartRoomExit,
            key="north",
            location=self.start_room,
            destination=self.start_room,
        )
        self.start_north
        self.start_south = create_object(
            key="south",
        )
        self.character.location = self.start_room

    def _move_character(self, direction):
        old_location = self.character.location
        for exi in old_location.exits:
            if exi.key == direction:
                # by setting target to old-location we trigger the
                # special behavior of this Exit type
                exi.at_traverse(self.character, exi.destination)
                break
        return self.character.location

    def test_start_room(self):
        """
        Test move through one of the start room exits.

        """
        # begin in start room
        self.assertEqual(self.character.location, self.start_room)

        # first go north, this should generate a new room
        new_room_north = self._move_character("north")
        self.assertNotEqual(self.start_room, new_room_north)
        self.assertTrue(inherits_from(new_room_north, dungeon.EvAdventureDungeonRoom))

        # check if Orchestrator was created
        orchestrator = new_room_north.db.dungeon_orchestrator
        self.assertTrue(bool(orchestrator))
        self.assertTrue(orchestrator.key.startswith("dungeon_orchestrator_north_"))

    def test_different_start_directions(self):

        # back to start room
        start_room = self._move_character("south")
        self.assertEqual(self.start_room, start_room)

        # next go south, this should generate a new room
        new_room_south = self._move_character("south")
        self.assertNotEqual(self.start_room, new_room_south)
        self.assertNotEqual(new_room_north, new_room_south)

        # back to start room again
        start_room = self._move_character("north")

"""
Test the EvAdventure equipment handler.

"""

from ..equipment import EquipmentError

class TestEquipment(EvAdventureMixin, BaseEvenniaTest):
    def test_count_slots(self):
        self.assertEqual(self.character.equipment.count_slots(), 0)

    def test_max_slots(self):
        self.assertEqual(self.character.equipment.max_slots, 11)
        setattr(self.character, Ability.CON.value, 3)
        self.assertEqual(self.character.equipment.max_slots, 13)

    def test_add__remove(self):
        self.character.equipment.add(self.helmet)
        self.assertEqual(self.character.equipment.slots[WieldLocation.BACKPACK], [self.helmet])
        self.character.equipment.remove(self.helmet)
        self.assertEqual(self.character.equipment.slots[WieldLocation.BACKPACK], [])

    def test_move__get_current_slot(self):
            self.character.equipment.get_current_slot(self.helmet), WieldLocation.BACKPACK
        )
        self.character.equipment.move(self.helmet)
        self.assertEqual(self.character.equipment.get_current_slot(self.helmet), WieldLocation.HEAD)

    def test_get_wearable_or_wieldable_objects_from_backpack(self):
        self.character.equipment.add(self.weapon)

            self.character.equipment.get_wieldable_objects_from_backpack(), [self.weapon]
        )
            self.character.equipment.get_wearable_objects_from_backpack(), [self.helmet]
        )

            self.character.equipment.all(),
            [
                (None, WieldLocation.WEAPON_HAND),
                (None, WieldLocation.SHIELD_HAND),
                (None, WieldLocation.TWO_HANDS),
                (None, WieldLocation.BODY),
                (None, WieldLocation.HEAD),
                (self.helmet, WieldLocation.BACKPACK),
                (self.weapon, WieldLocation.BACKPACK),
            ],
        )

    def _get_empty_slots(self):
        return {
            WieldLocation.BACKPACK: [],
            WieldLocation.WEAPON_HAND: None,
            WieldLocation.SHIELD_HAND: None,
            WieldLocation.TWO_HANDS: None,
            WieldLocation.BODY: None,
            WieldLocation.HEAD: None,
        }

    def test_equipmenthandler_max_slots(self):

        [
            # size, pass_validation?
            (1, True),
            (2, True),
            (11, True),
            (12, False),
            (20, False),
            (25, False),
        ]
    )
    def test_validate_slot_usage(self, size, is_ok):
        obj = MagicMock()
        obj.size = size

        with patch("evennia.contrib.tutorials.evadventure.equipment.inherits_from") as mock_inherit:
            mock_inherit.return_value = True
            if is_ok:
                self.assertTrue(self.character.equipment.validate_slot_usage(obj))
            else:
                with self.assertRaises(EquipmentError):
                    self.character.equipment.validate_slot_usage(obj)

        [
            # item, where
            ("helmet", WieldLocation.HEAD),
            ("shield", WieldLocation.SHIELD_HAND),
            ("armor", WieldLocation.BODY),
            ("weapon", WieldLocation.WEAPON_HAND),
            ("big_weapon", WieldLocation.TWO_HANDS),
            ("item", WieldLocation.BACKPACK),
        ]
    )
    def test_move(self, itemname, where):
        self.assertEqual(self.character.equipment.slots, self._get_empty_slots())

        obj = getattr(self, itemname)
        self.character.equipment.move(obj)
        # check that item ended up in the right place
        if where is WieldLocation.BACKPACK:
            self.assertTrue(obj in self.character.equipment.slots[where])
        else:
            self.assertEqual(self.character.equipment.slots[where], obj)

        self.assertEqual(self.character.equipment.slots[WieldLocation.WEAPON_HAND], None)
        self.assertTrue(self.weapon in self.character.equipment.slots[WieldLocation.BACKPACK])

    def test_two_handed_exclusive(self):
        """Two-handed weapons can't be used together with weapon+shield"""
        self.character.equipment.move(self.big_weapon)
        self.assertEqual(self.character.equipment.slots[WieldLocation.TWO_HANDS], self.big_weapon)
        # equipping sword or shield removes two-hander
        self.character.equipment.move(self.shield)
        self.assertEqual(self.character.equipment.slots[WieldLocation.SHIELD_HAND], self.shield)
        self.assertEqual(self.character.equipment.slots[WieldLocation.TWO_HANDS], None)
        self.character.equipment.move(self.weapon)
        self.assertEqual(self.character.equipment.slots[WieldLocation.WEAPON_HAND], self.weapon)

        # the two-hander removes the two weapons
        self.assertEqual(self.character.equipment.slots[WieldLocation.SHIELD_HAND], None)

    def test_remove__with_obj(self):
        self.character.equipment.move(self.item)

            self.character.equipment.slots[WieldLocation.BACKPACK], [self.item, self.weapon]
        )

        self.assertEqual(self.character.equipment.remove(self.shield), [self.shield])
        self.assertEqual(self.character.equipment.remove(self.item), [self.item])

        self.assertEqual(self.character.equipment.slots[WieldLocation.BACKPACK], [self.weapon])

    def test_remove__with_slot(self):

            self.character.equipment.slots[WieldLocation.BACKPACK], [self.item, self.helmet]
        )

        self.assertEqual(self.character.equipment.remove(WieldLocation.SHIELD_HAND), [self.shield])
            self.character.equipment.remove(WieldLocation.BACKPACK), [self.item, self.helmet]
        )

    def test_properties(self):
        self.character.equipment.move(self.armor)
        self.assertEqual(self.character.equipment.armor, 1)
        self.assertEqual(self.character.equipment.armor, 2)
        self.assertEqual(self.character.equipment.armor, 3)

        self.assertEqual(self.character.equipment.weapon, self.weapon)
        self.assertEqual(self.character.equipment.weapon, self.big_weapon)

"""
Test NPC classes.

"""

from .. import npcs

class TestNPCBase(EvenniaTest):
    def test_npc_base(self):
        npc = create_object(
            npcs.EvAdventureNPC,
            key="TestNPC",
            attributes=[("hit_dice", 4), ("armor", 1), ("morale", 9)],
        )

        self.assertEqual(npc.hp_multiplier, 4)
        self.assertEqual(npc.hp_max, 16)
        self.assertEqual(npc.strength, 4)
        self.assertEqual(npc.charisma, 4)

"""
Testing Quest functionality.

"""

from .. import quests
from ..objects import EvAdventureObject

class _TestQuest(quests.EvAdventureQuest):
    """
    Test quest.

    """

    key = "testquest"
    desc = "A test quest!"

    start_step = "A"
    end_text = "This task is completed."

    help_A = "You need to do A first."
    help_B = "Next, do B."

    def step_A(self, *args, **kwargs):
        """
        Quest-step A is completed when quester carries an item with tag "QuestA" and category
        "quests".
        """
        # note - this could be done with a direct db query instead to avoid a loop, for a
        # unit test it's fine though
        if any(obj for obj in self.quester.contents if obj.tags.has("QuestA", category="quests")):
            self.quester.msg("Completed step A of quest!")
            self.current_step = "B"
            self.progress()

    def step_B(self, *args, **kwargs):
        """
        Quest-step B is completed when the progress-check is called with a special kwarg
        "complete_quest_B"

        """
        if kwargs.get("complete_quest_B", False):
            self.quester.msg("Completed step B of quest!")
            self.quester.db.test_quest_counter = 0
            self.current_step = "C"

    def help_C(self):
        """Testing the method-version of getting a help entry"""
        return f"Only C left now, {self.quester.key}!"

    def step_C(self, *args, **kwargs):
        """
        Step C (final) step of quest completes when a counter on quester is big enough.

        """
        if self.quester.db.test_quest_counter and self.quester.db.test_quest_counter > 5:
            self.quester.msg("Quest complete! Get XP rewards!")
            self.quester.db.xp += 10
            self.complete()

        """
        Cleanup data related to quest.

        """
        del self.quester.db.test_quest_counter

class EvAdventureQuestTest(EvAdventureMixin, BaseEvenniaTest):
    """
    Test questing.

    """

        self.character.quests.add(_TestQuest)
        self.character.msg = MagicMock()

    def _get_quest(self):
        return self.character.quests.get(_TestQuest.key)

    def _fulfillA(self):
        """Fulfill quest step A"""
        EvAdventureObject.create(
            key="quest obj", location=self.character, tags=(("QuestA", "quests"),)
        )

    def _fulfillC(self):
        """Fullfill quest step C"""
        self.character.db.test_quest_counter = 6

        """Get help"""
        # get help for all quests
        help_txt = self.character.quests.get_help()
        self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - You need to do A first."])

        # get help for one specific quest
        help_txt = self.character.quests.get_help(_TestQuest.key)

        # help for finished quest
        self._get_quest().is_completed = True
        self.assertEqual(help_txt, ["|ctestquest|n\n A test quest!\n\n - This quest is completed!"])

    def test_progress__fail(self):
        """
        Check progress without having any.
        """
        # progress all quests
        self.character.quests.progress()
        # progress one quest
        self.character.quests.progress(_TestQuest.key)

        # still on step A
        self.assertEqual(self._get_quest().current_step, "A")

    def test_progress(self):
        """
        Fulfill the quest steps in sequess

        """
        # A requires a certain object in inventory
        self._fulfillA()
        self.assertEqual(self._get_quest().current_step, "B")

        # B requires progress be called with specific kwarg
        # should not step (no kwarg)

        # should step (kwarg sent)
        self.character.quests.progress(complete_quest_B=True)
        self.assertEqual(self._get_quest().current_step, "C")

        # C requires a counter Attribute on char be high enough
        self._fulfillC()
        self.assertEqual(self._get_quest().current_step, "C")  # still on last step
        self.assertEqual(self._get_quest().is_completed, True)

"""
Test of EvAdventure Rooms

"""

from evennia import DefaultExit, create_object
from evennia.utils.test_resources import EvenniaTestCase

class EvAdventureRoomTest(EvenniaTestCase):
        self.char = create_object(EvAdventureCharacter, key="TestChar")

    def test_map(self):
        center_room = create_object(EvAdventureRoom, key="room_center")
        n_room = create_object(EvAdventureRoom, key="room_n")
        create_object(DefaultExit, key="north", location=center_room, destination=n_room)
        ne_room = create_object(EvAdventureRoom, key="room_ne")
        create_object(DefaultExit, key="northeast", location=center_room, destination=ne_room)
        e_room = create_object(EvAdventureRoom, key="room_e")
        create_object(DefaultExit, key="east", location=center_room, destination=e_room)
        se_room = create_object(EvAdventureRoom, key="room_se")
        create_object(DefaultExit, key="southeast", location=center_room, destination=se_room)
        s_room = create_object(EvAdventureRoom, key="room_")
        create_object(DefaultExit, key="south", location=center_room, destination=s_room)
        sw_room = create_object(EvAdventureRoom, key="room_sw")
        create_object(DefaultExit, key="southwest", location=center_room, destination=sw_room)
        w_room = create_object(EvAdventureRoom, key="room_w")
        create_object(DefaultExit, key="west", location=center_room, destination=w_room)
        nw_room = create_object(EvAdventureRoom, key="room_nw")
        create_object(DefaultExit, key="northwest", location=center_room, destination=nw_room)

        desc = center_room.return_appearance(self.char)

        expected = r"""
  o o o
   \|/
  o-@-o
   /|\
  o o o
room_center
You see nothing special.
Exits: north, northeast, east, southeast, south, southwest, west, and northwest"""

        result = "\n".join(part.rstrip() for part in strip_ansi(desc).split("\n"))
        expected = "\n".join(part.rstrip() for part in expected.split("\n"))
        # print(result)
        # print(expected)

        self.assertEqual(result, expected)

"""
Test the rules and chargen.

"""

from unittest.mock import MagicMock, call, patch

from .. import characters, enums, equipment, random_tables, rules

class EvAdventureRollEngineTest(BaseEvenniaTest):
    """
    Test the roll engine in the rules module. This is the core of any RPG.

    """

        self.roll_engine = rules.EvAdventureRollEngine()

    def test_roll(self, mock_randint):
        mock_randint.return_value = 8
        self.assertEqual(self.roll_engine.roll("1d6"), 8)
        mock_randint.assert_called_with(1, 6)

        self.assertEqual(self.roll_engine.roll("2d8"), 2 * 8)
        mock_randint.assert_called_with(1, 8)

        self.assertEqual(self.roll_engine.roll("4d12"), 4 * 8)
        mock_randint.assert_called_with(1, 12)

        self.assertEqual(self.roll_engine.roll("8d100"), 8 * 8)
        mock_randint.assert_called_with(1, 100)

    def test_roll_limits(self):
            self.roll_engine.roll("100d6", max_number=10)  # too many die
            self.roll_engine.roll("100")  # no d
            self.roll_engine.roll("dummy")  # non-numerical
            self.roll_engine.roll("Ad4")  # non-numerical
            self.roll_engine.roll("1d10000")  # limit is d1000

    def test_roll_with_advantage_disadvantage(self, mock_randint):
        mock_randint.return_value = 9

        # no advantage/disadvantage
        self.assertEqual(self.roll_engine.roll_with_advantage_or_disadvantage(), 9)
        mock_randint.assert_called_once()
        mock_randint.reset_mock()

        # cancel each other out
            self.roll_engine.roll_with_advantage_or_disadvantage(disadvantage=True, advantage=True),
            9,
        )

        # run with advantage/disadvantage
        self.assertEqual(self.roll_engine.roll_with_advantage_or_disadvantage(advantage=True), 9)
        mock_randint.assert_has_calls([call(1, 20), call(1, 20)])

        self.assertEqual(self.roll_engine.roll_with_advantage_or_disadvantage(disadvantage=True), 9)

    def test_saving_throw(self, mock_randint):

        character = MagicMock()
        character.strength = 2
        character.dexterity = 1

            self.roll_engine.saving_throw(character, bonus_type=enums.Ability.STR),
            (False, None, Something),
        )
            self.roll_engine.saving_throw(character, bonus_type=enums.Ability.DEX, modifier=1),
        )
            self.roll_engine.saving_throw(
                character, advantage=True, bonus_type=enums.Ability.DEX, modifier=6
            ),
        )
                character, disadvantage=True, bonus_type=enums.Ability.DEX, modifier=7
            ),
            (True, None, Something),
        )

        mock_randint.return_value = 1
                character, disadvantage=True, bonus_type=enums.Ability.STR, modifier=2
            ),
            (False, enums.Ability.CRITICAL_FAILURE, Something),
        )

        mock_randint.return_value = 20
            ),
            (True, enums.Ability.CRITICAL_SUCCESS, Something),
        )

    def test_opposed_saving_throw(self, mock_randint):

        attacker, defender = MagicMock(), MagicMock()
        attacker.strength = 1
        defender.armor = 2

            self.roll_engine.opposed_saving_throw(
                attacker, defender, attack_type=enums.Ability.STR, defense_type=enums.Ability.ARMOR
            ),
        )
                attacker,
                defender,
                attack_type=enums.Ability.STR,
                defense_type=enums.Ability.ARMOR,
                modifier=2,
            ),
        )

    def test_roll_random_table(self, mock_randint):

            self.roll_engine.roll_random_table("1d20", random_tables.chargen_tables["physique"]),
            "scrawny",
        )
            self.roll_engine.roll_random_table("1d20", random_tables.chargen_tables["vice"]),
            "irascible",
        )
            self.roll_engine.roll_random_table("1d20", random_tables.chargen_tables["alignment"]),
            "neutrality",
        )
            self.roll_engine.roll_random_table(
                "1d20", random_tables.chargen_tables["helmets and shields"]
            ),
            "no helmet or shield",
        )
        # testing faulty rolls outside of the table ranges
        mock_randint.return_value = 25
            ),
            "helmet and shield",
        )
        mock_randint.return_value = -10
            ),
        )

    def test_morale_check(self, mock_randint):
        defender = MagicMock()
        defender.morale = 12

        mock_randint.return_value = 7  # 2d6 is rolled, so this will become 14
        self.assertEqual(self.roll_engine.morale_check(defender), False)

        mock_randint.return_value = 3  # 2d6 is rolled, so this will become 6
        self.assertEqual(self.roll_engine.morale_check(defender), True)

    def test_heal_from_rest(self, mock_randint):
        character.heal = MagicMock()
        character.hp_max = 8
        character.hp = 1
        character.constitution = 1

        mock_randint.return_value = 5
        self.roll_engine.heal_from_rest(character)
        mock_randint.assert_called_with(1, 8)  # 1d8
        character.heal.assert_called_with(6)  # roll + constitution bonus

    def test_roll_death(self, mock_randint):
        character.strength = 13
        character.hp = 0

        # death
        self.roll_engine.roll_death(character)
        character.at_death.assert_called()
        # strength loss
        mock_randint.return_value = 3
        self.assertEqual(character.strength, 10)

"""
Tests of the utils module.

"""

from .. import utils

    def test_get_obj_stats(self):
        obj = create.create_object(
            EvAdventureObject, key="testobj", attributes=(("desc", "A test object"),)
        )
        result = utils.get_obj_stats(obj)

            result,
            """
|ctestobj|n
Value: ~|y0|n coins

A test object

Slots: |w1|n, Used from: |wbackpack|n
Quality: |wN/A|n, Uses: |wuses|n
Attacks using |wNo attack|n against |wNo defense|n
Damage roll: |wNone|n
        )

"""
Various utilities.

"""

_OBJ_STATS = """
|c{key}|n
Value: ~|y{value}|n coins{carried}

{desc}

Slots: |w{size}|n, Used from: |w{use_slot_name}|n
Quality: |w{quality}|n, Uses: |wuses|n
Attacks using |w{attack_type_name}|n against |w{defense_type_name}|n
Damage roll: |w{damage_roll}|n""".strip()

def get_obj_stats(obj, owner=None):
    """
    Get a string of stats about the object.

    Args:
        obj (EvAdventureObject): The object to get stats for.
        owner (EvAdventureCharacter, optional): If given, it allows us to
            also get information about if the item is currently worn/wielded.

    Returns:
        str: A stat string to show about the object.

    """
    carried = ""
    if owner:
        objmap = dict(owner.equipment.all())
        carried = objmap.get(obj)
        carried = f", Worn: [{carried.value}]" if carried else ""

    attack_type = getattr(obj, "attack_type", None)
    defense_type = getattr(obj, "attack_type", None)

    return _OBJ_STATS.format(
        key=obj.key,
        value=obj.value,
        carried=carried,
        desc=obj.db.desc,
        size=obj.size,
        use_slot_name=obj.inventory_use_slot.value,
        quality=getattr(obj, "quality", "N/A"),
        uses=getattr(obj, "uses", "N/A"),
        attack_type_name=attack_type.value if attack_type else "No attack",
        defense_type_name=defense_type.value if defense_type else "No defense",
        damage_roll=getattr(obj, "damage_roll", "None"),
    )

"""
TutorialMirror

A simple mirror object to experiment with.

"""

from evennia import DefaultObject, logger
from evennia.utils import is_iter, make_iter

class TutorialMirror(DefaultObject):
    """
    A simple mirror object that
    - echoes back the description of the object looking at it
    - echoes back whatever is being sent to its .msg - to the
      sender, if given, otherwise to the location of the mirror.

    """

        """
        This formats the description of this object. Called by the 'look' command.

        Args:
        """

        if isinstance(looker, self.__class__):
            # avoid infinite recursion by having two mirrors look at each other
            return "The image of yourself stretches into infinity."
        return f"{self.key} shows your reflection:\n{looker.db.desc}"

    def msg(self, text=None, from_obj=None, **kwargs):
        """
        Simply override .msg to echo back to the messenger or to the current
        location.

        Args:
            from_obj (obj or iterable)
                given, at_msg_send will be called. This value will be
                passed on to the protocol. If iterable, will execute hook
                on all entities in it.
        """
        if not text:
            text = "<silence>"
        text = text[0] if is_iter(text) else text
                obj.msg(f'{self.key} echoes back to you:\n"{text}".')
        elif self.location:
            self.location.msg_contents(f'{self.key} echoes back:\n"{text}".', exclude=[self])
        else:
            # no from_obj and no location, just log
            logger.log_msg(f"{self.key}.msg was called without from_obj and .location is None.")

"""
Red Button

Griatch - 2011

This is a more advanced example object. It combines functions from
script.examples as well as commands.examples to make an interactive
button typeclass.

Create this button with

    create/drop tutorials.red_button.RedButton

Note that you must drop the button before you can see its messages!

## Technical

The button's functionality is controlled by CmdSets that gets added and removed
depending on the 'state' the button is in.

- Lid-closed state: In this state the button is covered by a glass cover and trying
  to 'push' it will fail. You can 'nudge', 'smash' or 'open' the lid.
- Lid-open state: In this state the lid is open but will close again after a certain
  time. Using 'push' now will press the button and trigger the Blind-state.
- Blind-state: In this mode you are blinded by a bright flash. This will affect your
  normal commands like 'look' and help until the blindness wears off after a certain
  time.

Timers are handled by persistent delays on the button. These are examples of
`evennia.utils.utils.delay` calls that wait a certain time before calling a method -
such as when closing the lid and un-blinding a character.

"""

from evennia import CmdSet, Command, DefaultObject
from evennia.utils.utils import delay, interactive, repeat

# Commands on the button (not all awailable at the same time)

# Commands for the state when the lid covering the button is closed.

class CmdPushLidClosed(Command):
    """
    Push the red button (lid closed)

    Usage:
      push button

    """

    key = "push button"
    aliases = ["push", "press button", "press"]

        """
        This is the version of push used when the lid is closed.

        An alternative would be to make a 'push' command in a default cmdset
        that is always available on the button and then use if-statements to
        check if the lid is open or closed.

        """
        self.caller.msg("You cannot push the button = there is a glass lid covering it.")

class CmdNudge(Command):
    """
    Try to nudge the button's lid.

    Usage:
      nudge lid

    This command will have you try to push the lid of the button away.

    """

    key = "nudge lid"  # two-word command name!
    aliases = ["nudge"]

        """
        Nudge the lid. Random chance of success to open it.

        """
        if rand < 0.5:
            self.caller.msg("You nudge at the lid. It seems stuck.")
            self.caller.msg("You move the lid back and forth. It won't budge.")
        else:
            self.caller.msg("You manage to get a nail under the lid.")
            # self.obj is the button object
            self.obj.to_open_state()

class CmdSmashGlass(Command):
    """
    Smash the protective glass.

    Usage:
      smash glass

    Try to smash the glass of the button.

    """

    key = "smash glass"
    aliases = ["smash lid", "break lid", "smash"]

        """
        The lid won't open, but there is a small chance of causing the lamp to
        break.

        """
            f"{self.caller.name} tries to smash the glass of the button.", exclude=self.caller
        )

        if rand < 0.2:
            string = (
                "You smash your hand against the glass"
                " with all your might. The lid won't budge"
                " but you cause quite the tremor through the button's mount."
                "\nIt looks like the button's lamp stopped working for the time being, "
                "but the lid is still as closed as ever."
            )
            # self.obj is the button itself
            self.obj.break_lamp()
            string = "You hit the lid hard. It doesn't move an inch."
        else:
            string = (
                "You place a well-aimed fist against the glass of the lid."
                " Unfortunately all you get is a pain in your hand. Maybe"
                " you should just try to just ... open the lid instead?"
            )

class CmdOpenLid(Command):
    """
    open lid

    Usage:
      open lid

    """

    key = "open lid"
    aliases = ["open button"]

        "simply call the right function."

        if self.obj.db.lid_locked:
            self.caller.msg("This lid seems locked in place for the moment.")
            return

        string = "\nA ticking sound is heard, like a winding mechanism. Seems "
        string += "the lid will soon close again."
            f"{self.caller.name} opens the lid of the button.", exclude=self.caller
        )
        self.obj.to_open_state()

class LidClosedCmdSet(CmdSet):
    """
    A simple cmdset tied to the redbutton object.

    It contains the commands that launches the other
    command sets, making the red button a self-contained
    item (i.e. you don't have to manually add any
    scripts etc to it when creating it).

    Note that this is given with a `key_mergetype` set. This
    is set up so that the cmdset with merge with Union merge type
    *except* if the other cmdset to merge with is LidOpenCmdSet,
    in which case it will Replace that. So these two cmdsets will
    be mutually exclusive.

    """

    key = "LidClosedCmdSet"

        "Populates the cmdset when it is instantiated."
        self.add(CmdPushLidClosed())
        self.add(CmdNudge())
        self.add(CmdSmashGlass())
        self.add(CmdOpenLid())

# Commands for the state when the button's protective cover is open - now the
# push command will work. You can also close the lid again.

class CmdPushLidOpen(Command):
    """
    Push the red button

    Usage:

    """

        """
        This version of push will immediately trigger the next button state.

        The use of the @interactive decorator allows for using `yield` to add
        simple pauses in how quickly a message is returned to the user.  This
        kind of pause will not survive a server reload.

        """
        # pause a little between each message.
        self.caller.msg("You reach out to press the big red button ...")
        yield (2)  # pause 2s before next message
        self.caller.msg("\n\n|wBOOOOM! A bright light blinds you!|n")
        yield (1)  # pause 1s before next message
        self.caller.msg("\n\n|xThe world goes dark ...|n")

        name = self.caller.name
            f"{name} presses the button. BOOM! {name} is blinded by a flash!", exclude=self.caller
        )
        self.obj.blind_target(self.caller)

class CmdCloseLid(Command):
    """
    Close the lid

    Usage:
      close lid

    Closes the lid of the red button.
    """

    key = "close lid"

        "Close the lid"

        self.obj.to_closed_state()

        # this will clean out scripts dependent on lid being open.
        self.caller.msg("You close the button's lid. It clicks back into place.")
            f"{self.caller.name} closes the button's lid.", exclude=self.caller
        )

class LidOpenCmdSet(CmdSet):
    """
    This is the opposite of the Closed cmdset.

    *except* if the other cmdset to merge with is LidClosedCmdSet,

    """

    key = "LidOpenCmdSet"

        """Setup the cmdset"""
        self.add(CmdPushLidOpen())
        self.add(CmdCloseLid())

# Commands for when the button has been pushed and the player is blinded. This
# replaces commands on the player making them 'blind' for a while.

class CmdBlindLook(Command):
    """
    Looking around in darkness

    Usage:
      look <obj>

    ... not that there's much to see in the dark.

    """

    aliases = ["l", "get", "examine", "ex", "feel", "listen"]

        "This replaces all the senses when blinded."

        # we decide what to reply based on which command was
        # actually tried

        if self.cmdstring == "get":
            string = "You fumble around blindly without finding anything."
        elif self.cmdstring == "examine":
            string = "You try to examine your surroundings, but can't see a thing."
        elif self.cmdstring == "listen":
            string = "You are deafened by the boom."
        elif self.cmdstring == "feel":
            string = "You fumble around, hands outstretched. You bump your knee."
        else:
            # trying to look
            string = (
                "You are temporarily blinded by the flash. "
                "Until it wears off, all you can do is feel around blindly."
            )
            f"{self.caller.name} stumbles around, blinded.", exclude=self.caller
        )

class CmdBlindHelp(Command):
    """
    Help function while in the blinded state

    Usage:
      help

    """

    aliases = "h"

        """
        Just give a message while blinded. We could have added this to the
        CmdBlindLook command too if we wanted to keep things more compact.

        """
        self.caller.msg("You are beyond help ... until you can see again.")

class BlindCmdSet(CmdSet):
    """
    This is the cmdset added to the *account* when
    the button is pushed.

    Since this has mergetype Replace it will completely remove the commands of
    all other cmdsets while active. To allow some limited interaction
    (pose/say) we import those default commands and add them too.

    We also disable all exit-commands generated by exits and
    object-interactions while blinded by setting `no_exits` and `no_objs` flags
    on the cmdset. This is to avoid the player walking off or interfering with
    other objects while blinded. Account-level commands however (channel messaging
    etc) will not be affected by the blinding.

    """

    key = "BlindCmdSet"
    # we want it to completely replace all normal commands
    # until the timed script removes it again.
    # we want to stop the player from walking around
    # in this blinded state, so we hide all exits too.
    # (channel commands will still work).
    no_exits = True  # keep player in the same room
    no_objs = True  # don't allow object commands

        "Setup the blind cmdset"
        from evennia.commands.default.general import CmdPose, CmdSay

        self.add(CmdBlindLook())
        self.add(CmdBlindHelp())

#
# Definition of the object itself
#

class RedButton(DefaultObject):
    """
    This class describes an evil red button.  It will blink invitingly and
    temporarily blind whomever presses it.

    The button can take a few optional attributes controlling how things will
    be displayed in its various states. This is a useful way to give builders
    the option to customize a complex object from in-game. Actual return messages
    to event-actions are (in this example) left with each command, but one could
    also imagine having those handled via Attributes as well, if one wanted a
    completely in-game customizable button without needing to tweak command
    classes.

    Attributes:
    - `desc_closed_lid`:  This is the description to show of the button
      when the lid is closed.
    - `desc_open_lid`": Shown when the lid is open
    - `auto_close_msg`: Message to show when lid auto-closes
    - `desc_add_lamp_broken`: Extra desc-line added after normal desc when lamp
      is broken.
    - blink_msg: A list of strings to randomly choose from when the lamp
      blinks.

    Notes:
    The button starts with lid closed. To set the initial description,
    you can either set desc after creating it or pass a `desc` attribute
    when creating it, such as
    `button = create_object(RedButton, ..., attributes=[('desc', 'my desc')])`.

    """

    # these are the pre-set descriptions. Setting attributes will override
    # these on the fly.

    desc_closed_lid = (
        "This is a large red button, inviting yet evil-looking. A closed glass lid protects it."
    )
    desc_open_lid = (
        "This is a large red button, inviting yet evil-looking. "
        "Its glass cover is open and the button exposed."
    )
    auto_close_msg = "The button's glass lid silently slides back in place."
    lamp_breaks_msg = "The lamp flickers, the button going dark."
    desc_add_lamp_broken = "\nThe big red button has stopped blinking for the time being."
    # note that this is a list. A random message will display each time
    blink_msgs = [
        "The red button flashes briefly.",
        "The red button blinks invitingly.",
        "The red button flashes. You know you wanna push it!",
    ]

        """
        This function is called (once) when object is created.

        """
        self.db.lamp_works = True

        # start closed
        self.to_closed_state()

        # start blinking every 35s.
        repeat(35, self._do_blink, persistent=True)

    def _do_blink(self):
        """
        Have the button blink invitingly unless it's broken.

        """
        if self.location and self.db.lamp_works:
            possible_messages = self.db.blink_msgs or self.blink_msgs
            self.location.msg_contents(random.choice(possible_messages))

    def _set_desc(self, attrname=None):
        """
        Set a description, based on the attrname given, taking the lamp-status
        into account.

        Args:
            attrname (str, optional): This will first check for an Attribute with this name,
                secondly for a property on the class. So if `attrname="auto_close_msg"`,
                we will first look for an attribute `.db.auto_close_msg` and if that's
                not found we'll use `.auto_close_msg` instead. If unset (`None`), the
                currently set desc will not be changed (only lamp will be checked).

        Notes:
            If `self.db.lamp_works` is `False`, we'll append
            `desc_add_lamp_broken` text.

        """
        if attrname:
            # change desc
            desc = self.attributes.get(attrname) or getattr(self, attrname)
        else:
            # use existing desc
            desc = self.db.desc

        if not self.db.lamp_works:
            # lamp not working. Add extra to button's desc
            desc += self.db.desc_add_lamp_broken or self.desc_add_lamp_broken

        self.db.desc = desc

    # state-changing methods and actions

    def to_closed_state(self, msg=None):
        """
        Switches the button to having its lid closed.

        Args:
            msg (str, optional): If given, display a message to the room
            when lid closes.

        This will first try to get the Attribute (self.db.desc_closed_lid) in
        case it was set by a builder and if that was None, it will fall back to
        self.desc_closed_lid, the default description (note that lack of .db).
        """
        self._set_desc("desc_closed_lid")
        # remove lidopen-state, if it exists
        self.cmdset.remove(LidOpenCmdSet)
        # add lid-closed cmdset
        self.cmdset.add(LidClosedCmdSet, persistent=True)

        if msg and self.location:
            self.location.msg_contents(msg)

    def to_open_state(self):
        """
        Switches the button to having its lid open. This also starts a timer
        that will eventually close it again.

        """
        self._set_desc("desc_open_lid")
        self.cmdset.remove(LidClosedCmdSet)
        # add lid-open cmdset
        self.cmdset.add(LidOpenCmdSet, persistent=True)

        # wait 20s then call self.to_closed_state with a message as argument
        delay(
            35, self.to_closed_state, self.db.auto_close_msg or self.auto_close_msg, persistent=True
        )

    def _unblind_target(self, caller):
        """
        This is called to un-blind after a certain time.

        """
        caller.cmdset.remove(BlindCmdSet)
        caller.msg("You blink feverishly as your eyesight slowly returns.")
            f"{caller.name} seems to be recovering their eyesight, blinking feverishly.",
            exclude=caller,
        )

    def blind_target(self, caller):
        """
        Someone was foolish enough to press the button! Blind them
        temporarily.

        Args:
            caller (Object): The one to be blinded.

        """

        # we don't need to remove other cmdsets, this will replace all,
        # then restore whatever was there when it goes away.
        # we don't make this persistent, to make sure any problem is just a reload away
        caller.cmdset.add(BlindCmdSet)

        # wait 20s then call self._unblind to remove blindness effect. The
        # persistent=True means the delay should survive a server reload.
        delay(20, self._unblind_target, caller, persistent=True)

    def _unbreak_lamp(self):
        """
        This is called to un-break the lamp after a certain time.

        """
        # we do this quietly, the user will just notice it starting blinking again
        self._set_desc()

    def break_lamp(self):
        """
        Breaks the lamp in the button, stopping it from blinking for a while

        """
        self.db.lamp_works = False
        # this will update the desc with the info about the broken lamp
        self.location.msg_contents(self.db.lamp_breaks_msg or self.lamp_breaks_msg)

        # wait 21s before unbreaking the lamp again
        delay(21, self._unbreak_lamp)

"""
Evennia Talkative NPC

Contribution - Griatch 2011, grungies1138, 2016

This is a static NPC object capable of holding a simple menu-driven
conversation. It's just meant as an example.

Create the NPC by creating an object of typeclass `contrib.tutorials.talking_npc.TalkingNPC`,
For example:

    create/drop John : contrib.tutorials.talking_npc.TalkingNPC

Use `talk` in the same room as the NPC to start a conversation.

If there are many talkative npcs in the same room you will get to
choose which one's talk command to call (Evennia handles this
automatically). This use of EvMenu is very simplistic; See EvMenu for
a lot more complex possibilities.

"""

from evennia import CmdSet, DefaultObject, default_cmds

# Menu implementing the dialogue tree

def menu_start_node(caller):
    text = "'Hello there, how can I help you?'"

    options = (
        {"desc": "Hey, do you know what this 'Evennia' thing is all about?", "goto": "info1"},
        {"desc": "What's your name, little NPC?", "goto": "info2"},
    )

def info1(caller):
    text = "'Oh, Evennia is where you are right now! Don't you feel the power?'"

    options = (
        {"desc": "Sure, *I* do, not sure how you do though. You are just an NPC.", "goto": "info3"},
        {"desc": "Sure I do. What's yer name, NPC?", "goto": "info2"},
        {"desc": "Ok, bye for now then.", "goto": "END"},
    )

def info2(caller):
    text = "'My name is not really important ... I'm just an NPC after all.'"

    options = (
        {"desc": "I didn't really want to know it anyhow.", "goto": "info3"},
        {"desc": "Okay then, so what's this 'Evennia' thing about?", "goto": "info1"},
    )

def info3(caller):
    text = (
        "'Well ... I'm sort of busy so, have to go. NPC business. "
        "Important stuff. You wouldn't understand.'"
    )

    options = (
        {"desc": "Oookay ... I won't keep you. Bye.", "goto": "END"},
        {"desc": "Wait, why don't you tell me your name first?", "goto": "info2"},
    )

def END(caller):
    text = "'Goodbye, then.'"

    options = ()

#
# The talk command (sits on the NPC)
#

class CmdTalk(default_cmds.MuxCommand):
    """
    Talks to an npc

    Usage:
      talk

    This command is only available if a talkative non-player-character
    (NPC) is actually present. It will strike up a conversation with
    that NPC and give you options on what to talk about.
    """

        "Implements the command."

        # self.obj is the NPC this is defined on
        self.caller.msg("(You walk up and talk to %s.)" % self.obj.key)

        # Initiate the menu. Change this if you are putting this on
        # some other custom NPC class.
        EvMenu(
            "evennia.contrib.tutorials.talking_npc.talking_npc",
            startnode="menu_start_node",
        )

class TalkingCmdSet(CmdSet):
    "Stores the talk command."
    key = "talkingcmdset"

        "populates the cmdset"

class TalkingNPC(DefaultObject):
    """
    This implements a simple Object using the talk command and using
    the conversation defined above.
    """

        "This is called when object is first created."
        self.db.desc = "This is a talkative NPC."
        # assign the talk command to npc
        self.cmdset.add_default(TalkingCmdSet, persistent=True)

"""
Tutorial - talking NPC tests.

"""

from . import talking_npc

class TestTalkingNPC(BaseEvenniaCommandTest):
    def test_talkingnpc(self):
        npc = create_object(talking_npc.TalkingNPC, key="npctalker", location=self.room1)
        self.call(talking_npc.CmdTalk(), "", "(You walk up and talk to Char.)")
        npc.delete()

"""
Intro menu / game tutor

Evennia contrib - Griatch 2020

This contrib is an intro-menu for general MUD and evennia usage using the
EvMenu menu-templating system.

EvMenu templating is a way to create a menu using a string-format instead
of creating all nodes manually. Of course, for full functionality one must
still create the goto-callbacks.

"""

from evennia import CmdSet, create_object
from evennia.utils.evmenu import EvMenu, parse_menu_template

# Goto callbacks and helper resources for the menu

def do_nothing(caller, raw_string, **kwargs):
    """
    Re-runs the current node
    """

def send_testing_tagged(caller, raw_string, **kwargs):
    """
    Test to send a message to a pane tagged with 'testing' in the webclient.

    """
    caller.msg(
        (
            (
                "This is a message tagged with 'testing' and "
                "should appear in the pane you selected!\n "
                f"You wrote: '{raw_string}'"
            ),
            {"type": "testing"},
        )
    )

# Resources for the first help-command demo

class DemoCommandSetHelp(CmdSet):
    """
    Demo the help command
    """

    key = "Help Demo Set"
    priority = 2

        from evennia import default_cmds

        self.add(default_cmds.CmdHelp())
        self.add(default_cmds.CmdChannel())

def goto_command_demo_help(caller, raw_string, **kwargs):
    "Sets things up before going to the help-demo node"
    _maintain_demo_room(caller, delete=True)
    caller.cmdset.remove(DemoCommandSetRoom)
    caller.cmdset.remove(DemoCommandSetComms)
    caller.cmdset.add(DemoCommandSetHelp)  # TODO - make persistent
    return kwargs.get("gotonode") or "command_demo_help"

# Resources for the comms demo

class DemoCommandSetComms(CmdSet):
    """
    Demo communications
    """

    key = "Color Demo Set"
    no_exits = True
    no_objs = True

        self.add(default_cmds.CmdSay())
        self.add(default_cmds.CmdPose())
        self.add(default_cmds.CmdPage())
        self.add(default_cmds.CmdColorTest())

def goto_command_demo_comms(caller, raw_string, **kwargs):
    """
    Setup and go to the color demo node.
    """
    caller.cmdset.remove(DemoCommandSetHelp)
    caller.cmdset.add(DemoCommandSetComms)
    return kwargs.get("gotonode") or "comms_demo_start"

# Resources for the room demo

_ROOM_DESC = """
This is a small and comfortable wood cabin. Bright sunlight is shining in
through the windows.

Use |ylook sign|n or |yl sign|n to examine the wooden sign nailed to the wall.

"""

_SIGN_DESC = """
The small sign reads:

    Good! Now try '|ylook small|n'.

    ... You'll get a multi-match error! There are two things that 'small' could
    refer to here - the 'small wooden sign' or the 'small, cozy cabin' itself.  You will
    get a list of the possibilities.

    You could either tell Evennia which one you wanted by picking a unique part
    of their name (like '|ylook cozy|n') or use the number in the list to pick
    the one you want, like this:

        |ylook small-2|n

    As long as what you write is uniquely identifying you can be lazy and not
    write the full name of the thing you want to look at. Try '|ylook bo|n',
    '|yl co|n' or '|yl sm-1|n'!

    ... Oh, and if you see database-ids like (#1245) by the name of objects,
    it's because you are playing with Builder-privileges or higher. Regular
    players will not see the numbers.

    Next try |ylook door|n.

"""

_DOOR_DESC_OUT = """
This is a solid wooden door leading to the outside of the cabin. Some
text is written on it:

    This is an |wexit|n. An exit is often named by its compass-direction like
    |weast|n, |wwest|n, |wnorthwest|n and so on, but it could be named
    anything, like this door. To use the exit, you just write its name. So by
    writing |ydoor|n you will leave the cabin.

"""

_DOOR_DESC_IN = """
This is a solid wooden door leading to the inside of the cabin. On
are some carved text:

    This exit leads back into the cabin. An exit is just like any object,
    so while has a name, it can also have aliases. To get back inside
    you can both write |ydoor|n but also |yin|n.

"""

_MEADOW_DESC = """
This is a lush meadow, just outside a cozy cabin. It's surrounded
by trees and sunlight filters down from a clear blue sky.

There is a |wstone|n here. Try looking at it!

"""

_STONE_DESC = """
This is a fist-sized stone covered in runes:

    To pick me up, use

    |yget stone|n

    You can see what you carry with the |yinventory|n (|yi|n).

    To drop me again, just write

    |ydrop stone|n

    Use |ynext|n when you are done exploring and want to
    continue with the tutorial.

"""

def _maintain_demo_room(caller, delete=False):
    """
    Handle the creation/cleanup of demo assets. We store them
    on the character and clean them when leaving the menu later.
    """
    # this is a tuple (room, obj)
    roomdata = caller.db.tutorial_world_demo_room_data

    if delete:
        if roomdata:
            # we delete directly for simplicity. We need to delete
            # in specific order to avoid deleting rooms moves
            # its contents to their default home-location
            prev_loc, room1, sign, room2, stone, door_out, door_in = roomdata
            caller.location = prev_loc
            sign.delete()
            stone.delete()
            door_out.delete()
            door_in.delete()
            room1.delete()
            room2.delete()
            del caller.db.tutorial_world_demo_room_data
    elif not roomdata:
        # create and describe the cabin and box
        room1 = create_object("evennia.objects.objects.DefaultRoom", key="A small, cozy cabin")
        room1.db.desc = _ROOM_DESC.lstrip()
        sign = create_object(
            "evennia.objects.objects.DefaultObject", key="small wooden sign", location=room1
        )
        sign.db.desc = _SIGN_DESC.strip()
        sign.locks.add("get:false()")
        sign.db.get_err_msg = "The sign is nailed to the wall. It's not budging."

        # create and describe the meadow and stone
        room2 = create_object("evennia.objects.objects.DefaultRoom", key="A lush summer meadow")
        room2.db.desc = _MEADOW_DESC.lstrip()
        stone = create_object(
            "evennia.objects.objects.DefaultObject", key="carved stone", location=room2
        )
        stone.db.desc = _STONE_DESC.strip()

        # make the linking exits
        door_out = create_object(
            key="Door",
            location=room1,
            destination=room2,
            locks=["get:false()"],
        )
        door_out.db.desc = _DOOR_DESC_OUT.strip()
        door_in = create_object(
            key="entrance to the cabin",
            aliases=["door", "in", "entrance"],
            location=room2,
            destination=room1,
        )
        door_in.db.desc = _DOOR_DESC_IN.strip()

        # store references for easy removal later
        caller.db.tutorial_world_demo_room_data = (
            caller.location,
            room1,
            sign,
            room2,
            stone,
            door_out,
            door_in,
        )
        # move caller into room
        caller.location = room1

class DemoCommandSetRoom(CmdSet):
    """
    Demo some general in-game commands command.
    """

    key = "Room Demo Set"
    no_exits = False
    no_objs = False

        self.add(default_cmds.CmdLook())
        self.add(default_cmds.CmdGet())
        self.add(default_cmds.CmdDrop())
        self.add(default_cmds.CmdInventory())
        self.add(default_cmds.CmdExamine())
        self.add(default_cmds.CmdPy())

def goto_command_demo_room(caller, raw_string, **kwargs):
    """
    Setup and go to the demo-room node. Generates a little 2-room environment
    for testing out some commands.
    """
    _maintain_demo_room(caller)
    caller.cmdset.add(DemoCommandSetRoom)
    return "command_demo_room"

def goto_cleanup_cmdsets(caller, raw_strings, **kwargs):
    """
    Cleanup all cmdsets.
    """
    return kwargs.get("gotonode")

# register all callables that can be used in the menu template

GOTO_CALLABLES = {
    "send_testing_tagged": send_testing_tagged,
    "do_nothing": do_nothing,
    "goto_command_demo_help": goto_command_demo_help,
    "goto_command_demo_comms": goto_command_demo_comms,
    "goto_command_demo_room": goto_command_demo_room,
    "goto_cleanup_cmdsets": goto_cleanup_cmdsets,
}

# Main menu definition

MENU_TEMPLATE = """

## NODE start

|g** Evennia introduction wizard **|n

If you feel lost you can learn some of the basics of how to play a text-based
game here. You can also learn a little about the system and how to find more
help. You can exit this tutorial-wizard at any time by entering '|yq|n' or '|yquit|n'.

Press |y<return>|n or write |ynext|n to step forward. Or select a number to jump to.

## OPTIONS

    1 (next);1;next;n: What is a MUD/MU*? -> about_muds
    2: About Evennia -> about_evennia
    3: Using the webclient -> using webclient
    4: The help command -> goto_command_demo_help()
    5: Communicating with others -> goto_command_demo_help(gotonode='talk on channels')
    6: Using colors -> goto_command_demo_comms(gotonode='testing_colors')
    7: Moving and exploring -> goto_command_demo_room()
    8: Conclusions & next steps-> conclusions
    >: about_muds

# ---------------------------------------------------------------------------------

## NODE about_muds

|g** About MUDs **|n

The term '|wMUD|n' stands for Multi-user-Dungeon or -Dimension. A MUD is
primarily played by inserting text |wcommands|n and getting text back.

MUDS were the |wprecursors|n to graphical MMORPG-style games like World of
Warcraft. While not as mainstream as they once were, comparing a text-game to a
graphical game is like comparing a book to a movie - it's just a different
experience altogether.

MUDs are |wdifferent|n from Interactive Fiction (IF) in that they are multiplayer
and usually have a consistent game world with many stories and protagonists
acting at the same time.

Like there are many different styles of graphical MMOs, there are |wmany
variations|n of MUDs: They can be slow-paced or fast. They can cover fantasy,
sci-fi, horror or other genres. They can allow PvP or not and be casual or
hardcore, strategic, tactical, turn-based or play in real-time.

Whereas 'MUD' is arguably the most well-known term, there are other terms
centered around particular game engines - such as MUSH, MOO, MUX, MUCK, LPMuds,
ROMs, Diku and others. Many people that played MUDs in the past used one of
these existing families of text game-servers, whether they knew it or not.

|cEvennia|n is a newer text game engine designed to emulate almost any existing
gaming style you like and possibly any new ones you can come up with!

## OPTIONS

    next;n: About Evennia -> about_evennia
    back to start;back;start;t: start
    >: about_evennia

## NODE about_evennia

|g** About Evennia **|n

|cEvennia|n is a Python game engine for creating multiplayer online text-games
(aka MUDs, MUSHes, MUX, MOOs...). It is open-source and |wfree to use|n, also for
commercial projects (BSD license).

Out of the box, Evennia provides a |wworking, if empty game|n. Whereas you can play
via traditional telnet MUD-clients, the server runs your game's website and
offers a |wHTML5 webclient|n so that people can play your game in their browser
without downloading anything extra.

Evennia deliberately |wdoes not|n hard-code any game-specific things like
combat-systems, races, skills, etc. They would not match what just you wanted
anyway! Whereas we do have optional contribs with many examples, most of our
users use them as inspiration to make their own thing.

Evennia is developed entirely in |wPython|n, using modern developer practices.
The advantage of text is that even a solo developer or small team can
realistically make a competitive multiplayer game (as compared to a graphical
MMORPG which is one of the most expensive game types in existence to develop).
Many also use Evennia as a |wfun way to learn Python|n!

## OPTIONS

    next;n: Using the webclient -> using webclient
    back;b: About MUDs -> about_muds
    >: using webclient

## NODE using webclient

|g** Using the Webclient **|n

|RNote: This is only relevant if you use Evennia's HTML5 web client. If you use a
third-party (telnet) mud-client, you can skip this section.|n

Evennia's web client is (for a local install) found by pointing your browser to

    |yhttp://localhost:4001/webclient|n

For a live example, the public Evennia demo can be found at

    |yhttps://demo.evennia.com/webclient|n

The web client starts out having two panes - the input-pane for entering commands
and the main window.

- Use |y<Return>|n (or click the arrow on the right) to send your input.
- Use |yCtrl + <up/down-arrow>|n to step back and forth in your command-history.
- Use |yCtrl + <Return>|n to add a new line to your input without sending.
(Cmd instead of Ctrl-key on Macs)

There is also some |wextra|n info to learn about customizing the webclient.

## OPTIONS

    extra: Customizing the webclient -> customizing the webclient
    next;n: Playing the game -> goto_command_demo_help()
    back;b: About Evennia -> about_evennia
    back to start;start: start
    >: goto_command_demo_help()

# this is a dead-end 'leaf' of the menu

## NODE customizing the webclient

|g** Extra hints on customizing the Webclient **|n

|y1)|n The panes of the webclient can be resized and you can create additional panes.

- Press the little  plus (|w+|n) sign in the top left and a new tab will appear.
- Click and drag the tab and pull it far to the right and release when it creates two
  panes next to each other.

|y2)|n You can have certain server output only appear in certain panes.

- In your new rightmost pane, click the diamond (⯁) symbol at the top.
- Unselect everything and make sure to select "testing".
- Click the diamond again so the menu closes.
- Next, write "|ytest Hello world!|n". A test-text should appear in your rightmost pane!

|y3)|n You can customize general webclient settings by pressing the cogwheel in the upper
left corner. It allows to change things like font and if the client should play sound.

The "message routing" allows for rerouting text matching a certain regular expression (regex)
to a web client pane with a specific tag that you set yourself.

|y4)|n Close the right-hand pane with the |wX|n in the rop right corner.

## OPTIONS

    back;b: using webclient
    > test *: send tagged message to new pane -> send_testing_tagged()

# we get here via goto_command_demo_help()

## NODE command_demo_help

|g** Playing the game **|n

Evennia has about |w90 default commands|n. They include useful administration/building
commands and a few limited "in-game" commands to serve as examples. They are intended
to be changed, extended and modified as you please.

First to try is |yhelp|n. This lists all commands |wcurrently|n available to you.

Use |yhelp <topic>|n to get specific help. Try |yhelp help|n to get help on using
the help command. For your game you could add help about your game, lore, rules etc
as well.

At the moment you probably only have |whelp|n and a |wchannel|n command
(the '<menu commands>' is just a placeholder to indicate you are using this menu).

We'll add more commands as we get to them in this tutorial - but we'll only
cover a small handful. Once you exit you'll find a lot more! Now let's try
those channels ...

## OPTIONS

    next;n: Talk on Channels -> talk on channels
    back;b: Using the webclient -> goto_cleanup_cmdsets(gotonode='using webclient')
    >: talk on channels

## NODE talk on channels

|g** Talk on Channels **|n

|wChannels|n are like in-game chatrooms. The |wChannel Names|n help-category
holds the names of the channels available to you right now. One such channel is
|wpublic|n. Use |yhelp public|n to see how to use it. Try it:

    |ypublic Hello World!|n

This will send a message to the |wpublic|n channel where everyone on that
channel can see it. If someone else is on your server, you may get a reply!

Evennia can link its in-game channels to external chat networks. This allows
you to talk with people not actually logged into the game.

## OPTIONS

    next;n: Talk to people in-game -> goto_command_demo_comms()
    back;b: Finding help -> goto_command_demo_help()
    >: goto_command_demo_comms()

# we get here via goto_command_demo_comms()

## NODE comms_demo_start

|g** Talk to people in-game **|n

You can also chat with people inside the game. If you try |yhelp|n now you'll
find you have a few more commands available for trying this out.

    |ysay Hello there!|n
    |y'Hello there!|n

|wsay|n is used to talk to people in the same location you are. Everyone in the
room will see what you have to say. A single quote |y'|n is a  convenient shortcut.

    |ypose smiles|n
    |y:smiles|n

|wpose|n (or |wemote|n) describes what you do to those nearby. This is a very simple
command by default, but it can be extended to much more complex parsing in order to
include other people/objects in the emote, reference things by a short-description etc.

## OPTIONS

    next;n: Paging people -> paging_people
    back;b: Talk on Channels -> goto_command_demo_help(gotonode='talk on channels')
    >: paging_people

## NODE paging_people

|g** Paging people **|n

Halfway between talking on a |wChannel|n and chatting in your current location
with |wsay|n and |wpose|n, you can also send private messages with |wpage|n:

    |ypage <name> Hello there!|n

Put your own name as |y<name>|n to page yourself as a test. Write just |ypage|n
to see your latest pages. This will also show you if anyone paged you while you
were offline.

## OPTIONS

    next;n: Using colors -> testing_colors
    back;b: Talk to people in-game -> comms_demo_start
    >: testing_colors

## NODE testing_colors

|g** U|rs|yi|gn|wg |c|yc|wo|rl|bo|gr|cs |g**|n

You can add color in your text by the help of tags. However, remember that not
everyone will see your colors - it depends on their client (and some use
screenreaders). Using color can also make text harder to read. So use it
sparingly.

To start coloring something |rred|n, add a ||r (red) marker and then
end with ||n (to go back to neutral/no-color):

    |ysay This is a ||rred||n text!
    say This is a ||Rdark red||n text!|n

You can also change the background:

    |ysay This is a ||[x||bblue text on a light-grey background!|n

There are 16 base colors and as many background colors (called ANSI colors). Some
clients also supports so-called Xterm256 which gives a total of 256 colors. These are
given as |w||rgb|n, where r, g, b are the components of red, green and blue from 0-5:

    |ysay This is ||050solid green!|n
    |ysay This is ||520an orange color!|n
    |ysay This is ||[005||555white on bright blue background!|n

If you don't see the expected colors from the above examples, it's because your
client does not support it - try out the Evennia webclient instead. To see all
color codes printed, try

    |ycolor ansi
    |ycolor xterm

## OPTIONS

    next;n: Moving and Exploring -> goto_command_demo_room()
    back;b: Paging people -> goto_command_demo_comms(gotonode='paging_people')
    >: goto_command_demo_room()

# we get here via goto_command_demo_room()

## NODE command_demo_room

|gMoving and Exploring|n

For exploring the game, a very important command is '|ylook|n'. It's also
abbreviated '|yl|n' since it's used so much. Looking displays/redisplays your
current location. You can also use it to look closer at items in the world. So
far in this tutorial, using 'look' would just redisplay the menu.

Try |ylook|n now. You have been quietly transported to a sunny cabin to look
around in. Explore a little and use |ynext|n when you are done.

## OPTIONS

    next;n: Conclusions -> conclusions
    back;b: Channel commands -> goto_command_demo_comms(gotonode='testing_colors')
    >: conclusions

## NODE conclusions

|gConclusions|n

That concludes this little quick-intro to using the base game commands of
Evennia. With this you should be able to continue exploring and also find help
if you get stuck!

Write |ynext|n to end this wizard. If you want there is also some |wextra|n info
for where to go beyond that.

## OPTIONS

    extra: Where to go next -> post scriptum
    next;next;n: End -> end
    back;b: Moving and Exploring -> goto_command_demo_room()
    >: end

## NODE post scriptum

|gWhere to next?|n

After playing through the tutorial-world quest, if you aim to make a game with
Evennia you are wise to take a look at the |wEvennia documentation|n at

    |yhttps://www.evennia.com/docs/latest|n

- You can start by trying to build some stuff by following the |wBuilder quick-start|n:

    |yhttps://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Building-Quickstart|n

- The tutorial-world may or may not be your cup of tea, but it does show off
  several |wuseful tools|n of Evennia. You may want to check out how it works:

    |yhttps://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Tutorial-World|n

- You can then continue looking through the |wTutorials|n and pick one that
  fits your level of understanding.

    |yhttps://www.evennia.com/docs/latest/Howtos/Howtos-Overview|n

- Make sure to |wjoin our forum|n and connect to our |wsupport chat|n! The
  Evennia community is very active and friendly and no question is too simple.
  You will often quickly get help. You can everything you need linked from

    |yhttps://www.evennia.com|n

## OPTIONS

back: conclusions
>: conclusions

## NODE end

|gGood luck!|n

"""

# -------------------------------------------------------------------------------------------
#
# EvMenu implementation and access function
#

class TutorialEvMenu(EvMenu):
    """
    Custom EvMenu for displaying the intro-menu
    """

    def close_menu(self):
        """Custom cleanup actions when closing menu"""
        self.caller.cmdset.remove(DemoCommandSetHelp)
        self.caller.cmdset.remove(DemoCommandSetRoom)
        self.caller.cmdset.remove(DemoCommandSetComms)
        _maintain_demo_room(self.caller, delete=True)
        super().close_menu()
        if self.caller.account:
            self.caller.msg("Restoring permissions ...")
            self.caller.account.execute_cmd("unquell")

    def options_formatter(self, optionslist):
        navigation_keys = ("next", "back", "back to start")

        other = []
        navigation = []
        for key, desc in optionslist:
            if key in navigation_keys:
                desc = f" ({desc})" if desc else ""
                navigation.append(f"|lc{key}|lt|w{key}|n|le{desc}")
            else:
                other.append((key, desc))
        navigation = (
            (" " + " |W|||n ".join(navigation) + " |W|||n " + "|wQ|Wuit|n") if navigation else ""
        )
        other = super().options_formatter(other)
        sep = "\n\n" if navigation and other else ""

        return f"{navigation}{sep}{other}"

def init_menu(caller):
    """
    Call to initialize the menu.

    """
    menutree = parse_menu_template(caller, MENU_TEMPLATE, GOTO_CALLABLES)
    TutorialEvMenu(caller, menutree)

"""
This module implements a simple mobile object with
a very rudimentary AI as well as an aggressive enemy
object based on that mobile class.

"""

from evennia import TICKER_HANDLER, CmdSet, Command, logger, search_object

from . import objects as tut_objects

class CmdMobOnOff(Command):
    """
    Activates/deactivates Mob

    Usage:
        mobon <mob>
        moboff <mob>

    This turns the mob from active (alive) mode
    to inactive (dead) mode. It is used during
    building to  activate the mob once it's
    prepared.
    """

    key = "mobon"
    aliases = "moboff"

        """
        Uses the mob's set_alive/set_dead methods
        to turn on/off the mob."
        """
            self.caller.msg("Usage: mobon||moboff <mob>")
            return
        mob = self.caller.search(self.args)
        if not mob:
            return
        if self.cmdstring == "mobon":
            mob.set_alive()
        else:
            mob.set_dead()

class MobCmdSet(CmdSet):
    """
    Holds the admin command controlling the mob
    """

        self.add(CmdMobOnOff())

class Mob(tut_objects.TutorialObject):
    """
    This is a state-machine AI mobile. It has several states which are
    controlled from setting various Attributes. All default to True:

        patrolling: if set, the mob will move randomly
            from room to room, but preferring to not return
            the way it came. If unset, the mob will remain
            stationary (idling) until attacked.
        aggressive: if set, will attack Characters in
            the same room using whatever Weapon it
            carries (see tutorial_world.objects.TutorialWeapon).
            if unset, the mob will never engage in combat
            no matter what.
        hunting: if set, the mob will pursue enemies trying
            to flee from it, so it can enter combat. If unset,
            it will return to patrolling/idling if fled from.
        immortal: If set, the mob cannot take any damage.
        irregular_echoes: list of strings the mob generates at irregular intervals.
        desc_alive: the physical description while alive
        desc_dead: the physical descripion while dead
        send_defeated_to: unique key/alias for location to send defeated enemies to
        defeat_msg: message to echo to defeated opponent
        defeat_msg_room: message to echo to room. Accepts %s as the name of the defeated.
        hit_msg: message to echo when this mob is hit. Accepts %s for the mob's key.
        weapon_ineffective_msg: message to echo for useless attacks
        death_msg: message to echo to room when this mob dies.
        patrolling_pace: how many seconds per tick, when patrolling
        aggressive_pace:   -"-         attacking
        hunting_pace:      -"-         hunting
        death_pace:        -"-         returning to life when dead

        field 'home' - the home location should set to someplace inside
           the patrolling area. The mob will use this if it should
           happen to roam into a room with no exits.

    """

        """
        When initialized from cache (after a server reboot), set up
        the AI state.
        """
        # The AI state machine (not persistent).
        self.ndb.is_patrolling = self.db.patrolling and not self.db.is_dead
        self.ndb.is_attacking = False
        self.ndb.is_hunting = False
        self.ndb.is_immortal = self.db.immortal or self.db.is_dead

        """
        Called the first time the object is created.
        We set up the base properties and flags here.
        """
        self.cmdset.add(MobCmdSet, persistent=True)
        # Main AI flags. We start in dead mode so we don't have to
        # chase the mob around when building.
        self.db.patrolling = True
        self.db.aggressive = True
        self.db.immortal = False
        # db-store if it is dead or not
        self.db.is_dead = True
        # specifies how much damage we divide away from non-magic weapons
        self.db.damage_resistance = 100.0
        # pace (number of seconds between ticks) for
        # the respective modes.
        self.db.patrolling_pace = 6
        self.db.aggressive_pace = 2
        self.db.hunting_pace = 1
        self.db.death_pace = 100  # stay dead for 100 seconds

        # we store the call to the tickerhandler
        # so we can easily deactivate the last
        # ticker subscription when we switch.
        # since we will use the same idstring
        # throughout we only need to save the
        # previous interval we used.
        self.db.last_ticker_interval = None

        # store two separate descriptions, one for alive and
        # one for dead (corpse)
        self.db.desc_alive = "This is a moving object."
        self.db.desc_dead = "A dead body."

        # health stats
        self.db.full_health = 20
        self.db.health = 20

        # when this mob defeats someone, we move the character off to
        # some other place (Dark Cell in the tutorial).
        self.db.send_defeated_to = "dark cell"
        # text to echo to the defeated foe.
        self.db.defeat_msg = "You fall to the ground."
        self.db.defeat_msg_room = "%s falls to the ground."
        self.db.weapon_ineffective_msg = (
            "Your weapon just passes through your enemy, causing almost no effect!"
        )

        self.db.death_msg = "After the last hit %s evaporates." % self.key
        self.db.hit_msg = "%s wails, shudders and writhes." % self.key
        self.db.irregular_msgs = ["the enemy looks about.", "the enemy changes stance."]

        self.db.tutorial_info = "This is an object with simple state AI, using a ticker to move."

    def _set_ticker(self, interval, hook_key, stop=False):
        """
        Set how often the given hook key should
        be "ticked".

        Args:
            interval (int or None): The number of seconds
                between ticks
            hook_key (str or None): The name of the method
                (on this mob) to call every interval
                seconds.
            stop (bool, optional): Just stop the
                last ticker without starting a new one.
                With this set, the interval and hook_key
                arguments are unused.

        In order to only have one ticker
        running at a time, we make sure to store the
        previous ticker subscription so that we can
        easily find and stop it before setting a
        new one. The tickerhandler is persistent so
        we need to remember this across reloads.

        """
        idstring = "tutorial_mob"  # this doesn't change
        last_interval = self.db.last_ticker_interval
        last_hook_key = self.db.last_hook_key
        if last_interval and last_hook_key:
            # we have a previous subscription, kill this first.
            TICKER_HANDLER.remove(
                interval=last_interval, callback=getattr(self, last_hook_key), idstring=idstring
            )
        self.db.last_ticker_interval = interval
        self.db.last_hook_key = hook_key
        if not stop:
            # set the new ticker
            TICKER_HANDLER.add(
                interval=interval, callback=getattr(self, hook_key), idstring=idstring
            )

    def _find_target(self, location):
        """
        Scan the given location for suitable targets (this is defined
        as Characters) to attack.  Will ignore superusers.

        Args:
            location (Object): the room to scan.

        Returns:
            The first suitable target found.

        """
        targets = [
            obj
            for obj in location.contents_get(exclude=self)
            if obj.has_account and not obj.is_superuser
        ]
        return targets[0] if targets else None

    def set_alive(self, *args, **kwargs):
        """
        Set the mob to "alive" mode. This effectively
        resurrects it from the dead state.
        """
        self.db.health = self.db.full_health
        self.db.is_dead = False
        self.db.desc = self.db.desc_alive
        self.ndb.is_immortal = self.db.immortal
        self.ndb.is_patrolling = self.db.patrolling
            self.move_to(self.home)
        if self.db.patrolling:
            self.start_patrolling()

    def set_dead(self):
        """
        Set the mob to "dead" mode. This turns it off
        and makes sure it can take no more damage.
        It also starts a ticker for when it will return.
        """
        self.location = None
        self.ndb.is_patrolling = False
        self.ndb.is_immortal = True
        # we shall return after some time
        self._set_ticker(self.db.death_pace, "set_alive")

    def start_idle(self):
        """
        Starts just standing around. This will kill
        the ticker and do nothing more.
        """
        self._set_ticker(None, None, stop=True)

    def start_patrolling(self):
        """
        Start the patrolling state by
        registering us with the ticker-handler
        at a leasurely pace.
        """
        if not self.db.patrolling:
            self.start_idle()
            return
        self._set_ticker(self.db.patrolling_pace, "do_patrol")
        self.ndb.is_patrolling = True
        # for the tutorial, we also heal the mob in this mode

    def start_hunting(self):
        """
        Start the hunting state
        """
        if not self.db.hunting:
            return
        self._set_ticker(self.db.hunting_pace, "do_hunt")
        self.ndb.is_hunting = True

    def start_attacking(self):
        """
        Start the attacking state
        """
        if not self.db.aggressive:
            self.start_hunting()
            return
        self._set_ticker(self.db.aggressive_pace, "do_attack")
        self.ndb.is_attacking = True

    def do_patrol(self, *args, **kwargs):
        """
        Called repeatedly during patrolling mode.  In this mode, the
        mob scans its surroundings and randomly chooses a viable exit.
        One should lock exits with the traverse:has_account() lock in
        order to block the mob from moving outside its area while
        allowing account-controlled characters to move normally.
        """
        if random.random() < 0.01 and self.db.irregular_msgs:
            self.location.msg_contents(random.choice(self.db.irregular_msgs))
        if self.db.aggressive:
            # first check if there are any targets in the room.
            target = self._find_target(self.location)
            if target:
                self.start_attacking()
                return
        # no target found, look for an exit.
        exits = [exi for exi in self.location.exits if exi.access(self, "traverse")]
        if exits:
            # randomly pick an exit
            exit = random.choice(exits)
            # move there.
            self.move_to(exit.destination)
        else:
            # no exits! teleport to home to get away.

    def do_hunting(self, *args, **kwargs):
        """
        Called regularly when in hunting mode. In hunting mode the mob
        scans adjacent rooms for enemies and moves towards them to
        attack if possible.
        """
            if target:
                return
        # no targets found, scan surrounding rooms
        if exits:
            # scan the exits destination for targets
            for exit in exits:
                target = self._find_target(exit.destination)
                if target:
                    # a target found. Move there.
                    self.move_to(exit.destination)
                    return
            # if we get to this point we lost our
            # prey. Resume patrolling.
        else:

    def do_attack(self, *args, **kwargs):
        """
        Called regularly when in attacking mode. In attacking mode
        the mob will bring its weapons to bear on any targets
        in the room.
        """
        # first make sure we have a target
        target = self._find_target(self.location)
            # no target, start looking for one
            return

        # we use the same attack commands as defined in
        # tutorial_world.objects.TutorialWeapon, assuming that
        # the mob is given a Weapon to attack with.
        attack_cmd = random.choice(("thrust", "pierce", "stab", "slash", "chop"))
        self.execute_cmd("%s %s" % (attack_cmd, target))

        # analyze the current state
        if target.db.health <= 0:
            # we reduced the target to <= 0 health. Move them to the
            # defeated room
            target.msg(self.db.defeat_msg)
            self.location.msg_contents(self.db.defeat_msg_room % target.key, exclude=target)
            send_defeated_to = search_object(self.db.send_defeated_to)
            if send_defeated_to:
                target.move_to(send_defeated_to[0], quiet=True)
            else:
                    "Mob: mob.db.send_defeated_to not found: %s" % self.db.send_defeated_to
                )

    # response methods - called by other objects

    def at_hit(self, weapon, attacker, damage):
        """
        Someone landed a hit on us. Check our status
        and start attacking if not already doing so.
        """
        if self.db.health is None:
            # health not set - this can't be damaged.
            attacker.msg(self.db.weapon_ineffective_msg)
            return

        if not self.ndb.is_immortal:
            if not weapon.db.magic:
                # not a magic weapon - divide away magic resistance
                damage /= self.db.damage_resistance
                attacker.msg(self.db.weapon_ineffective_msg)
            else:
                self.location.msg_contents(self.db.hit_msg)
            self.db.health -= damage

        # analyze the result
        if self.db.health <= 0:
            # we are dead!
            attacker.msg(self.db.death_msg)
            self.set_dead()
        else:
            # still alive, start attack if not already attacking
            if self.db.aggressive and not self.ndb.is_attacking:

    def at_new_arrival(self, new_character):
        """
        This is triggered whenever a new character enters the room.
        This is called by the TutorialRoom the mob stands in and
        allows it to be aware of changes immediately without needing
        to poll for them all the time. For example, the mob can react
        right away, also when patrolling on a very slow ticker.
        """
        # the room actually already checked all we need, so
        # we know it is a valid target.
        if self.db.aggressive and not self.ndb.is_attacking:
            self.start_attacking()

"""
TutorialWorld - basic objects - Griatch 2011

This module holds all "dead" object definitions for
the tutorial world. Object-commands and -cmdsets
are also defined here, together with the object.

Objects:

TutorialObject

TutorialReadable
TutorialClimbable
Obelisk
LightSource
CrumblingWall
TutorialWeapon
TutorialWeaponRack

"""

from evennia import CmdSet, Command, DefaultExit, DefaultObject
from evennia.utils import dedent, delay, search

#
# TutorialObject
#
# The TutorialObject is the base class for all items
# in the tutorial. They have an attribute "tutorial_info"
# on them that the global tutorial command can use to extract
# interesting behind-the scenes information about the object.
#
# TutorialObjects may also be "reset". What the reset means
# is up to the object. It can be the resetting of the world
# itself, or the removal of an inventory item from a
# character's inventory when leaving the tutorial, for example.
#

class TutorialObject(DefaultObject):
    """
    This is the baseclass for all objects in the tutorial.
    """

        """Called when the object is first created."""
        self.db.tutorial_info = "No tutorial info is available for this object."

        """Resets the object, whatever that may mean."""
        self.location = self.home

#
# Readable - an object that can be "read"
#

#
# Read command
#

class CmdRead(Command):
    """
    Usage:
      read [obj]

    Read some text of a readable object.
    """

    key = "read"
    help_category = "TutorialWorld"

        """
        Implements the read command. This simply looks for an
        Attribute "readable_text" on the object and displays that.
        """

            obj = self.caller.search(self.args.strip())
        else:
        if not obj:
            return
        # we want an attribute read_text to be defined.
        readtext = obj.db.readable_text
        if readtext:
            string = "You read |C%s|n:\n  %s" % (obj.key, readtext)
        else:
            string = "There is nothing to read on %s." % obj.key

class CmdSetReadable(CmdSet):
    """
    A CmdSet for readables.
    """

        """
        Called when the cmdset is created.
        """
        self.add(CmdRead())

class TutorialReadable(TutorialObject):
    """
    This simple object defines some attributes and
    """

        """
        Called when object is created. We make sure to set the needed
        Attribute and add the readable cmdset.
        """
        self.db.tutorial_info = (
            "This is an object with a 'read' command defined in a command set on itself."
        )
        self.db.readable_text = "There is no text written on %s." % self.key
        # define a command on the object.
        self.cmdset.add_default(CmdSetReadable, persistent=True)

#
# Climbable object
#
# The climbable object works so that once climbed, it sets
# a flag on the climber to show that it was climbed. A simple
# command 'climb' handles the actual climbing. The memory
# of what was last climbed is used in a simple puzzle in the
# tutorial.
#

class CmdClimb(Command):
    """
    Climb an object

    Usage:
      climb <object>

    This allows you to climb.
    """

    key = "climb"

        """Implements function"""

            self.caller.msg("What do you want to climb?")
            return
        obj = self.caller.search(self.args.strip())
        if not obj:
            return
        if obj != self.obj:
            self.caller.msg("Try as you might, you cannot climb that.")
            return
        ostring = self.obj.db.climb_text
        if not ostring:
            ostring = "You climb %s. Having looked around, you climb down again." % self.obj.name
        self.caller.msg(ostring)
        # set a tag on the caller to remember that we climbed.
        self.caller.tags.add("tutorial_climbed_tree", category="tutorial_world")

class CmdSetClimbable(CmdSet):
    """Climbing cmdset"""

        """populate set"""
        self.add(CmdClimb())

class TutorialClimbable(TutorialObject):
    """
    A climbable object. All that is special about it is that it has
    the "climb" command available on it.
    """

        """Called at initial creation only"""
        self.cmdset.add_default(CmdSetClimbable, persistent=True)

#
# Obelisk - a unique item
#
# The Obelisk is an object with a modified return_appearance method
# that causes it to look slightly different every time one looks at it.
# Since what you actually see is a part of a game puzzle, the act of
# looking also stores a key attribute on the looking object (different
# depending on which text you saw) for later reference.
#

class Obelisk(TutorialObject):
    """
    This object changes its description randomly, and which is shown
    determines which order "clue id" is stored on the Character for
    future puzzles.

    Important Attribute:
       puzzle_descs (list): list of descriptions. One of these is
        picked randomly when this object is looked at and its index
        in the list is used as a key for to solve the puzzle.

    """

        """Called when object is created."""
            "This object changes its desc randomly, and makes sure to remember which one you saw."
        )
        self.db.puzzle_descs = ["You see a normal stone slab"]
        # make sure this can never be picked up
        self.locks.add("get:false()")

    def return_appearance(self, caller):
        """
        This hook is called by the look command to get the description
        of the object. We overload it with our own version.
        """
        # randomly get the index for one of the descriptions
        descs = self.db.puzzle_descs
        clueindex = random.randint(0, len(descs) - 1)
        # set this description, with the random extra
        string = (
            "The surface of the obelisk seem to waver, shift and writhe under your gaze, with "
            "different scenes and structures appearing whenever you look at it. "
        )
        self.db.desc = string + descs[clueindex]
        # remember that this was the clue we got. The Puzzle room will
        # look for this later to determine if you should be teleported
        # or not.
        caller.db.puzzle_clue = clueindex
        # call the parent function as normal (this will use
        # the new desc Attribute we just set)
        return super().return_appearance(caller)

#
# LightSource
#
# This object emits light. Once it has been turned on it
# cannot be turned off. When it burns out it will delete
# itself.
#
# This could be implemented using a single-repeat Script or by
# registering with the TickerHandler. We do it simpler by
# using the delay() utility function. This is very simple
# to use but does not survive a server @reload. Because of
# where the light matters (in the Dark Room where you can
# find new light sources easily), this is okay here.
#

class CmdLight(Command):
    """
    Creates light where there was none. Something to burn.
    """

    key = "on"
    aliases = ["light", "burn"]
    # only allow this command if command.obj is carried by caller.
    locks = "cmd:holds()"

        """
        Implements the light command. Since this command is designed
        to sit on a "lightable" object, we operate only on self.obj.
        """

        if self.obj.light():
            self.caller.msg("You light %s." % self.obj.key)
                "%s lights %s!" % (self.caller, self.obj.key), exclude=[self.caller]
            )
        else:
            self.caller.msg("%s is already burning." % self.obj.key)

class CmdSetLight(CmdSet):
    """CmdSet for the lightsource commands"""

    key = "lightsource_cmdset"
    # this is higher than the dark cmdset - important!
    priority = 3

        """called at cmdset creation"""
        self.add(CmdLight())

class LightSource(TutorialObject):
    """
    This implements a light source object.

    When burned out, the object will be deleted.
    """

        """
        If this is called with the Attribute is_giving_light already
        set, we know that the timer got killed by a server
        reload/reboot before it had time to finish. So we kill it here
        instead. This is the price we pay for the simplicity of the
        non-persistent delay() method.
        """
        if self.db.is_giving_light:

        """Called when object is first created."""
            "This object can be lit to create light. It has a timeout for how long it burns."
        )
        self.db.is_giving_light = False
        self.db.burntime = 60 * 3  # 3 minutes
        # this is the default desc, it can of course be customized
        # when created.
        self.db.desc = "A splinter of wood with remnants of resin on it, enough for burning."
        # add the Light command
        self.cmdset.add_default(CmdSetLight, persistent=True)

    def _burnout(self):
        """
        This is called when this light source burns out. We make no
        use of the return value.
        """
        # delete ourselves from the database
        try:
            self.location.location.msg_contents(
                "%s's %s flickers and dies." % (self.location, self.key), exclude=self.location
            )
            self.location.msg("Your %s flickers and dies." % self.key)
            self.location.location.check_light_state()
            try:
                self.location.msg_contents("A %s on the floor flickers and dies." % self.key)
                self.location.location.check_light_state()
                # Mainly happens if we happen to be in a None location
                pass

    def light(self):
        """
        Light this object - this is called by Light command.
        """
        # burn for 3 minutes before calling _burnout
        self.db.is_giving_light = True
        # if we are in a dark room, trigger its light check
        try:
            try:
                # maybe we are directly in the room
                self.location.check_light_state()
                # we are in a None location
                pass
        finally:
            # start the burn timer. When it runs out, self._burnout
            # will be called. We store the deferred so it can be
            # killed in unittesting.
            self.deferred = delay(60 * 3, self._burnout)

#
# Crumbling wall - unique exit
#
# This implements a simple puzzle exit that needs to be
# accessed with commands before one can get to traverse it.
#
# The puzzle-part is simply to move roots (that have
# presumably covered the wall) aside until a button for a
# secret door is revealed. The original position of the
# roots blocks the button, so they have to be moved to a certain
# position - when they have, the "press button" command
# is made available and the Exit is made traversable.
#

# There are four roots - two horizontal and two vertically
# running roots. Each can have three positions: top/middle/bottom
# and left/middle/right respectively. There can be any number of
# roots hanging through the middle position, but only one each
# along the sides. The goal is to make the center position clear.
# (yes, it's really as simple as it sounds, just move the roots
# to each side to "win". This is just a tutorial, remember?)
#
# The ShiftRoot command depends on the root object having an
# Attribute root_pos (a dictionary) to describe the current
# position of the roots.

class CmdShiftRoot(Command):
    """
    Shifts roots around.

    Usage:
      shift blue root left/right
      shift red root left/right
      shift yellow root up/down
      shift green root up/down

    """

    key = "shift"
    aliases = ["shiftroot", "push", "pull", "move"]
    # we only allow to use this command while the
    # room is properly lit, so we lock it to the
    # setting of Attribute "is_lit" on our location.
    locks = "cmd:locattr(is_lit)"

        """
        Custom parser; split input by spaces for simplicity.
        """
        self.arglist = self.args.strip().split()

        """
        Implement the command.
          blue/red - vertical roots
          yellow/green - horizontal roots
        """

        if not self.arglist:
            self.caller.msg("What do you want to move, and in what direction?")
            return

        if "root" in self.arglist:
            # we clean out the use of the word "root"
            self.arglist.remove("root")

        # we accept arguments on the form <color> <direction>

        if not len(self.arglist) > 1:
                "You must define which colour of root you want to move, and in which direction."
            )
            return

        color = self.arglist[0].lower()
        direction = self.arglist[1].lower()

        # get current root positions dict
        root_pos = self.obj.db.root_pos

        if color not in root_pos:
            self.caller.msg("No such root to move.")
            return

        # first, vertical roots (red/blue) - can be moved left/right
        if color == "red":
            if direction == "left":
                root_pos[color] = max(-1, root_pos[color] - 1)
                self.caller.msg("You shift the reddish root to the left.")
                if root_pos[color] != 0 and root_pos[color] == root_pos["blue"]:
                    root_pos["blue"] += 1
                        "The root with blue flowers gets in the way and is pushed to the right."
                    )
            elif direction == "right":
                root_pos[color] = min(1, root_pos[color] + 1)
                self.caller.msg("You shove the reddish root to the right.")
                    root_pos["blue"] -= 1
                        "The root with blue flowers gets in the way and is pushed to the left."
                    )
            else:
                    "The root hangs straight down - you can only move it left or right."
                )
        elif color == "blue":
                self.caller.msg("You shift the root with small blue flowers to the left.")
                if root_pos[color] != 0 and root_pos[color] == root_pos["red"]:
                    root_pos["red"] += 1
                        "The reddish root is too big to fit as well, so that one falls away to the left."
                    )
                self.caller.msg("You shove the root adorned with small blue flowers to the right.")
                    root_pos["red"] -= 1
                        "The thick reddish root gets in the way and is pushed back to the left."
                    )
            else:
                )

        # now the horizontal roots (yellow/green). They can be moved up/down
        elif color == "yellow":
            if direction == "up":
                self.caller.msg("You shift the root with small yellow flowers upwards.")
                if root_pos[color] != 0 and root_pos[color] == root_pos["green"]:
                    root_pos["green"] += 1
                    self.caller.msg("The green weedy root falls down.")
            elif direction == "down":
                self.caller.msg("You shove the root adorned with small yellow flowers downwards.")
                    root_pos["green"] -= 1
                    self.caller.msg("The weedy green root is shifted upwards to make room.")
            else:
                self.caller.msg("The root hangs across the wall - you can only move it up or down.")
        elif color == "green":
                self.caller.msg("You shift the weedy green root upwards.")
                if root_pos[color] != 0 and root_pos[color] == root_pos["yellow"]:
                    root_pos["yellow"] += 1
                    self.caller.msg("The root with yellow flowers falls down.")
                self.caller.msg("You shove the weedy green root downwards.")
                    root_pos["yellow"] -= 1
                        "The root with yellow flowers gets in the way and is pushed upwards."
                    )
            else:

        # we have moved the root. Store new position
        self.obj.db.root_pos = root_pos

        # Check victory condition
        if list(root_pos.values()).count(0) == 0:  # no roots in middle position
            # This will affect the cmd: lock of CmdPressButton
            self.obj.db.button_exposed = True
            self.caller.msg("Holding aside the root you think you notice something behind it ...")

class CmdPressButton(Command):
    """
    Presses a button.
    """

    key = "press"
    aliases = ["press button", "button", "push button"]
    # only accessible if the button was found and there is light. This checks
    # the Attribute button_exposed on the Wall object so that
    # you can only push the button when the puzzle is solved. It also
    # checks the is_lit Attribute on the location.
    locks = "cmd:objattr(button_exposed) and objlocattr(is_lit)"

        """Implements the command"""

        if self.caller.db.crumbling_wall_found_exit:
            # we already pushed the button
                "The button folded away when the secret passage opened. You cannot push it again."
            )
            return

        # pushing the button
        string = (
            "You move your fingers over the suspicious depression, then gives it a "
            "decisive push. First nothing happens, then there is a rumble and a hidden "
            "|wpassage|n opens, dust and pebbles rumbling as part of the wall moves aside."
        )
        string = (
            "%s moves their fingers over the suspicious depression, then gives it a "
        )
        self.caller.location.msg_contents(string % self.caller.key, exclude=self.caller)
        if not self.obj.open_wall():
            self.caller.msg("The exit leads nowhere, there's just more stone behind it ...")

class CmdSetCrumblingWall(CmdSet):
    """Group the commands for crumblingWall"""

    key = "crumblingwall_cmdset"

        """called when object is first created."""
        self.add(CmdShiftRoot())
        self.add(CmdPressButton())

class CrumblingWall(TutorialObject, DefaultExit):
    """
    This is a custom Exit.

    The CrumblingWall can be examined in various ways, but only if a
    lit light source is in the room. The traversal itself is blocked
    by a traverse: lock on the exit that only allows passage if a
    certain attribute is set on the trying account.

    Important attribute
     destination - this property must be set to make this a valid exit
                   whenever the button is pushed (this hides it as an exit
                   until it actually is)
    """

        """
        Called when object is recalled from cache.
        """
        self.reset()

        """called when the object is first created."""

        self.aliases.add(["secret passage", "passage", "crack", "opening", "secret"])

        # starting root positions. H1/H2 are the horizontally hanging roots,
        # V1/V2 the vertically hanging ones. Each can have three positions:
        # (-1, 0, 1) where 0 means the middle position. yellow/green are
        # horizontal roots and red/blue vertical, all may have value 0, but n
        # ever any other identical value.
        self.db.root_pos = {"yellow": 0, "green": 0, "red": 0, "blue": 0}

        # flags controlling the puzzle victory conditions
        self.db.button_exposed = False
        self.db.exit_open = False

        # this is not even an Exit until it has a proper destination, and we won't assign
        # that until it is actually open. Until then we store the destination here. This
        # should be given a reasonable value at creation!
        self.db.destination = "#2"

        # we lock this Exit so that one can only execute commands on it
        # if its location is lit and only traverse it once the Attribute
        # exit_open is set to True.
        self.locks.add("cmd:locattr(is_lit);traverse:objattr(exit_open)")
        # set cmdset
        self.cmdset.add(CmdSetCrumblingWall, persistent=True)

    def open_wall(self):
        """
        This method is called by the push button command once the puzzle
        is solved. It opens the wall and sets a timer for it to reset
        itself.
        """
        # this will make it into a proper exit (this returns a list)
        eloc = search.search_object(self.db.destination)
        if not eloc:
        else:
            self.destination = eloc[0]
        self.db.exit_open = True
        # start a 45 second timer before closing again. We store the deferred so it can be
        # killed in unittesting.
        self.deferred = delay(45, self.reset)

    def _translate_position(self, root, ipos):
        """Translates the position into words"""
        rootnames = {
            "red": "The |rreddish|n vertical-hanging root ",
            "blue": "The thick vertical root with |bblue|n flowers ",
            "yellow": "The thin horizontal-hanging root with |yyellow|n flowers ",
            "green": "The weedy |ggreen|n horizontal root ",
        }
        vpos = {
            -1: "hangs far to the |wleft|n on the wall.",
            0: "hangs straight down the |wmiddle|n of the wall.",
            1: "hangs far to the |wright|n of the wall.",
        }
        hpos = {
            -1: "covers the |wupper|n part of the wall.",
            0: "passes right over the |wmiddle|n of the wall.",
            1: "nearly touches the floor, near the |wbottom|n of the wall.",
        }

        if root in ("yellow", "green"):
            string = rootnames[root] + hpos[ipos]
        else:
            string = rootnames[root] + vpos[ipos]

        """
        This is called when someone looks at the wall. We need to echo the
        current root positions.
        """
        if self.db.button_exposed:
            # we found the button by moving the roots
            result = [
                "Having moved all the roots aside, you find that the center of the wall, "
                "previously hidden by the vegetation, hid a curious square depression. It was maybe once "
                "concealed and made to look a part of the wall, but with the crumbling of stone around it, "
                "it's now easily identifiable as some sort of button."
            ]
        elif self.db.exit_open:
            # we pressed the button; the exit is open
            result = [
                "With the button pressed, a crack has opened in the root-covered wall, just wide enough "
                "to squeeze through. A cold draft is coming from the hole and you get the feeling the "
                "opening may close again soon."
            ]
        else:
            # puzzle not solved yet.
            result = [
                "The wall is old and covered with roots that here and there have permeated the stone. "
                "The roots (or whatever they are - some of them are covered in small nondescript flowers) "
                "crisscross the wall, making it hard to clearly see its stony surface. Maybe you could "
                "try to |wshift|n or |wmove|n them (like '|wshift red up|n').\n"
            ]
            # display the root positions to help with the puzzle
            for key, pos in self.db.root_pos.items():
                result.append("\n" + self._translate_position(key, pos))
        self.db.desc = "".join(result)

        # call the parent to continue execution (will use the desc we just set)

    def at_post_traverse(self, traverser, source_location):
        """
        This is called after we traversed this exit. Cleans up and resets
        the puzzle.
        """
        del traverser.db.crumbling_wall_found_buttothe
        del traverser.db.crumbling_wall_found_exit

    def at_failed_traverse(self, traverser):
        """This is called if the account fails to pass the Exit."""
        traverser.msg("No matter how you try, you cannot force yourself through %s." % self.key)

        """
        Called by tutorial world runner, or whenever someone successfully
        traversed the Exit.
        """
            "The secret door closes abruptly, roots falling back into place."
        )

        # reset the flags and remove the exit destination

        # Reset the roots with some random starting positions for the roots:
        start_pos = [
            {"yellow": 1, "green": 0, "red": 0, "blue": 0},
            {"yellow": 0, "green": 0, "red": 0, "blue": 0},
            {"yellow": 0, "green": 1, "red": -1, "blue": 0},
            {"yellow": 0, "green": 0, "red": 0, "blue": 1},
        ]
        self.db.root_pos = random.choice(start_pos)

#
# TutorialWeapon - object type
#
# A weapon is necessary in order to fight in the tutorial
# world. A weapon (which here is assumed to be a bladed
# melee weapon for close combat) has three commands,
# stab, slash and defend. Weapons also have a property "magic"
# to determine if they are usable against certain enemies.
#
# Since Characters don't have special skills in the tutorial,
# we let the weapon itself determine how easy/hard it is
# to hit with it, and how much damage it can do.
#

    """
    Attack the enemy. Commands:

      stab <enemy>
      slash <enemy>
      parry

    stab - (thrust) makes a lot of damage but is harder to hit with.
    slash - is easier to land, but does not make as much damage.
    parry - forgoes your attack but will make you harder to hit on next
            enemy attack.

    """

    # this is an example of implementing many commands as a single
    # command class, using the given command alias to separate between them.

    aliases = [
        "hit",
        "kill",
        "fight",
        "thrust",
        "pierce",
        "stab",
        "slash",
        "chop",
        "bash",
        "parry",
        "defend",
    ]

        """Implements the stab"""

        cmdstring = self.cmdstring

        if cmdstring in ("attack", "fight"):
            string = "How do you want to fight? Choose one of 'stab', 'slash' or 'defend'."
            return

        # parry mode
        if cmdstring in ("parry", "defend"):
            string = (
                "You raise your weapon in a defensive pose, ready to block the next enemy attack."
            )
            self.caller.db.combat_parry_mode = True
                "%s takes a defensive stance" % self.caller, exclude=[self.caller]
            )
            return

            self.caller.msg("Who do you attack?")
            return
        target = self.caller.search(self.args.strip())
            return

        if cmdstring in ("thrust", "pierce", "stab"):
            hit = float(self.obj.db.hit) * 0.7  # modified due to stab
            damage = self.obj.db.damage * 2  # modified due to stab
            string = "You stab with %s. " % self.obj.key
            tstring = "%s stabs at you with %s. " % (self.caller.key, self.obj.key)
            ostring = "%s stabs at %s with %s. " % (self.caller.key, target.key, self.obj.key)
            self.caller.db.combat_parry_mode = False
        elif cmdstring in ("slash", "chop", "bash"):
            hit = float(self.obj.db.hit)  # un modified due to slash
            damage = self.obj.db.damage  # un modified due to slash
            string = "You slash with %s. " % self.obj.key
            tstring = "%s slash at you with %s. " % (self.caller.key, self.obj.key)
            ostring = "%s slash at %s with %s. " % (self.caller.key, target.key, self.obj.key)
        else:
                "You fumble with your weapon, unsure of whether to stab, slash or parry ..."
            )
                "%s fumbles with their weapon." % self.caller, exclude=self.caller
            )
            return

        if target.db.combat_parry_mode:
            # target is defensive; even harder to hit!
            target.msg("|GYou defend, trying to avoid the attack.|n")
            hit *= 0.5

        if random.random() <= hit:
            self.caller.msg(string + "|gIt's a hit!|n")
            target.msg(tstring + "|rIt's a hit!|n")
                ostring + "It's a hit!", exclude=[target, self.caller]
            )

            # call enemy hook
            if hasattr(target, "at_hit"):
                # should return True if target is defeated, False otherwise.
                target.at_hit(self.obj, self.caller, damage)
                return
            elif target.db.health:
                target.db.health -= damage
            else:
                # sorry, impossible to fight this enemy ...
                self.caller.msg("The enemy seems unaffected.")
                return
        else:
            self.caller.msg(string + "|rYou miss.|n")
            target.msg(tstring + "|gThey miss you.|n")
            self.caller.location.msg_contents(ostring + "They miss.", exclude=[target, self.caller])

class CmdSetWeapon(CmdSet):
    """Holds the attack command."""

        """called at first object creation."""

class TutorialWeapon(TutorialObject):
    """
    This defines a bladed weapon.

    Important attributes (set at creation):
      hit - chance to hit (0-1)
      parry - chance to parry (0-1)
      damage - base damage given (modified by hit success and
               type of attack) (0-10)

    """

        """Called at first creation of the object"""
        self.db.hit = 0.4  # hit chance
        self.db.parry = 0.8  # parry chance
        self.db.damage = 1.0
        self.db.magic = False
        self.cmdset.add_default(CmdSetWeapon, persistent=True)

        """
        When reset, the weapon is simply deleted, unless it has a place
        to return to.
        """
        if self.location.has_account and self.home == self.location:
            self.location.msg_contents(
                "%s suddenly and magically fades into nothingness, as if it was never there ..."
                % self.key
            )
        else:
            self.location = self.home

#
# Weapon rack - spawns weapons
#
# This is a spawner mechanism that creates custom weapons from a
# spawner prototype dictionary. Note that we only create a single typeclass
# (Weapon) yet customize all these different weapons using the spawner.
# The spawner dictionaries could easily sit in separate modules and be
# used to create unique and interesting variations of typeclassed
# objects.
#

WEAPON_PROTOTYPES = {
    "weapon": {
        "typeclass": "evennia.contrib.tutorials.tutorial_world.objects.TutorialWeapon",
        "key": "Weapon",
        "hit": 0.2,
        "parry": 0.2,
        "damage": 1.0,
        "magic": False,
        "desc": "A generic blade.",
    },
    "knife": {
        "prototype_parent": "weapon",
        "aliases": "sword",
        "key": "Kitchen knife",
        "desc": "A rusty kitchen knife. Better than nothing.",
        "damage": 3,
    },
    "dagger": {
        "prototype_parent": "knife",
        "key": "Rusty dagger",
        "aliases": ["knife", "dagger"],
        "desc": "A double-edged dagger with a nicked edge and a wooden handle.",
        "hit": 0.25,
    },
    "sword": {
        "key": "Rusty sword",
        "aliases": ["sword"],
        "desc": "A rusty shortsword. It has a leather-wrapped handle covered i food grease.",
        "hit": 0.3,
        "damage": 5,
        "parry": 0.5,
    },
    "club": {
        "key": "Club",
        "desc": "A heavy wooden club, little more than a heavy branch.",
        "hit": 0.4,
        "damage": 6,
    },
    "axe": {
        "key": "Axe",
        "desc": "A woodcutter's axe with a keen edge.",
    },
    "ornate longsword": {
        "prototype_parent": "sword",
        "key": "Ornate longsword",
        "desc": "A fine longsword with some swirling patterns on the handle.",
        "hit": 0.5,
        "magic": True,
    },
    "warhammer": {
        "prototype_parent": "club",
        "key": "Silver Warhammer",
        "aliases": ["hammer", "warhammer", "war"],
        "desc": "A heavy war hammer with silver ornaments. This huge weapon causes massive damage - if you can hit.",
        "damage": 8,
    },
    "rune axe": {
        "prototype_parent": "axe",
        "key": "Runeaxe",
        "aliases": ["axe"],
    },
    "thruning": {
        "prototype_parent": "ornate longsword",
        "key": "Broadsword named Thruning",
        "desc": "This heavy bladed weapon is marked with the name 'Thruning'. It is very powerful in skilled hands.",
        "hit": 0.6,
        "parry": 0.6,
        "damage": 7,
    },
    "slayer waraxe": {
        "prototype_parent": "rune axe",
        "key": "Slayer waraxe",
        "aliases": ["waraxe", "war", "slayer"],
        "desc": "A huge double-bladed axe marked with the runes for 'Slayer'."
        " It has more runic inscriptions on its head, which you cannot decipher.",
        "hit": 0.7,
    },
    "ghostblade": {
        "key": "The Ghostblade",
        "aliases": ["blade", "ghost"],
        "desc": "This massive sword is large as you are tall, yet seems to weigh almost nothing."
        " It's almost like it's not really there.",
        "hit": 0.9,
        "parry": 0.8,
        "damage": 10,
    },
    "hawkblade": {
        "prototype_parent": "ghostblade",
        "key": "The Hawkblade",
        "aliases": ["hawk", "blade"],
        "desc": "The weapon of a long-dead heroine and a more civilized age,"
        " the hawk-shaped hilt of this blade almost has a life of its own.",
        "hit": 0.85,
        "parry": 0.7,
        "damage": 11,
    },
}

class CmdGetWeapon(Command):
    """
    Usage:
      get weapon

    This will try to obtain a weapon from the container.
    """

    key = "get weapon"
    aliases = "get weapon"

        """
        Get a weapon from the container. It will
        itself handle all messages.
        """
        self.obj.produce_weapon(self.caller)

class CmdSetWeaponRack(CmdSet):
    """
    The cmdset for the rack.
    """

    key = "weaponrack_cmdset"

        """Called at first creation of cmdset"""
        self.add(CmdGetWeapon())

class TutorialWeaponRack(TutorialObject):
    """
    This object represents a weapon store. When people use the
    "get weapon" command on this rack, it will produce one
    random weapon from among those registered to exist
    on it. This will also set a property on the character
    to make sure they can't get more than one at a time.

    Attributes to set on this object:
        available_weapons: list of prototype-keys from
            WEAPON_PROTOTYPES, the weapons available in this rack.
        no_more_weapons_msg - error message to return to accounts
            who already got one weapon from the rack and tries to
            grab another one.

    """

        """
        called at creation
        """
        self.cmdset.add_default(CmdSetWeaponRack, persistent=True)
        self.db.rack_id = "weaponrack_1"
        # these are prototype names from the prototype
        # dictionary above.
        self.db.get_weapon_msg = dedent(
            """
            You find |c%s|n. While carrying this weapon, these actions are available:
              |wstab/thrust/pierce <target>|n - poke at the enemy. More damage but harder to hit.
              |wslash/chop/bash <target>|n - swipe at the enemy. Less damage but easier to hit.
              |wdefend/parry|n - protect yourself and make yourself harder to hit.)
            """
        ).strip()

        self.db.no_more_weapons_msg = "you find nothing else of use."
        self.db.available_weapons = ["knife", "dagger", "sword", "club"]

    def produce_weapon(self, caller):
        """
        This will produce a new weapon from the rack,
        assuming the caller hasn't already gotten one. When
        doing so, the caller will get Tagged with the id
        of this rack, to make sure they cannot keep
        pulling weapons from it indefinitely.
        """
        rack_id = self.db.rack_id
        if caller.tags.get(rack_id, category="tutorial_world"):
            caller.msg(self.db.no_more_weapons_msg)
        else:
            prototype = random.choice(self.db.available_weapons)
            # use the spawner to create a new Weapon from the
            # spawner dictionary, tag the caller
            wpn = spawn(WEAPON_PROTOTYPES[prototype], prototype_parents=WEAPON_PROTOTYPES)[0]
            caller.tags.add(rack_id, category="tutorial_world")
            wpn.location = caller
            caller.msg(self.db.get_weapon_msg % wpn.key)

"""

Room Typeclasses for the TutorialWorld.

This defines special types of Rooms available in the tutorial. To keep
everything in one place we define them together with the custom
commands needed to control them. Those commands could also have been
in a separate module (e.g. if they could have been re-used elsewhere.)

"""

# the system error-handling module is defined in the settings. We load the
# given setting here using utils.object_from_module. This way we can use
# it regardless of if we change settings later.

    TICKER_HANDLER,
    CmdSet,
    Command,
    utils,
)

from .objects import LightSource

_SEARCH_AT_RESULT = utils.object_from_module(settings.SEARCH_AT_RESULT)

#
# Tutorial room - parent room class
#
# This room is the parent of all rooms in the tutorial.
# It defines a tutorial command on itself (available to
# all those who are in a tutorial room).
#

#
# Special command available in all tutorial rooms

class CmdTutorial(Command):
    """
    Get help during the tutorial

    Usage:
      tutorial [obj]

    This command allows you to get behind-the-scenes info
    about an object or the current location.

    """

    key = "tutorial"
    aliases = ["tut"]

        """
        All we do is to scan the current location for an Attribute
        called `tutorial_info` and display that.
        """

            target = self.obj  # this is the room the command is defined on
        else:
            target = caller.search(self.args.strip())
                return
        helptext = target.db.tutorial_info or ""

        if helptext:
            helptext = f" |G{helptext}|n"
        else:
            helptext = " |RSorry, there is no tutorial help available here.|n"
        helptext += "\n\n (Write 'give up' if you want to abandon your quest.)"
        caller.msg(helptext)

# for the @detail command we inherit from MuxCommand, since
# we want to make use of MuxCommand's pre-parsing of '=' in the
# argument.
class CmdTutorialSetDetail(default_cmds.MuxCommand):
    """

    Usage:
        @detail <key> = <description>

    Example:

    This sets a "detail" on the object this command is defined on
    (TutorialRoom for this tutorial). This detail can be accessed with
    the TutorialRoomLook command sitting on TutorialRoom objects (details
    are set as a simple dictionary on the room). This is a Builder command.

    We custom parse the key for the ;-separator in order to create
    multiple aliases to the detail all at once.
    """

        """
        All this does is to check if the object has
        the set_detail method and uses it.
        """
            self.caller.msg("Usage: @detail key = description")
            return
        if not hasattr(self.obj, "set_detail"):
            self.caller.msg("Details cannot be set on %s." % self.obj)
            return
            self.obj.set_detail(key, self.rhs)
        self.caller.msg("Detail set: '%s': '%s'" % (self.lhs, self.rhs))

class CmdTutorialLook(default_cmds.CmdLook):
    """
    looks at the room and on details

    Usage:
        look <obj>
        look <room detail>
        look *<account>

    Observes your location, details at your location or objects
    in your vicinity.

    Tutorial: This is a child of the default Look command, that also
    allows us to look at "details" in the room.  These details are
    things to examine and offers some extra description without
    actually having to be actual database objects. It uses the
    return_detail() hook on TutorialRooms for this.
    """

    # we don't need to specify key/locks etc, this is already
    # set by the parent.

        """
        Handle the looking. This is a copy of the default look
        code except for adding in the details.
        """
        if args:
            # we use quiet=True to turn off automatic error reporting.
            # This tells search that we want to handle error messages
            # ourself. This also means the search function will always
            # return a list (with 0, 1 or more elements) rather than
            # result/None.
            looking_at_obj = caller.search(
                args,
                # note: excludes room/room aliases
                candidates=caller.location.contents + caller.contents,
                use_nicks=True,
                quiet=True,
            )
            if len(looking_at_obj) != 1:
                # no target found or more than one target found (multimatch)
                # look for a detail that may match
                detail = self.obj.return_detail(args)
                if detail:
                    self.caller.msg(detail)
                    return
                else:
                    # no detail found, delegate our result to the normal
                    # error message handler.
                    _SEARCH_AT_RESULT(looking_at_obj, caller, args)
                    return
            else:
                # we found a match, extract it from the list and carry on
                # normally with the look handling.
                looking_at_obj = looking_at_obj[0]

        else:
            looking_at_obj = caller.location
            if not looking_at_obj:
                return

        if not hasattr(looking_at_obj, "return_appearance"):
            # this is likely due to us having an account instead
            looking_at_obj = looking_at_obj.character
        if not looking_at_obj.access(caller, "view"):
            caller.msg("Could not find '%s'." % args)
            return
        # get object's appearance
        caller.msg(looking_at_obj.return_appearance(caller))
        # the object's at_desc() method.
        looking_at_obj.at_desc(looker=caller)
        return

class CmdTutorialGiveUp(default_cmds.MuxCommand):
    """
    Give up the tutorial-world quest and return to Limbo, the start room of the
    server.

    """

    aliases = ["abort"]

        outro_room = OutroRoom.objects.all()
        if outro_room:
            outro_room = outro_room[0]
        else:
                "That didn't work (seems like a bug). "
                "Try to use the |wteleport|n command instead."
            )
            return

        self.caller.move_to(outro_room, move_type="teleport")

class TutorialRoomCmdSet(CmdSet):
    """
    Implements the simple tutorial cmdset. This will overload the look
    command in the default CharacterCmdSet since it has a higher
    priority (ChracterCmdSet has prio 0)
    """

    key = "tutorial_cmdset"

        """add the tutorial-room commands"""
        self.add(CmdTutorial())
        self.add(CmdTutorialSetDetail())
        self.add(CmdTutorialLook())
        self.add(CmdTutorialGiveUp())

class TutorialRoom(DefaultRoom):
    """
    This is the base room type for all rooms in the tutorial world.
    It defines a cmdset on itself for reading tutorial info about the location.
    """

        """Called when room is first created"""
            "This is a tutorial room. It allows you to use the 'tutorial' command."
        )
        self.cmdset.add_default(TutorialRoomCmdSet)

    def at_object_receive(self, new_arrival, source_location, move_type="move", **kwargs):
        """
        When an object enter a tutorial room we tell other objects in
        the room about it by trying to call a hook on them. The Mob object
        uses this to cheaply get notified of enemies without having
        to constantly scan for them.

        Args:
            new_arrival (Object): the object that just entered this room.
            source_location (Object): the previous location of new_arrival.

        """
        if new_arrival.has_account and not new_arrival.is_superuser:
            # this is a character
            for obj in self.contents_get(exclude=new_arrival):
                if hasattr(obj, "at_new_arrival"):
                    obj.at_new_arrival(new_arrival)

    def return_detail(self, detailkey):
        """
        This looks for an Attribute "obj_details" and possibly
        returns the value of it.

        Args:
            detailkey (str): The detail being looked at. This is
                case-insensitive.

        """
        details = self.db.details
        if details:
            return details.get(detailkey.lower(), None)

    def set_detail(self, detailkey, description):
        """

        Args:
                at the given detailkey.

        """
        if self.db.details:
            self.db.details[detailkey.lower()] = description
        else:
            self.db.details = {detailkey.lower(): description}

class TutorialStartExit(DefaultExit):
    """
    This is like a normal exit except it makes the `intro` command available
    on itself. We put it on the exit in order to provide this command to the
    Limbo room without modifying Limbo itself - deleting the tutorial exit
    will also  clean up the intro command.

    """

        self.cmdset.add(CmdSetEvenniaIntro, persistent=True)

#
# Weather room - room with a ticker
#

# These are rainy weather strings
WEATHER_STRINGS = (
    "The rain coming down from the iron-grey sky intensifies.",
    "A gust of wind throws the rain right in your face. Despite your cloak you shiver.",
    "The rainfall eases a bit and the sky momentarily brightens.",
    "For a moment it looks like the rain is slowing, then it begins anew with renewed force.",
    "The rain pummels you with large, heavy drops. You hear the rumble of thunder in the distance.",
    "The wind is picking up, howling around you, throwing water droplets in your face. It's cold.",
    "Bright fingers of lightning flash over the sky, moments later followed by a deafening rumble.",
    "It rains so hard you can hardly see your hand in front of you. You'll soon be drenched to the bone.",
    "Lightning strikes in several thundering bolts, striking the trees in the forest to your west.",
    "You hear the distant howl of what sounds like some sort of dog or wolf.",
    "Large clouds rush across the sky, throwing their load of rain over the world.",
)

class WeatherRoom(TutorialRoom):
    """
    This should probably better be called a rainy room...

    This sets up an outdoor room typeclass. At irregular intervals,
    the effects of weather will show in the room. Outdoor rooms should
    inherit from this.

    """

        """
        Called when object is first created.
        We set up a ticker to update this room regularly.

        Note that we could in principle also use a Script to manage
        the ticking of the room; the TickerHandler works fine for
        simple things like this though.
        """
        # subscribe ourselves to a ticker to repeatedly call the hook
        # "update_weather" on this object. The interval is randomized
        # so as to not have all weather rooms update at the same time.
        self.db.interval = random.randint(50, 70)
        TICKER_HANDLER.add(
            interval=self.db.interval, callback=self.update_weather, idstring="tutorial"
        )
        # this is parsed by the 'tutorial' command on TutorialRooms.
        self.db.tutorial_info = "This room has a Script running that has it echo a weather-related message at irregular intervals."

    def update_weather(self, *args, **kwargs):
        """
        Called by the tickerhandler at regular intervals. Even so, we
        only update 20% of the time, picking a random weather message
        when we do. The tickerhandler requires that this hook accepts
        any arguments and keyword arguments (hence the *args, **kwargs
        even though we don't actually use them in this example)
        """
        if random.random() < 0.2:
            # only update 20 % of the time
            self.msg_contents("|w%s|n" % random.choice(WEATHER_STRINGS))

SUPERUSER_WARNING = (
    "\nWARNING: You are playing as a superuser ({name}). Use the {quell} command to\n"
    "play without superuser privileges (many functions and puzzles ignore the \n"
    "presence of a superuser, making this mode useful for exploring things behind \n"
    "the scenes later).\n"
)

#
# Intro Room - unique room
#
# This room marks the start of the tutorial. It sets up properties on
# the player char that is needed for the tutorial.
#

class CmdEvenniaIntro(Command):
    """
    Start the Evennia intro wizard.

    Usage:
        intro

    """

    key = "intro"

        from .intro_menu import init_menu

        # quell also superusers
            self.caller.msg("Auto-quelling permissions while in intro ...")
            self.caller.account.execute_cmd("quell")
        init_menu(self.caller)

class CmdSetEvenniaIntro(CmdSet):
    key = "Evennia Intro StartSet"

        self.add(CmdEvenniaIntro())

class IntroRoom(TutorialRoom):
    """
    Intro room

    properties to customize:
     char_health - integer > 0 (default 20)
    """

        """
        Called when the room is first created.
        """
            "The first room of the tutorial. "
            "This assigns the health Attribute to "
            "the account."
        )

    def at_object_receive(self, character, source_location, move_type="move", **kwargs):
        """
        Assign properties on characters
        """

        # setup character for the tutorial
        health = self.db.char_health or 20

        if character.has_account:
            character.db.health = health
            character.db.health_max = health

        if character.is_superuser:
            string = "-" * 78 + SUPERUSER_WARNING + "-" * 78
            character.msg("|r%s|n" % string.format(name=character.key, quell="|wquell|r"))
        else:
            # quell user
            if character.account:
                character.account.execute_cmd("quell")
                character.msg("(Auto-quelling while in tutorial-world)")

#
# Bridge - unique room
#
# Defines a special west-eastward "bridge"-room, a large room that takes
# several steps to cross. It is complete with custom commands and a
# chance of falling off the bridge. This room has no regular exits,
# instead the exitings are handled by custom commands set on the account
# upon first entering the room.
#
# Since one can enter the bridge room from both ends, it is
# divided into five steps:
#       westroom <- 0 1 2 3 4 -> eastroom
#

class CmdEast(Command):
    """
    Go eastwards across the bridge.

    Tutorial info:
        This command relies on the caller having two Attributes
        (assigned by the room when entering):
            - east_exit: a unique name or dbref to the room to go to
              when exiting east.
            - west_exit: a unique name or dbref to the room to go to
              when exiting west.
       The room must also have the following Attributes
           - tutorial_bridge_posistion: the current position on
             on the bridge, 0 - 4.

    """

    key = "east"
    aliases = ["e"]

        """move one step eastwards"""

        bridge_step = min(5, caller.db.tutorial_bridge_position + 1)

        if bridge_step > 4:
            # we have reached the far east end of the bridge.
            # Move to the east room.
            eexit = search_object(self.obj.db.east_exit)
            if eexit:
                caller.move_to(eexit[0], move_type="traverse")
            else:
                caller.msg("No east exit was found for this room. Contact an admin.")
            return
        caller.db.tutorial_bridge_position = bridge_step
        # since we are really in one room, we have to notify others
        # in the room when we move.
            "%s steps eastwards across the bridge." % caller.name, exclude=caller
        )
        caller.execute_cmd("look")

# go back across the bridge
class CmdWest(Command):
    """
    Go westwards across the bridge.

       This command relies on the caller having two Attributes
       (assigned by the room when entering):
           - east_exit: a unique name or dbref to the room to go to
             when exiting east.
           - west_exit: a unique name or dbref to the room to go to
             when exiting west.
       The room must also have the following property:

    """

    key = "west"
    aliases = ["w"]

        """move one step westwards"""

        bridge_step = max(-1, caller.db.tutorial_bridge_position - 1)

        if bridge_step < 0:
            # we have reached the far west end of the bridge.
            # Move to the west room.
            wexit = search_object(self.obj.db.west_exit)
            if wexit:
                caller.move_to(wexit[0], move_type="traverse")
            else:
                caller.msg("No west exit was found for this room. Contact an admin.")
            return
            "%s steps westwards across the bridge." % caller.name, exclude=caller
        )

BRIDGE_POS_MESSAGES = (
    "You are standing |wvery close to the the bridge's western foundation|n."
    " If you go west you will be back on solid ground ...",
    "The bridge slopes precariously where it extends eastwards"
    " towards the lowest point - the center point of the hang bridge.",
    "You are |whalfways|n out on the unstable bridge.",
    "The bridge slopes precariously where it extends westwards"
    "You are standing |wvery close to the bridge's eastern foundation|n."
    " If you go east you will be back on solid ground ...",
)
BRIDGE_MOODS = (
    "The bridge sways in the wind.",
    "The hanging bridge creaks dangerously.",
    "You clasp the ropes firmly as the bridge sways and creaks under you.",
    "From the castle you hear a distant howling sound, like that of a large dog or other beast.",
    "The bridge creaks under your feet. Those planks does not seem very sturdy.",
    "Far below you the ocean roars and throws its waves against the cliff,"
    " as if trying its best to reach you.",
    "Parts of the bridge come loose behind you, falling into the chasm far below!",
    "A gust of wind causes the bridge to sway precariously.",
    "Under your feet a plank comes loose, tumbling down. For a moment you dangle over the abyss ...",
    "The section of rope you hold onto crumble in your hands,"
    " parts of it breaking apart. You sway trying to regain balance.",
)

FALL_MESSAGE = (
    "Suddenly the plank you stand on gives way under your feet! You fall!"
    "\nYou try to grab hold of an adjoining plank, but all you manage to do is to "
    "divert your fall westwards, towards the cliff face. This is going to hurt ... "
    "\n ... The world goes dark ...\n\n"
)

class CmdLookBridge(Command):
    """
    looks around at the bridge.

        This command assumes that the room has an Attribute
        "fall_exit", a unique name or dbref to the place they end upp
        if they fall off the bridge.
    """

    aliases = ["l"]

        """Looking around, including a chance to fall."""
        bridge_position = self.caller.db.tutorial_bridge_position
        # this command is defined on the room, so we get it through self.obj
        location = self.obj
        # randomize the look-echo
        message = "|c%s|n\n%s\n%s" % (
            location.key,
            BRIDGE_POS_MESSAGES[bridge_position],
            random.choice(BRIDGE_MOODS),
        )

        chars = [obj for obj in self.obj.contents_get(exclude=caller) if obj.has_account]
        if chars:
            # we create the You see: message manually here
            message += "\n You see: %s" % ", ".join("|c%s|n" % char.key for char in chars)
        self.caller.msg(message)

        # there is a chance that we fall if we are on the western or central
        # part of the bridge.
        if bridge_position < 3 and random.random() < 0.05 and not self.caller.is_superuser:
            # we fall 5% of time.
            fall_exit = search_object(self.obj.db.fall_exit)
            if fall_exit:
                self.caller.msg("|r%s|n" % FALL_MESSAGE)
                self.caller.move_to(fall_exit[0], quiet=True, move_type="fall")
                # inform others on the bridge
                self.obj.msg_contents(
                    "A plank gives way under %s's feet and "
                    "they fall from the bridge!" % self.caller.key
                )

# custom help command
class CmdBridgeHelp(Command):
    """
    Overwritten help command while on the bridge.
    """

    help_category = "Tutorial world"

        string = (
            "You are trying hard not to fall off the bridge ..."
            "\n\nWhat you can do is trying to cross the bridge |weast|n"
            " or try to get back to the mainland |wwest|n)."
        )

class BridgeCmdSet(CmdSet):
    """This groups the bridge commands. We will store it on the room."""

    key = "Bridge commands"
    priority = 2  # this gives it precedence over the normal look/help commands.

        """Called at first cmdset creation"""
        self.add(CmdEast())
        self.add(CmdWest())
        self.add(CmdLookBridge())
        self.add(CmdBridgeHelp())

BRIDGE_WEATHER = (
    "The rain intensifies, making the planks of the bridge even more slippery.",
    "A gust of wind throws the rain right in your face.",
    "The bridge shakes under the thunder of a closeby thunder strike.",
    "The rain pummels you with large, heavy drops. You hear the distinct howl of a large hound in the distance.",
    "The wind is picking up, howling around you and causing the bridge to sway from side to side.",
    "Some sort of large bird sweeps by overhead, giving off an eery screech. Soon it has disappeared in the gloom.",
    "The bridge sways from side to side in the wind.",
    "Below you a particularly large wave crashes into the rocks.",
    "From the ruin you hear a distant, otherwordly howl. Or maybe it was just the wind.",
)

class BridgeRoom(WeatherRoom):
    """
    The bridge room implements an unsafe bridge. It also enters the player into
    a state where they get new commands so as to try to cross the bridge.

     We want this to result in the account getting a special set of
     commands related to crossing the bridge. The result is that it
     will take several steps to cross it, despite it being represented
     by only a single room.

     We divide the bridge into steps:

        self.db.west_exit     -   -  |  -   -     self.db.east_exit
                              0   1  2  3   4

     The position is handled by a variable stored on the character
     when entering and giving special move commands will
     increase/decrease the counter until the bridge is crossed.

     We also has self.db.fall_exit, which points to a gathering
     location to end up if we happen to fall off the bridge (used by
     the CmdLookBridge command).

    """

        """Setups the room"""
        # this will start the weather room's ticker and tell
        # it to call update_weather regularly.
        # this identifies the exits from the room (should be the command
        # needed to leave through that exit). These are defaults, but you
        # could of course also change them after the room has been created.
        self.db.west_exit = "cliff"
        self.db.east_exit = "gate"
        self.db.fall_exit = "cliffledge"
        # add the cmdset on the room.
        self.cmdset.add(BridgeCmdSet, persistent=True)
        # since the default Character's at_look() will access the room's
        # return_description (this skips the cmdset) when
        # first entering it, we need to explicitly turn off the room
        # as a normal view target - once inside, our own look will
        # handle all return messages.
        self.locks.add("view:false()")

        """
        This is called at irregular intervals and makes the passage
        over the bridge a little more interesting.
        """
        if random.random() < 80:
            # send a message most of the time
            self.msg_contents("|w%s|n" % random.choice(BRIDGE_WEATHER))

        """
        This hook is called by the engine whenever the player is moved
        into this room.
        """
            # we only run this if the entered object is indeed a player object.
            # check so our east/west exits are correctly defined.
            wexit = search_object(self.db.west_exit)
            eexit = search_object(self.db.east_exit)
            fexit = search_object(self.db.fall_exit)
            if not (wexit and eexit and fexit):
                    "The bridge's exits are not properly configured. "
                    "Contact an admin. Forcing west-end placement."
                )
                character.db.tutorial_bridge_position = 0
                return
            if source_location == eexit[0]:
                # we assume we enter from the same room we will exit to
                character.db.tutorial_bridge_position = 4
            else:
                # if not from the east, then from the west!
            character.execute_cmd("look")

    def at_object_leave(self, character, target_location, move_type="move", **kwargs):
        """
        This is triggered when the player leaves the bridge room.
        """
            # clean up the position attribute
            del character.db.tutorial_bridge_position

# -------------------------------------------------------------------------------
#
# Dark Room - a room with states
#
# This room limits the movemenets of its denizens unless they carry an active
# LightSource object (LightSource is defined in
#                     tutorialworld.objects.LightSource)
#

DARK_MESSAGES = (
    "It is pitch black. You are likely to be eaten by a grue.",
    "It's pitch black. You fumble around but cannot find anything.",
    "You don't see a thing. You feel around, managing to bump your fingers hard against something. Ouch!",
    "You don't see a thing! Blindly grasping the air around you, you find nothing.",
    "It's totally dark here. You almost stumble over some un-evenness in the ground.",
    "You are completely blind. For a moment you think you hear someone breathing nearby ... "
    "\n ... surely you must be mistaken.",
    "Blind, you think you find some sort of object on the ground, but it turns out to be just a stone.",
    "Blind, you bump into a wall. The wall seems to be covered with some sort of vegetation,"
    " but its too damp to burn.",
    "You can't see anything, but the air is damp. It feels like you are far underground.",
)

ALREADY_LIGHTSOURCE = (
    "You don't want to stumble around in blindness anymore. You already "
    "found what you need. Let's get light already!"
)

FOUND_LIGHTSOURCE = (
    "Your fingers bump against a splinter of wood in a corner."
    " It smells of resin and seems dry enough to burn! "
    "You pick it up, holding it firmly. Now you just need to"
    " |wlight|n it using the flint and steel you carry with you."
)

class CmdLookDark(Command):
    """
    Look around in darkness

    Usage:
      look

    Look around in the darkness, trying
    to find something.
    """

    aliases = ["l", "feel", "search", "feel around", "fiddle"]

        """

        This works both as a look and a search command; there is a
        random chance of eventually finding a light source.
        """

        # count how many searches we've done
        nr_searches = caller.ndb.dark_searches
        if nr_searches is None:
            nr_searches = 0
            caller.ndb.dark_searches = nr_searches

        if nr_searches < 4 and random.random() < 0.90:
            # we don't find anything
            caller.msg(random.choice(DARK_MESSAGES))
            caller.ndb.dark_searches += 1
        else:
            # we could have found something!
            if any(obj for obj in caller.contents if utils.inherits_from(obj, LightSource)):
                #  we already carry a LightSource object.
                caller.msg(ALREADY_LIGHTSOURCE)
            else:
                # don't have a light source, create a new one.
                create_object(LightSource, key="splinter", location=caller)
                caller.msg(FOUND_LIGHTSOURCE)

class CmdDarkHelp(Command):
    """
    Help command for the dark state.
    """

        """
        Replace the the help command with a not-so-useful help
        """
        string = (
            "Can't help you until you find some light! Try looking/feeling around for something to burn. "
            "You shouldn't give up even if you don't find anything right away."
        )

class CmdDarkNoMatch(Command):
    """
    This is a system command. Commands with special keys are used to
    override special sitations in the game. The CMD_NOMATCH is used
    when the given command is not found in the current command set (it
    replaces Evennia's default behavior or offering command
    suggestions)
    """

            "Until you find some light, there's not much you can do. "
            "Try feeling around, maybe you'll find something helpful!"
        )

class DarkCmdSet(CmdSet):
    """
    Groups the commands of the dark room together.  We also import the
    default say command here so that players can still talk in the
    darkness.

    We give the cmdset the mergetype "Replace" to make sure it
    completely replaces whichever command set it is merged onto
    (usually the default cmdset)
    """

    key = "darkroom_cmdset"

        """populate the cmdset."""
        self.add(CmdLookDark())
        self.add(CmdDarkHelp())
        self.add(CmdDarkNoMatch())
        self.add(default_cmds.CmdQuit())
        self.add(default_cmds.CmdHome())

class DarkRoom(TutorialRoom):
    """
    A dark room. This tries to start the DarkState script on all
    objects entering. The script is responsible for making sure it is
    valid (that is, that there is no light source shining in the room).

    The is_lit Attribute is used to define if the room is currently lit
    or not, so as to properly echo state changes.

    Since this room (in the tutorial) is meant as a sort of catch-all,
    we also make sure to heal characters ending up here, since they
    may have been beaten up by the ghostly apparition at this point.

    """

        """
        """
        self.db.tutorial_info = "This is a room with custom command sets on itself."
        # the room starts dark.
        self.db.is_lit = False
        self.cmdset.add(DarkCmdSet, persistent=True)

        """
        Called when room is first recached (such as after a reload)
        """
        self.check_light_state()

    def _carries_light(self, obj):
        """
        Checks if the given object carries anything that gives light.

        Note that we do NOT look for a specific LightSource typeclass,
        but for the Attribute is_giving_light - this makes it easy to
        later add other types of light-giving items. We also accept
        if there is a light-giving object in the room overall (like if
        a splinter was dropped in the room)
        """
        return (
            obj.is_superuser
            or obj.db.is_giving_light
            or any(o for o in obj.contents if o.db.is_giving_light)
        )

    def _heal(self, character):
        """
        Heal a character.
        """
        health = character.db.health_max or 20
        character.db.health = health

    def check_light_state(self, exclude=None):
        """
        This method checks if there are any light sources in the room.
        If there isn't it makes sure to add the dark cmdset to all
        characters in the room. It is called whenever characters enter
        the room and also by the Light sources when they turn on.

        Args:
            exclude (Object): An object to not include in the light check.
        """
        if any(self._carries_light(obj) for obj in self.contents if obj != exclude):
            self.locks.add("view:all()")
            self.cmdset.remove(DarkCmdSet)
            self.db.is_lit = True
            for char in (obj for obj in self.contents if obj.has_account):
                # this won't do anything if it is already removed
                char.msg("The room is lit up.")
        else:
            # noone is carrying light - darken the room
            self.db.is_lit = False
            self.locks.add("view:false()")
            self.cmdset.add(DarkCmdSet, persistent=True)
                if char.is_superuser:
                    char.msg("You are Superuser, so you are not affected by the dark state.")
                else:
                    # put players in darkness
                    char.msg("The room is completely dark.")

    def at_object_receive(self, obj, source_location, move_type="move", **kwargs):
        """
        Called when an object enters the room.
        """
        if obj.has_account:
            # a puppeted object, that is, a Character
            self._heal(obj)
            # in case the new guy carries light with them
            self.check_light_state()

    def at_object_leave(self, obj, target_location, move_type="move", **kwargs):
        """
        In case people leave with the light, we make sure to clear the
        DarkCmdSet if necessary.  This also works if they are
        teleported away.
        """
        # since this hook is called while the object is still in the room,
        # we exclude it from the light check, to ignore any light sources
        # it may be carrying.
        self.check_light_state(exclude=obj)

#
# Teleport room - puzzles solution
#
# This is a sort of puzzle room that requires a certain
# attribute on the entering character to be the same as
# an attribute of the room. If not, the character will
# be teleported away to a target location. This is used
# by the Obelisk - grave chamber puzzle, where one must
# have looked at the obelisk to get an attribute set on
# oneself, and then pick the grave chamber with the
# matching imagery for this attribute.
#

class TeleportRoom(TutorialRoom):
    """
    Teleporter - puzzle room.

      puzzle_key    - which attr to look for on character
      puzzle_value  - what char.db.puzzle_key must be set to
      success_teleport_to -  where to teleport in case if success
      success_teleport_msg - message to echo while teleporting to success
      failure_teleport_to - where to teleport to in case of failure
      failure_teleport_msg - message to echo while teleporting to failure

    """

        """Called at first creation"""
        # what character.db.puzzle_clue must be set to, to avoid teleportation.
        self.db.puzzle_value = 1
        # target of successful teleportation. Can be a dbref or a
        # unique room name.
        self.db.success_teleport_msg = "You are successful!"
        self.db.success_teleport_to = "treasure room"
        # the target of the failure teleportation.
        self.db.failure_teleport_msg = "You fail!"
        self.db.failure_teleport_to = "dark cell"

        """
        This hook is called by the engine whenever the player is moved into
        this room.
        """
        if not character.has_account:
            # only act on player characters.
            return
        # determine if the puzzle is a success or not
        is_success = str(character.db.puzzle_clue) == str(self.db.puzzle_value)
        teleport_to = self.db.success_teleport_to if is_success else self.db.failure_teleport_to
        # note that this returns a list
        results = search_object(teleport_to)
        if not results or len(results) > 1:
            # we cannot move anywhere since no valid target was found.
            character.msg("no valid teleport target for %s was found." % teleport_to)
            return
            # superusers don't get teleported
            character.msg("Superuser block: You would have been teleported to %s." % results[0])
            return
        # perform the teleport
            character.msg(self.db.success_teleport_msg)
        else:
            character.msg(self.db.failure_teleport_msg)
        # teleport quietly to the new place
        character.move_to(results[0], quiet=True, move_hooks=False, move_type="teleport")
        # we have to call this manually since we turn off move_hooks
        # - this is necessary to make the target dark room aware of an
        # already carried light.
        results[0].at_object_receive(character, self)

#
# Outro room - unique exit room
#
# Cleans up the character from all tutorial-related properties.
#

class OutroRoom(TutorialRoom):
    """
    Outro room.

    Called when exiting the tutorial, cleans the
    character of tutorial-related attributes.

    """

        """
        """
            "The last room of the tutorial. "
            "This cleans up all temporary Attributes "
            "the tutorial may have assigned to the "
            "character."
        )

        """
        Do cleanup.
        """
            del character.db.health_max
            del character.db.health
            del character.db.last_climbed
            del character.db.puzzle_clue
            del character.db.combat_parry_mode
            for obj in character.contents:
                if obj.typeclass_path.startswith("evennia.contrib.tutorials.tutorial_world"):
            character.tags.clear(category="tutorial_world")

    def at_object_leave(self, character, destination, move_type="move", **kwargs):
        if character.account:
            character.account.execute_cmd("unquell")

"""
Test tutorial_world/mob

"""

from twisted.internet.base import DelayedCall
from twisted.trial.unittest import TestCase as TwistedTestCase

from evennia.utils.test_resources import BaseEvenniaTest, mockdeferLater, mockdelay

from . import mob
from . import objects as tutobjects
from . import rooms as tutrooms

class TestTutorialWorldMob(BaseEvenniaTest):
    def test_mob(self):
        mobobj = create_object(mob.Mob, key="mob")
        self.assertEqual(mobobj.db.is_dead, True)
        mobobj.set_alive()
        self.assertEqual(mobobj.db.is_dead, False)
        mobobj.set_dead()
        mobobj._set_ticker(0, "foo", stop=True)
        # TODO should be expanded with further tests of the modes and damage etc.

#  test tutorial_world/objects

DelayedCall.debug = True

class TestTutorialWorldObjects(TwistedTestCase, BaseEvenniaCommandTest):
    def test_tutorialobj(self):
        obj1 = create_object(tutobjects.TutorialObject, key="tutobj")
        obj1.reset()
        self.assertEqual(obj1.location, obj1.home)

    def test_readable(self):
        readable = create_object(tutobjects.TutorialReadable, key="book", location=self.room1)
        readable.db.readable_text = "Text to read"
        self.call(tutobjects.CmdRead(), "book", "You read book:\n  Text to read", obj=readable)

    def test_climbable(self):
        climbable = create_object(tutobjects.TutorialClimbable, key="tree", location=self.room1)
            tutobjects.CmdClimb(),
            "tree",
            "You climb tree. Having looked around, you climb down again.",
            obj=climbable,
        )
            self.char1.tags.get("tutorial_climbed_tree", category="tutorial_world"),
            "tutorial_climbed_tree",
        )

    def test_obelisk(self):
        obelisk = create_object(tutobjects.Obelisk, key="obelisk", location=self.room1)
        self.assertEqual(obelisk.return_appearance(self.char1).startswith("|cobelisk("), True)

    @patch("evennia.contrib.tutorials.tutorial_world.objects.delay", mockdelay)
    @patch("evennia.scripts.taskhandler.deferLater", mockdeferLater)
    def test_lightsource(self):
        light = create_object(tutobjects.LightSource, key="torch", location=self.room1)
            tutobjects.CmdLight(),
            "",
            "A torch on the floor flickers and dies.|You light torch.",
            obj=light,
        )
        self.assertFalse(light.pk)

    def test_crumblingwall(self):
        wall = create_object(tutobjects.CrumblingWall, key="wall", location=self.room1)
        wall.db.destination = self.room2.dbref
        self.assertFalse(wall.db.button_exposed)
        self.assertFalse(wall.db.exit_open)
        wall.db.root_pos = {"yellow": 0, "green": 0, "red": 0, "blue": 0}
            tutobjects.CmdShiftRoot(),
            "blue root right",
            "You shove the root adorned with small blue flowers to the right.",
            obj=wall,
        )
            "red root left",
            "You shift the reddish root to the left.",
            obj=wall,
        )
            "yellow root down",
            "You shove the root adorned with small yellow flowers downwards.",
            obj=wall,
        )
            "green root up",
            "You shift the weedy green root upwards.|Holding aside the root you "
            "think you notice something behind it ...",
            obj=wall,
        )
            tutobjects.CmdPressButton(),
            "",
            "decisive push. First",
            obj=wall,
        )
        # we patch out the delay, so these are closed immediately

    def test_weapon(self):
        weapon = create_object(tutobjects.TutorialWeapon, key="sword", location=self.char1)
            tutobjects.CmdAttack(), "Char", "You stab with sword.", obj=weapon, cmdstring="stab"
        )
            tutobjects.CmdAttack(), "Char", "You slash with sword.", obj=weapon, cmdstring="slash"
        )

    def test_weaponrack(self):
        rack = create_object(tutobjects.TutorialWeaponRack, key="rack", location=self.room1)
        rack.db.available_weapons = ["sword"]
        self.call(tutobjects.CmdGetWeapon(), "", "You find Rusty sword.", obj=rack)

class TestTutorialWorldRooms(BaseEvenniaCommandTest):
    def test_cmdtutorial(self):
        room = create_object(tutrooms.TutorialRoom, key="tutroom")
        self.call(tutrooms.CmdTutorial(), "", "Sorry, there is no tutorial help available here.")
            tutrooms.CmdTutorialSetDetail(),
            "detail;foo;foo2 = A detail",
            "Detail set: 'detail;foo;foo2': 'A detail'",
            obj=room,
        )
        self.call(tutrooms.CmdTutorialLook(), "", "tutroom(", obj=room)
        self.call(tutrooms.CmdTutorialLook(), "detail", "A detail", obj=room)
        self.call(tutrooms.CmdTutorialLook(), "foo", "A detail", obj=room)
        room.delete()

    def test_weatherroom(self):
        room = create_object(tutrooms.WeatherRoom, key="weatherroom")
        room.update_weather()
        tutrooms.TICKER_HANDLER.remove(
            interval=room.db.interval, callback=room.update_weather, idstring="tutorial"
        )

    def test_introroom(self):
        room = create_object(tutrooms.IntroRoom, key="introroom")
        room.at_object_receive(self.char1, self.room1)

    def test_bridgeroom(self):
        room = create_object(tutrooms.BridgeRoom, key="bridgeroom")
        self.char1.move_to(room, move_type="teleport")
            tutrooms.CmdBridgeHelp(),
            "",
            "You are trying hard not to fall off the bridge ...",
            obj=room,
        )
            tutrooms.CmdLookBridge(),
            "",
            "bridgeroom\nYou are standing very close to the the bridge's western foundation.",
            obj=room,
        )
        room.at_object_leave(self.char1, self.room1)
        )

    def test_darkroom(self):
        room = create_object(tutrooms.DarkRoom, key="darkroom")
        self.call(tutrooms.CmdDarkHelp(), "", "Can't help you until")

    def test_teleportroom(self):
        create_object(tutrooms.TeleportRoom, key="teleportroom")

    def test_outroroom(self):
        create_object(tutrooms.OutroRoom, key="outroroom")

"""
Auditable Server Sessions - Example Outputs
Example methods demonstrating output destinations for logs generated by
audited server sessions.

This is designed to be a single source of events for developers to customize
and add any additional enhancements before events are written out-- i.e. if you
want to keep a running list of what IPs a user logs in from on account/character
objects, or if you want to perform geoip or ASN lookups on IPs before committing,
or tag certain events with the results of a reputational lookup, this should be
the easiest place to do it. Write a method and invoke it via
`settings.AUDIT_CALLBACK` to have log data objects passed to it.

Evennia contribution - Johnny 2017
"""
import syslog

from evennia.utils.logger import log_file

def to_file(data):
    """
    Writes dictionaries of data generated by an AuditedServerSession to files
    in JSON format, bucketed by date.

    Uses Evennia's native logger and writes to the default
    log directory (~/yourgame/server/logs/ or settings.LOG_DIR)

    Args:
        data (dict): Parsed session transmission data.

    """
    # Bucket logs by day and remove objects before serialization
    bucket = data.pop("objects")["time"].strftime("%Y-%m-%d")

    # Write it
    log_file(json.dumps(data), filename="audit_%s.log" % bucket)

def to_syslog(data):
    """
    Writes dictionaries of data generated by an AuditedServerSession to syslog.

    Takes advantage of your system's native logger and writes to wherever
    you have it configured, which is independent of Evennia.
    Linux systems tend to write to /var/log/syslog.

    If you're running rsyslog, you can configure it to dump and/or forward logs
    to disk and/or an external data warehouse (recommended-- if your server is
    compromised or taken down, losing your logs along with it is no help!).

    Args:

    """
    # Remove objects before serialization
    data.pop("objects")

    # Write it out
    syslog.syslog(json.dumps(data))

"""
Auditable Server Sessions:
Extension of the stock ServerSession that yields objects representing
user inputs and system outputs.

"""
import os
import re
import socket

from django.conf import settings as ev_settings

from evennia.server.serversession import ServerSession
from evennia.utils import get_evennia_version, logger, mod_import, utils

# Attributes governing auditing of commands and where to send log objects
AUDIT_CALLBACK = getattr(
    ev_settings, "AUDIT_CALLBACK", "evennia.contrib.utils.auditing.outputs.to_file"
)
AUDIT_IN = getattr(ev_settings, "AUDIT_IN", False)
AUDIT_OUT = getattr(ev_settings, "AUDIT_OUT", False)
AUDIT_ALLOW_SPARSE = getattr(ev_settings, "AUDIT_ALLOW_SPARSE", False)
AUDIT_MASKS = [
    {"connect": r"^[@\s]*[connect]{5,8}\s+(\".+?\"|[^\s]+)\s+(?P<secret>.+)"},
    {"connect": r"^[@\s]*[connect]{5,8}\s+(?P<secret>[\w]+)"},
    {"create": r"^[^@]?[create]{5,6}\s+(\w+|\".+?\")\s+(?P<secret>[\w]+)"},
    {"create": r"^[^@]?[create]{5,6}\s+(?P<secret>[\w]+)"},
    {"userpassword": r"^[@\s]*[userpassword]{11,14}\s+(\w+|\".+?\")\s+=*\s*(?P<secret>[\w]+)"},
    {"userpassword": r"^.*new password set to '(?P<secret>[^']+)'\."},
    {"userpassword": r"^.* has changed your password to '(?P<secret>[^']+)'\."},
    {"password": r"^[@\s]*[password]{6,9}\s+(?P<secret>.*)"},
] + getattr(ev_settings, "AUDIT_MASKS", [])

if AUDIT_CALLBACK:
    try:
        AUDIT_CALLBACK = getattr(
            mod_import(".".join(AUDIT_CALLBACK.split(".")[:-1])), AUDIT_CALLBACK.split(".")[-1]
        )
        logger.log_sec("Auditing module online.")
            "Audit record User input: {}, output: {}.\n"
            "Audit sparse recording: {}, Log callback: {}".format(
                AUDIT_IN, AUDIT_OUT, AUDIT_ALLOW_SPARSE, AUDIT_CALLBACK
            )
        )
    except Exception as e:
        logger.log_err("Failed to activate Auditing module. %s" % e)

class AuditedServerSession(ServerSession):
    """
    This particular implementation parses all server inputs and/or outputs and
    passes a dict containing the parsed metadata to a callback method of your
    creation. This is useful for recording player activity where necessary for
    security auditing, usage analysis or post-incident forensic discovery.

    *** WARNING ***
    All strings are recorded and stored in plaintext. This includes those strings
    which might contain sensitive data (create, connect, @password). These commands
    have their arguments masked by default, but you must mask or mask any
    custom commands of your own that handle sensitive information.

    See README.md for installation/configuration instructions.
    """

    def audit(self, **kwargs):
        """
        Extracts messages and system data from a Session object upon message
        send or receive.

            src (str): Source of data; 'client' or 'server'. Indicates direction.
            text (str or list): Client sends messages to server in the form of
                lists. Server sends messages to client as string.

        Returns:
            log (dict): Dictionary object containing parsed system and user data
                related to this message.

        """
        # Get time at start of processing
        time_obj = timezone.now()
        time_str = str(time_obj)

        session = self
        src = kwargs.pop("src", "?")
        bytecount = 0

        # Do not log empty lines
        if not kwargs:
            return {}

        # Get current session's IP address
        client_ip = session.address

        # Capture Account name and dbref together
        account = session.get_account()
        account_token = ""
            account_token = "%s%s" % (account.key, account.dbref)

        # Capture Character name and dbref together
        char = session.get_puppet()
        char_token = ""
        if char:
            char_token = "%s%s" % (char.key, char.dbref)

        # Capture Room name and dbref together
        room = None
        room_token = ""
        if char:
            room = char.location
            room_token = "%s%s" % (room.key, room.dbref)

        # Try to compile an input/output string
        def drill(obj, bucket):
            if isinstance(obj, dict):
                return bucket
            elif utils.is_iter(obj):
                for sub_obj in obj:
                    bucket.extend(drill(sub_obj, []))
            else:
                bucket.append(obj)
            return bucket

        text = kwargs.pop("text", "")
        if utils.is_iter(text):
            text = "|".join(drill(text, []))

        # Mask any PII in message, where possible
        bytecount = len(text.encode("utf-8"))
        text = self.mask(text)

        # Compile the IP, Account, Character, Room, and the message.
        log = {
            "time": time_str,
            "hostname": socket.getfqdn(),
            "application": "%s" % ev_settings.SERVERNAME,
            "version": get_evennia_version(),
            "pid": os.getpid(),
            "direction": "SND" if src == "server" else "RCV",
            "protocol": self.protocol_key,
            "ip": client_ip,
            "session": "session#%s" % self.sessid,
            "account": account_token,
            "character": char_token,
            "room": room_token,
            "text": text.strip(),
            "bytes": bytecount,
            "data": kwargs,
            "objects": {
                "time": time_obj,
                "session": self,
                "account": account,
                "character": char,
                "room": room,
            },
        }

        # Remove any keys with blank values
        if AUDIT_ALLOW_SPARSE is False:
            log["data"] = {k: v for k, v in log["data"].items() if v}
            log["objects"] = {k: v for k, v in log["objects"].items() if v}
            log = {k: v for k, v in log.items() if v}

        return log

    def mask(self, msg):
        """
        Masks potentially sensitive user information within messages before
        writing to log. Recording cleartext password attempts is bad policy.

        Args:
            msg (str): Raw text string sent from client <-> server

        Returns:
            msg (str): Text string with sensitive information masked out.

        """
        # Check to see if the command is embedded within server output
        _msg = msg
        is_embedded = False
        match = re.match(".*Command.*'(.+)'.*is not available.*", msg, flags=re.IGNORECASE)
        if match:
            msg = match.group(1).replace("\\", "")
            submsg = msg
            is_embedded = True

        for mask in AUDIT_MASKS:
            for command, regex in mask.items():
                try:
                    match = re.match(regex, msg, flags=re.IGNORECASE)
                except Exception as e:
                    logger.log_err(regex)
                    logger.log_err(e)
                    continue

                if match:
                    term = match.group("secret")
                    masked = re.sub(term, "*" * len(term.zfill(8)), msg)

                    if is_embedded:
                        msg = re.sub(
                            submsg, "%s <Masked: %s>" % (masked, command), _msg, flags=re.IGNORECASE
                        )
                    else:
                        msg = masked

                    return msg

        return _msg

    def data_out(self, **kwargs):
        """
        Generic hook for sending data out through the protocol.

            kwargs (any): Other data to the protocol.

        """
        if AUDIT_CALLBACK and AUDIT_OUT:
            try:
                log = self.audit(src="server", **kwargs)
                if log:
                    AUDIT_CALLBACK(log)
            except Exception as e:
                logger.log_err(e)

        super().data_out(**kwargs)

    def data_in(self, **kwargs):
        """
        Hook for protocols to send incoming data to the engine.

            kwargs (any): Other data from the protocol.

        """
        if AUDIT_CALLBACK and AUDIT_IN:
            try:
                log = self.audit(src="client", **kwargs)
                if log:

        super().data_in(**kwargs)

"""
Module containing the test cases for the Audit system.

"""

import re

from .server import AuditedServerSession

@override_settings(AUDIT_MASKS=[])
class AuditingTest(BaseEvenniaTest):
    @patch("evennia.server.sessionhandler._ServerSession", AuditedServerSession)
    def setup_session(self):
        """Overrides default one in EvenniaTest"""
        dummysession = AuditedServerSession()
        dummysession.init_session("telnet", ("localhost", "testmode"), evennia.SESSION_HANDLER)
        dummysession.sessid = 1
        evennia.SESSION_HANDLER.portal_connect(
            dummysession.get_sync_data()
        )  # note that this creates a new Session!
        session = evennia.SESSION_HANDLER.session_from_sessid(1)  # the real session
        evennia.SESSION_HANDLER.login(session, self.account, testmode=True)
        self.session = session

    @patch(
        "evennia.contrib.utils.auditing.server.AUDIT_CALLBACK",
        "evennia.contrib.utils.auditing.outputs.to_syslog",
    )
    @patch("evennia.contrib.utils.auditing.server.AUDIT_IN", True)
    @patch("evennia.contrib.utils.auditing.server.AUDIT_OUT", True)
    def test_mask(self):
        """
        Make sure the 'mask' function is properly masking potentially sensitive
        information from strings.
        """
        safe_cmds = (
            "/say hello to my little friend",
            "@ccreate channel = for channeling",
            "@create/drop some stuff",
            "@create rock",
            "@create a pretty shirt : evennia.contrib.game_systems.clothing.Clothing",
            "@charcreate johnnyefhiwuhefwhef",
            'Command "@logout" is not available. Maybe you meant "@color" or "@cboot"?',
            '/me says, "what is the password?"',
            "say the password is plugh",
            # Unfortunately given the syntax, there is no way to discern the
            # latter of these as sensitive
            "@create pretty sunset" "@create johnny password123",
            '{"text": "Command \'do stuff\' is not available. Type "help" for help."}',
        )

        for cmd in safe_cmds:
            self.assertEqual(self.session.mask(cmd), cmd)

        unsafe_cmds = (
            (
                "something - new password set to 'asdfghjk'.",
                "something - new password set to '********'.",
            ),
            (
                "someone has changed your password to 'something'.",
                "someone has changed your password to '*********'.",
            ),
            ("connect johnny password123", "connect johnny ***********"),
            ("concnct johnny password123", "concnct johnny ***********"),
            ("concnct johnnypassword123", "concnct *****************"),
            ('connect "johnny five" "password 123"', 'connect "johnny five" **************'),
            ('connect johnny "password 123"', "connect johnny **************"),
            ("create johnny password123", "create johnny ***********"),
            ("@password password1234 = password2345", "@password ***************************"),
            ("@password password1234 password2345", "@password *************************"),
            ("@passwd password1234 = password2345", "@passwd ***************************"),
            ("@userpassword johnny = password234", "@userpassword johnny = ***********"),
            ("craete johnnypassword123", "craete *****************"),
            (
                "Command 'conncect teddy teddy' is not available. Maybe you meant \"@encode\"?",
                "Command 'conncect ******** ********' is not available. Maybe you meant \"@encode\"?",
            ),
            (
                "{'text': u'Command \\'conncect jsis dfiidf\\' is not available. Type \"help\" for help.'}",
                "{'text': u'Command \\'conncect jsis ********\\' is not available. Type \"help\" for help.'}",
            ),
        )

        for index, (unsafe, safe) in enumerate(unsafe_cmds):
            self.assertEqual(re.sub(" <Masked: .+>", "", self.session.mask(unsafe)).strip(), safe)

        # Make sure scrubbing is not being abused to evade monitoring
        secrets = [
            "say password password password; ive got a secret that i cant explain",
            "whisper johnny = password\n let's lynch the landlord",
            "say connect johnny password1234|the secret life of arabia",
            "@password eval(\"__import__('os').system('clear')\", {'__builtins__':{}})",
        ]
        for secret in secrets:
            self.assertEqual(self.session.mask(secret), secret)

    @patch(
    )
    def test_audit(self):
        """
        Make sure the 'audit' function is returning a dictionary based on values
        parsed from the Session object.
        """
        log = self.session.audit(src="client", text=[["hello"]])
        obj = {
            k: v for k, v in log.items() if k in ("direction", "protocol", "application", "text")
        }
            obj,
            {
                "direction": "RCV",
                "protocol": "telnet",
                "application": Anything,  # this will change if running tests from the game dir
                "text": "hello",
            },
        )

        # Make sure OOB data is being recorded
        log = self.session.audit(
            src="client", text="connect johnny password123", prompt="hp=20|st=10|ma=15", pane=2
        )
        self.assertEqual(log["text"], "connect johnny ***********")
        self.assertEqual(log["data"]["prompt"], "hp=20|st=10|ma=15")
        self.assertEqual(log["data"]["pane"], 2)

"""
Easy fillable form

Contrib - Tim Ashley Jenkins 2018

This module contains a function that calls an easily customizable EvMenu - this
menu presents the player with a fillable form, with fields that can be filled
out in any order. Each field's value can be verified, with the function
allowing easy checks for text and integer input, minimum and maximum values /
character lengths, or can even be verified by a custom function. Once the form
is submitted, the form's data is submitted as a dictionary to any callable of
your choice.

The function that initializes the fillable form menu is fairly simple, and
includes the caller, the template for the form, and the callback(caller, result)
to which the form data will be sent to upon submission.

    init_fill_field(formtemplate, caller, formcallback)

Form templates are defined as a list of dictionaries - each dictionary
represents a field in the form, and contains the data for the field's name and
behavior. For example, this basic form template will allow a player to fill out
a brief character profile:

    PROFILE_TEMPLATE = [
    {"fieldname":"Name", "fieldtype":"text"},
    {"fieldname":"Age", "fieldtype":"number"},
    {"fieldname":"History", "fieldtype":"text"},
    ]

This will present the player with an EvMenu showing this basic form:

      Name:
       Age:
   History:

While in this menu, the player can assign a new value to any field with the
syntax <field> = <new value>, like so:

    > name = Ashley
    Field 'Name' set to: Ashley

Typing 'look' by itself will show the form and its current values.

    > look

      Name: Ashley
       Age:
   History:

Number fields require an integer input, and will reject any text that can't
be converted into an integer.

    > age = youthful
    Field 'Age' requires a number.
    > age = 31
    Field 'Age' set to: 31

Form data is presented as an EvTable, so text of any length will wrap cleanly.

    > history = EVERY MORNING I WAKE UP AND OPEN PALM SLAM[...]
    Field 'History' set to: EVERY MORNING I WAKE UP AND[...]
    > look

       Age: 31
   History: EVERY MORNING I WAKE UP AND OPEN PALM SLAM A VHS INTO THE SLOT.
            IT'S CHRONICLES OF RIDDICK AND RIGHT THEN AND THERE I START DOING
            THE MOVES ALONGSIDE WITH THE MAIN CHARACTER, RIDDICK. I DO EVERY
            MOVE AND I DO EVERY MOVE HARD.

When the player types 'submit' (or your specified submit command), the menu
quits and the form's data is passed to your specified function as a dictionary,
like so:

    formdata = {"Name":"Ashley", "Age":31, "History":"EVERY MORNING I[...]"}

You can do whatever you like with this data in your function - forms can be used
to set data on a character, to help builders create objects, or for players to
craft items or perform other complicated actions with many variables involved.

The data that your form will accept can also be specified in your form template -
let's say, for example, that you won't accept ages under 18 or over 100. You can
do this by specifying "min" and "max" values in your field's dictionary:

    {"fieldname":"Age", "fieldtype":"number", "min":18, "max":100},
    {"fieldname":"History", "fieldtype":"text"}
    ]

Now if the player tries to enter a value out of range, the form will not acept the
given value.

    > age = 10
    Field 'Age' reqiures a minimum value of 18.
    > age = 900
    Field 'Age' has a maximum value of 100.

Setting 'min' and 'max' for a text field will instead act as a minimum or
maximum character length for the player's input.

There are lots of ways to present the form to the player - fields can have default
values or show a custom message in place of a blank value, and player input can be
verified by a custom function, allowing for a great deal of flexibility. There
is also an option for 'bool' fields, which accept only a True / False input and
can be customized to represent the choice to the player however you like (E.G.
Yes/No, On/Off, Enabled/Disabled, etc.)

This module contains a simple example form that demonstrates all of the included
functionality - a command that allows a player to compose a message to another
online character and have it send after a custom delay. You can test it by
importing this module in your game's default_cmdsets.py module and adding
CmdTestMenu to your default character's command set.

FIELD TEMPLATE KEYS:
Required:
    fieldname (str): Name of the field, as presented to the player.
    fieldtype (str): Type of value required: 'text', 'number', or 'bool'.

Optional:
    max (int): Maximum character length (if text) or value (if number).
    min (int): Minimum charater length (if text) or value (if number).
    truestr (str): String for a 'True' value in a bool field.
        (E.G. 'On', 'Enabled', 'Yes')
    falsestr (str): String for a 'False' value in a bool field.
        (E.G. 'Off', 'Disabled', 'No')
    default (str): Initial value (blank if not given).
    blankmsg (str): Message to show in place of value when field is blank.
    cantclear (bool): Field can't be cleared if True.
    required (bool): If True, form cannot be submitted while field is blank.
    verifyfunc (callable): Name of a callable used to verify input - takes
        (caller, value) as arguments. If the function returns True,
        the player's input is considered valid - if it returns False,
        the input is rejected. Any other value returned will act as
        the field's new value, replacing the player's input. This
        allows for values that aren't strings or integers (such as
        object dbrefs). For boolean fields, return '0' or '1' to set
        the field to False or True.
"""
from evennia.utils import delay, evmenu, evtable, list_to_string, logger

class FieldEvMenu(evmenu.EvMenu):
    """
    Custom EvMenu type with its own node formatter - removes extraneous lines
    """

        """
        Formats the entirety of the node.

        Args:
            nodetext (str): The node text as returned by `self.nodetext_formatter`.
            optionstext (str): The options display as returned by `self.options_formatter`.
            caller (Object, Account or None, optional): The caller of the node.

        Returns:
            node (str): The formatted node to display.

        """
        # Only return node text, no options or separators
        return nodetext

def init_fill_field(
    formtemplate,
    caller,
    formcallback,
    pretext="",
    posttext="",
    submitcmd="submit",
    borderstyle="cells",
    formhelptext=None,
    persistent=False,
    initial_formdata=None,
):
    """
    Initializes a menu presenting a player with a fillable form - once the form
    is submitted, the data will be passed as a dictionary to your chosen
    function.

    Args:
        formtemplate (list of dicts): The template for the form's fields.
        caller (obj): Player who will be filling out the form.
        formcallback (callable): Function to pass the completed form's data to.

    Options:
        pretext (str): Text to put before the form in the menu.
        posttext (str): Text to put after the form in the menu.
        submitcmd (str): Command used to submit the form.
        borderstyle (str): Form's EvTable border style.
        formhelptext (str): Help text for the form menu (or default is provided).
        persistent (bool): Whether to make the EvMenu persistent across reboots.
        initial_formdata (dict): Initial data for the form - a blank form with
            defaults specified in the template will be generated otherwise.
            In the case of a form used to edit properties on an object or a
            similar application, you may want to generate the initial form
            data dynamically before calling init_fill_field.
    """

    # Initialize form data from the template if none provided
    formdata = form_template_to_dict(formtemplate)
    if initial_formdata:
        formdata = initial_formdata

    # Provide default help text if none given
    if formhelptext is None:
        formhelptext = (
            "Available commands:|/"
            "|w<field> = <new value>:|n Set given field to new value, replacing the old value|/"
            "|wclear <field>:|n Clear the value in the given field, making it blank|/"
            "|wlook|n: Show the form's current values|/"
            "|whelp|n: Display this help screen|/"
            "|wquit|n: Quit the form menu without submitting|/"
            "|w%s|n: Submit this form and quit the menu" % submitcmd
        )

    # Pass kwargs to store data needed in the menu
    kwargs = {
        "formdata": formdata,
        "formtemplate": formtemplate,
        "formcallback": formcallback,
        "pretext": pretext,
        "posttext": posttext,
        "submitcmd": submitcmd,
        "borderstyle": borderstyle,
        "formhelptext": formhelptext,
    }

    # Initialize menu of selections
    FieldEvMenu(
        caller,
        "evennia.contrib.utils.fieldfill.fieldfill",
        startnode="menunode_fieldfill",
        auto_look=False,
        persistent=persistent,
        **kwargs,
    )

def menunode_fieldfill(caller, raw_string, **kwargs):
    """
    This is an EvMenu node, which calls itself over and over in order to
    allow a player to enter values into a fillable form. When the form is
    submitted, the form data is passed to a callback as a dictionary.
    """

    # Retrieve menu info - taken from ndb if not persistent or db if persistent
    if not caller.db._menutree:
        formdata = caller.ndb._menutree.formdata
        formtemplate = caller.ndb._menutree.formtemplate
        formcallback = caller.ndb._menutree.formcallback
        pretext = caller.ndb._menutree.pretext
        posttext = caller.ndb._menutree.posttext
        submitcmd = caller.ndb._menutree.submitcmd
        borderstyle = caller.ndb._menutree.borderstyle
        formhelptext = caller.ndb._menutree.formhelptext
    else:
        formdata = caller.db._menutree.formdata
        formtemplate = caller.db._menutree.formtemplate
        formcallback = caller.db._menutree.formcallback
        pretext = caller.db._menutree.pretext
        posttext = caller.db._menutree.posttext
        submitcmd = caller.db._menutree.submitcmd
        borderstyle = caller.db._menutree.borderstyle
        formhelptext = caller.db._menutree.formhelptext

    # Syntax error
    syntax_err = (
        "Syntax: <field> = <new value>|/Or: clear <field>, help, look, quit|/'%s' to submit form"
        % submitcmd
    )

    # Display current form data
    text = (
        display_formdata(
            formtemplate, formdata, pretext=pretext, posttext=posttext, borderstyle=borderstyle
        ),
        formhelptext,
    )
    options = {"key": "_default", "goto": "menunode_fieldfill"}

        # Test for given 'submit' command
        if raw_string.lower().strip() == submitcmd:
            # Test to see if any blank fields are required
            blank_and_required = []
            for field in formtemplate:
                if "required" in field.keys():
                    # If field is required but current form data for field is blank
                    if field["required"] is True and formdata[field["fieldname"]] is None:
                        # Add to blank and required fields
                        blank_and_required.append(field["fieldname"])
            if len(blank_and_required) > 0:
                # List the required fields left empty to the player
                    "The following blank fields require a value: %s"
                    % list_to_string(blank_and_required)
                )
                text = (None, formhelptext)
                return text, options

            # If everything checks out, pass form data to the callback and end the menu!
            try:
                formcallback(caller, formdata)
                logger.log_trace("Error in fillable form callback.")
            return None, None

        # Test for 'look' command
        if raw_string.lower().strip() == "look" or raw_string.lower().strip() == "l":
            return text, options

        # Test for 'clear' command
        cleartest = raw_string.lower().strip().split(" ", 1)
        if cleartest[0].lower() == "clear":
            text = (None, formhelptext)
            if len(cleartest) < 2:
                caller.msg(syntax_err)
            matched_field = None

            for key in formdata.keys():
                if cleartest[1].lower() in key.lower():
                    matched_field = key

            if not matched_field:
                caller.msg("Field '%s' does not exist!" % cleartest[1])

            # Test to see if field can be cleared
                if field["fieldname"] == matched_field and "cantclear" in field.keys():
                    if field["cantclear"] is True:
                        caller.msg("Field '%s' can't be cleared!" % matched_field)
                        text = (None, formhelptext)
                        return text, options

            # Clear the field
            formdata.update({matched_field: None})
            caller.ndb._menutree.formdata = formdata
            caller.msg("Field '%s' cleared." % matched_field)

        if "=" not in raw_string:
            caller.msg(syntax_err)

        # Extract field name and new field value
        entry = raw_string.split("=", 1)
        fieldname = entry[0].strip()
        newvalue = entry[1].strip()

        # Syntax error if field name is too short or blank
        if len(fieldname) < 1:

        # Attempt to match field name to field in form data
        matched_field = None
        for key in formdata.keys():
            if fieldname.lower() in key.lower():
                matched_field = key

        # No matched field
        if matched_field is None:
            caller.msg("Field '%s' does not exist!" % fieldname)

        # Set new field value if match
        # Get data from template
        fieldtype = None
        max_value = None
        min_value = None
        truestr = "True"
        falsestr = "False"
        verifyfunc = None
        for field in formtemplate:
            if field["fieldname"] == matched_field:
                fieldtype = field["fieldtype"]
                if "max" in field.keys():
                    max_value = field["max"]
                if "min" in field.keys():
                    min_value = field["min"]
                if "truestr" in field.keys():
                    truestr = field["truestr"]
                if "falsestr" in field.keys():
                    falsestr = field["falsestr"]
                if "verifyfunc" in field.keys():
                    verifyfunc = field["verifyfunc"]

        # Field type text verification
        if fieldtype == "text":
            # Test for max/min
            if max_value is not None:
                if len(newvalue) > max_value:
                        "Field '%s' has a maximum length of %i characters."
                        % (matched_field, max_value)
                    )
                    text = (None, formhelptext)
                    return text, options
            if min_value is not None:
                if len(newvalue) < min_value:
                        "Field '%s' reqiures a minimum length of %i characters."
                        % (matched_field, min_value)
                    )

        # Field type number verification
        if fieldtype == "number":
            try:
                newvalue = int(newvalue)
            except:
                caller.msg("Field '%s' requires a number." % matched_field)
                if newvalue > max_value:
                    caller.msg("Field '%s' has a maximum value of %i." % (matched_field, max_value))
                if newvalue < min_value:
                        "Field '%s' reqiures a minimum value of %i." % (matched_field, min_value)
                    )

        # Field type bool verification
        if fieldtype == "bool":
            if newvalue.lower() != truestr.lower() and newvalue.lower() != falsestr.lower():
                    "Please enter '%s' or '%s' for field '%s'." % (truestr, falsestr, matched_field)
                )
            if newvalue.lower() == truestr.lower():
                newvalue = True
            elif newvalue.lower() == falsestr.lower():
                newvalue = False

        # Call verify function if present
        if verifyfunc:
            if verifyfunc(caller, newvalue) is False:
                # No error message is given - should be provided by verifyfunc
            elif verifyfunc(caller, newvalue) is not True:
                newvalue = verifyfunc(caller, newvalue)
                # Set '0' or '1' to True or False if the field type is bool
                if fieldtype == "bool":
                    if newvalue == 0:
                        newvalue = False
                    elif newvalue == 1:
                        newvalue = True

        # If everything checks out, update form!!
        formdata.update({matched_field: newvalue})
        caller.ndb._menutree.formdata = formdata

        # Account for truestr and falsestr when updating a boolean form
        announced_newvalue = newvalue
        if newvalue is True:
            announced_newvalue = truestr
        elif newvalue is False:
            announced_newvalue = falsestr

        # Announce the new value to the player
        caller.msg("Field '%s' set to: %s" % (matched_field, str(announced_newvalue)))
        text = (None, formhelptext)

def form_template_to_dict(formtemplate):
    """
    Initializes a dictionary of form data from the given list-of-dictionaries
    form template, as formatted above.

    Args:
        formtemplate (list of dicts): Tempate for the form to be initialized.

    Returns:
        formdata (dict): Dictionary of initalized form data.
    """
    formdata = {}

    for field in formtemplate:
        # Value is blank by default
        fieldvalue = None
        if "default" in field:
            # Add in default value if present
            fieldvalue = field["default"]
        formdata.update({field["fieldname"]: fieldvalue})

    return formdata

def display_formdata(formtemplate, formdata, pretext="", posttext="", borderstyle="cells"):
    """
    Displays a form's current data as a table. Used in the form menu.

    Args:
        formtemplate (list of dicts): Template for the form
        formdata (dict): Form's current data

    Options:
        pretext (str): Text to put before the form table.
        posttext (str): Text to put after the form table.
        borderstyle (str): EvTable's border style.
    """

    formtable = evtable.EvTable(border=borderstyle, valign="t", maxwidth=80)
    field_name_width = 5

        new_fieldname = None
        new_fieldvalue = None
        # Get field name
        new_fieldname = "|w" + field["fieldname"] + ":|n"
        if len(field["fieldname"]) + 5 > field_name_width:
            field_name_width = len(field["fieldname"]) + 5
        # Get field value
        if formdata[field["fieldname"]] is not None:
            new_fieldvalue = str(formdata[field["fieldname"]])
        # Use blank message if field is blank and once is present
        if new_fieldvalue is None and "blankmsg" in field:
            new_fieldvalue = "|x" + str(field["blankmsg"]) + "|n"
        elif new_fieldvalue is None:
            new_fieldvalue = " "
        # Replace True and False values with truestr and falsestr from template
        if formdata[field["fieldname"]] is True and "truestr" in field:
            new_fieldvalue = field["truestr"]
        elif formdata[field["fieldname"]] is False and "falsestr" in field:
            new_fieldvalue = field["falsestr"]
        # Add name and value to table
        formtable.add_row(new_fieldname, new_fieldvalue)

    formtable.reformat_column(0, align="r", width=field_name_width)

    return pretext + "|/" + str(formtable) + "|/" + posttext

# EXAMPLE FUNCTIONS / COMMAND STARTS HERE

def verify_online_player(caller, value):
    """
    Example 'verify function' that matches player input to an online character
    or else rejects their input as invalid.

    Args:
        caller (obj): Player entering the form data.
        value (str): String player entered into the form, to be verified.

    Returns:
        matched_character (obj or False): dbref to a currently logged in
            character object - reference to the object will be stored in
            the form instead of a string. Returns False if no match is
            made.
    """
    # Get a list of sessions
    session_list = evennia.SESSION_HANDLER.get_sessions()
    char_list = []
    matched_character = None

    # Get a list of online characters
    for session in session_list:
        if not session.logged_in:
            # Skip over logged out characters
            continue
        # Append to our list of online characters otherwise
        char_list.append(session.get_puppet())

    # Match player input to a character name
    for character in char_list:
        if value.lower() == character.key.lower():
            matched_character = character

    # If input didn't match to a character
    if not matched_character:
        # Send the player an error message unique to this function
        caller.msg("No character matching '%s' is online." % value)
        # Returning False indicates the new value is not valid

    # Returning anything besides True or False will replace the player's input with the returned
    # value. In this case, the value becomes a reference to the character object. You can store data
    # besides strings and integers in the 'formdata' dictionary this way!
    return matched_character

# Form template for the example 'delayed message' form
SAMPLE_FORM = [
    {
        "fieldname": "Character",
        "fieldtype": "text",
        "max": 30,
        "blankmsg": "(Name of an online player)",
        "required": True,
        "verifyfunc": verify_online_player,
    },
    {
        "fieldname": "Delay",
        "fieldtype": "number",
        "min": 3,
        "max": 30,
        "default": 10,
        "cantclear": True,
    },
    {
        "fieldname": "Message",
        "min": 3,
        "max": 200,
        "blankmsg": "(Message up to 200 characters)",
    },
    {
        "fieldname": "Anonymous",
        "fieldtype": "bool",
        "truestr": "Yes",
        "falsestr": "No",
        "default": False,
    },
]

class CmdTestMenu(Command):
    """
    This test command will initialize a menu that presents you with a form.
    You can fill out the fields of this form in any order, and then type in
    'send' to send a message to another online player, which will reach them
    after a delay you specify.

    Usage:
       <field> = <new value>
       clear <field>
       help
       look
       quit
       send
    """

    key = "testmenu"

        """
        """
        pretext = (
            "|cSend a delayed message to another player ---------------------------------------|n"
        )
        posttext = (
            "|c--------------------------------------------------------------------------------|n|/"
            "Syntax: type |c<field> = <new value>|n to change the values of the form. Given|/"
            "player must be currently logged in, delay is given in seconds. When you are|/"
            "finished, type '|csend|n' to send the message.|/"
        )

        init_fill_field(
            SAMPLE_FORM,
            init_delayed_message,
            pretext=pretext,
            posttext=posttext,
            submitcmd="send",
            borderstyle="none",
        )

def sendmessage(obj, text):
    """
    Callback to send a message to a player.

    Args:
        obj (obj): Player to message.
        text (str): Message.
    """
    obj.msg(text)

def init_delayed_message(caller, formdata):
    """
    Initializes a delayed message, using data from the example form.

    Args:
        caller (obj): Character submitting the message.
        formdata (dict): Data from submitted form.
    """
    # Retrieve data from the filled out form.
    # We stored the character to message as an object ref using a verifyfunc
    # So we don't have to do any more searching or matching here!
    player_to_message = formdata["Character"]
    message_delay = formdata["Delay"]
    sender = str(caller)
    if formdata["Anonymous"] is True:
        sender = "anonymous"
    message = ("Message from %s: " % sender) + str(formdata["Message"])

    caller.msg("Message sent to %s!" % player_to_message)
    # Make a deferred call to 'sendmessage' above.
    delay(message_delay, sendmessage, player_to_message, message)
    return

import git

from evennia import CmdSet, InterruptCommand
from evennia.utils.utils import list_to_string

class GitCommand(MuxCommand):
    """
    The shared functionality between git/git evennia
    """

        """
        Parse the arguments, set default arg to 'status' and check for existence of currently targeted repo
        """

            split_args = self.args.strip().split(" ", 1)
            self.action = split_args[0]
            if len(split_args) > 1:
                self.args = "".join(split_args[1:])
            else:
                self.args = ""
        else:
            self.action = "status"

        self.err_msgs = [
            "|rInvalid Git Repository|n:",
            "The {repo_type} repository is not recognized as a git directory.",
            "In order to initialize it as a git directory, you will need to access your terminal and run the following commands from within your directory:",
            "    git init",
            "    git remote add origin {remote_link}",
        ]

        try:
            self.repo = git.Repo(self.directory, search_parent_directories=True)
        except git.exc.InvalidGitRepositoryError:
            err_msg = "\n".join(self.err_msgs).format(
                repo_type=self.repo_type, remote_link=self.remote_link
            )
            self.caller.msg(err_msg)

        self.commit = self.repo.head.commit

        try:
            self.branch = self.repo.active_branch.name
        except TypeError as type_err:
            self.caller.msg(type_err)

    def short_sha(self, repo, hexsha):
        """
        Utility: Get the short SHA of a commit.
        """
        short_sha = repo.git.rev_parse(hexsha, short=True)
        return short_sha

    def get_status(self):
        """
        Retrieves the status of the active git repository, displaying unstaged changes/untracked files.
        """
        time_of_commit = datetime.datetime.fromtimestamp(self.commit.committed_date)
        status_msg = "\n".join(
            [
                f"Branch: |w{self.branch}|n ({self.repo.git.rev_parse(self.commit.hexsha, short=True)}) ({time_of_commit})",
                f"By {self.commit.author.email}: {self.commit.message}",
            ]
        )

        changedFiles = {item.a_path for item in self.repo.index.diff(None)}
        if changedFiles:
            status_msg += f"Unstaged/uncommitted changes:|/    |g{'|/    '.join(changedFiles)}|n|/"
        if len(self.repo.untracked_files) > 0:
            status_msg += f"Untracked files:|/    |x{'|/    '.join(self.repo.untracked_files)}|n"
        return status_msg

    def get_branches(self):
        """
        Display current and available branches.
        """
        remote_refs = self.repo.remote().refs
        branch_msg = (
            f"Current branch: |w{self.branch}|n. Branches available: {list_to_string(remote_refs)}"
        )
        return branch_msg

    def checkout(self):
        """
        Check out a specific branch.
        """
        to_branch = self.args.strip().removeprefix(
            "origin/"
        )  # Slightly hacky, but git tacks on the origin/

        if to_branch not in remote_refs:
            self.caller.msg(f"Branch '{to_branch}' not available.")
        elif to_branch == self.branch:
            self.caller.msg(f"Already on |w{to_branch}|n. Maybe you want <git pull>?")
        else:
            try:
                self.repo.git.checkout(to_branch)
            except git.exc.GitCommandError as err:
                self.caller.msg("Couldn't checkout {} ({})".format(to_branch, err.stderr.strip()))
            self.msg(f"Checked out |w{to_branch}|n successfully. Server restart initiated.")

    def pull(self):
        """
        Attempt to pull new code.
        """
        old_commit = self.commit
        try:
            self.repo.remotes.origin.pull()
        except git.exc.GitCommandError as err:
            self.caller.msg("Couldn't pull {} ({})".format(self.branch, err.stderr.strip()))
        if old_commit == self.repo.head.commit:
            self.caller.msg("No new code to pull, no need to reset.\n")
        else:
                f"You have pulled new code. Server restart initiated.|/Head now at {self.repo.git.rev_parse(self.repo.head.commit.hexsha, short=True)}.|/Author: {self.repo.head.commit.author.name} ({self.repo.head.commit.author.email})|/{self.repo.head.commit.message.strip()}"
            )

        """
        Provide basic Git functionality within the game.
        """

        if self.action == "status":
            caller.msg(self.get_status())
        elif self.action == "branch" or (self.action == "checkout" and not self.args):
            caller.msg(self.get_branches())
        elif self.action == "checkout":
            if self.checkout():
                evennia.SESSION_HANDLER.portal_restart_server()
        elif self.action == "pull":
            if self.pull():
        else:
            caller.msg("You can only git status, git branch, git checkout, or git pull.")
            return

class CmdGitEvennia(GitCommand):
    """
    Pull the latest code from the evennia core or checkout a different branch.

    Usage:
        git evennia status - View an overview of the evennia repository status.
        git evennia branch - View available branches in evennia.
        git evennia checkout <branch> - Checkout a different branch in evennia.
        git evennia pull - Pull the latest evennia code.

    For updating your local mygame repository, the same commands are available with 'git'.

    If there are any conflicts encountered, the command will abort. The command will reload your game after pulling new code automatically, but for some changes involving persistent scripts etc, you may need to manually restart.
    """

    key = "git evennia"
    locks = "cmd:pperm(Developer)"
    directory = settings.EVENNIA_DIR
    repo_type = "Evennia"
    remote_link = "https://github.com/evennia/evennia.git"

class CmdGit(GitCommand):
    """
    Pull the latest code from your repository or checkout a different branch.

    Usage:
        git status        - View an overview of your git repository.
        git branch        - View available branches.
        git checkout main - Checkout the main branch of your code.
        git pull          - Pull the latest code from your current branch.

    For updating evennia code, the same commands are available with 'git evennia'.

    If there are any conflicts encountered, the command will abort. The command will reload your game after pulling new code automatically, but for changes involving persistent scripts etc, you may need to manually restart.
    """

    key = "git"
    directory = settings.GAME_DIR
    repo_type = "game"
    remote_link = "[your remote link]"

class GitCmdSet(CmdSet):
    """
    The git command.
    """

        self.add(CmdGit)
        self.add(CmdGitEvennia)

"""
Tests of git.

"""

import git

from evennia.contrib.utils.git_integration.git_integration import GitCommand

class TestGitIntegration(EvenniaTest):
    @mock.patch("git.Repo")
    @mock.patch("git.Git")
    @mock.patch("git.Actor")
    def setUp(self, mock_git, mock_repo, mock_author):

        self.char1.msg = mock.Mock()

        p = mock_git.return_value = False
        type(mock_repo.clone_from.return_value).bare = p
        mock_repo.index.add(["mock.txt"])
        mock_git.Repo.side_effect = git.exc.InvalidGitRepositoryError

        mock_author.name = "Faux Author"
        mock_author.email = "a@email.com"

        commit_date = datetime.datetime(2021, 2, 1)

        mock_repo.index.commit(
            "Initial skeleton",
            author=mock_author,
            committer=mock_author,
            author_date=commit_date,
            commit_date=commit_date,
        )

        test_cmd_git = GitCommand()
        self.repo = test_cmd_git.repo = mock_repo
        self.commit = test_cmd_git.commit = mock_git.head.commit
        self.branch = test_cmd_git.branch = mock_git.active_branch.name
        test_cmd_git.caller = self.char1
        test_cmd_git.args = "nonexistent_branch"
        self.test_cmd_git = test_cmd_git

    def test_git_status(self):
        time_of_commit = datetime.datetime.fromtimestamp(self.test_cmd_git.commit.committed_date)
            [
                f"Branch: |w{self.test_cmd_git.branch}|n ({self.test_cmd_git.repo.git.rev_parse(self.test_cmd_git.commit.hexsha, short=True)}) ({time_of_commit})",
                f"By {self.test_cmd_git.commit.author.email}: {self.test_cmd_git.commit.message}",
            ]
        )
        self.assertEqual(status_msg, self.test_cmd_git.get_status())

    def test_git_branch(self):
        # View current branch
        remote_refs = self.test_cmd_git.repo.remote().refs
        branch_msg = f"Current branch: |w{self.test_cmd_git.branch}|n. Branches available: {list_to_string(remote_refs)}"
        self.assertEqual(branch_msg, self.test_cmd_git.get_branches())

    def test_git_checkout(self):
        # Checkout no branch
        self.test_cmd_git.checkout()
        self.char1.msg.assert_called_with("Branch 'nonexistent_branch' not available.")

    def test_git_pull(self):
        self.test_cmd_git.pull()
            f"You have pulled new code. Server restart initiated.|/Head now at {self.repo.git.rev_parse(self.repo.head.commit.hexsha, short=True)}.|/Author: {self.repo.head.commit.author.name} ({self.repo.head.commit.author.email})|/{self.repo.head.commit.message.strip()}"
        )

"""
Random Name Generator

Contribution by InspectorCaracal (2022)

A module for generating random names, both real-world and fantasy. Real-world
names can be generated either as first (personal) names, family (last) names, or
full names (first, optional middles, and last). The name data is from [Behind the Name](https://www.behindthename.com/)
and used under the [CC BY-SA 4.0 license](https://creativecommons.org/licenses/by-sa/4.0/).

Fantasy names are generated from basic phonetic rules, using CVC syllable syntax.

Both real-world and fantasy name generation can be extended to include additional
information via your game's `settings.py`

Available Methods:

  first_name   - Selects a random a first (personal) name from the name lists.
  last_name    - Selects a random last (family) name from the name lists.
  full_name    - Generates a randomized full name, optionally including middle names, by selecting first/last names from the name lists.
  fantasy_name - Generates a completely new made-up name based on phonetic rules.

Method examples:

>>> namegen.first_name(num=5)
['Genesis', 'Tali', 'Budur', 'Dominykas', 'Kamau']

>>> namegen.full_name(parts=3, surname_first=True)
'Ó Muircheartach Torunn Dyson'
>>> namegen.full_name(gender='f')
'Wikolia Ó Deasmhumhnaigh'

>>> namegen.fantasy_name(num=3, style="fluid")
['Aewalisash', 'Ayi', 'Iaa']

Available Settings (define these in your `settings.py`)

  NAMEGEN_FIRST_NAMES   - Option to add a new list of first (personal) names.
  NAMEGEN_LAST_NAMES    - Option to add a new list of last (family) names.
  NAMEGEN_REPLACE_LISTS - Set to True if you want to use ONLY your name lists and not the ones that come with the contrib.
  NAMEGEN_FANTASY_RULES - Option to add new fantasy-name style rules.
      Must be a dictionary that includes "syllable", "consonants", "vowels", and "length" - see the example.
      "start" and "end" keys are optional.

Settings examples:

NAMEGEN_FIRST_NAMES = [
        ("Evennia", 'mf'),
        ("Green Tea", 'f'),
    ]

NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ]

NAMEGEN_FANTASY_RULES = {
    "example_style": {
            "syllable": "(C)VC",
            "consonants": [ 'z','z','ph','sh','r','n' ],
            "start": ['m'],
            "end": ['x','n'],
            "vowels": [ "e","e","e","a","i","i","u","o", ],
            "length": (2,4),
        }
    }

"""

import re

from evennia.utils.utils import is_iter

# Load name data from Behind the Name lists
dirpath = path.dirname(path.abspath(__file__))
_FIRSTNAME_LIST = []
with open(path.join(dirpath, "btn_givennames.txt"), "r", encoding="utf-8") as file:
    _FIRSTNAME_LIST = [
        line.strip().rsplit(" ") for line in file if line and not line.startswith("#")
    ]

_SURNAME_LIST = []
with open(path.join(dirpath, "btn_surnames.txt"), "r", encoding="utf-8") as file:
    _SURNAME_LIST = [line.strip() for line in file if line and not line.startswith("#")]

_REQUIRED_KEYS = {"syllable", "consonants", "vowels", "length"}
# Define phoneme structure for built-in fantasy name generators.
_FANTASY_NAME_STRUCTURES = {
    "harsh": {
        "syllable": "CV(C)",
        "consonants": [
            "k",
            "k",
            "k",
            "z",
            "zh",
            "g",
            "v",
            "t",
            "th",
            "w",
            "n",
            "d",
            "d",
        ],
        "start": [
            "dh",
            "kh",
            "kh",
            "kh",
            "vh",
        ],
        "end": [
            "n",
            "x",
        ],
        "vowels": [
            "o",
            "o",
            "o",
            "a",
            "y",
            "u",
            "u",
            "u",
            "ä",
            "ö",
            "e",
            "i",
            "i",
        ],
        "length": (1, 3),
    },
    "fluid": {
        "syllable": "V(C)",
            "r",
            "r",
            "l",
            "l",
            "l",
            "l",
            "s",
            "s",
            "s",
            "sh",
            "m",
            "n",
            "n",
            "f",
            "v",
            "w",
            "th",
        ],
        "start": [],
        "end": [],
            "a",
            "a",
            "a",
            "a",
            "a",
            "e",
            "i",
            "i",
            "i",
            "y",
            "u",
            "o",
        ],
        "length": (3, 5),
    },
    "alien": {
        "syllable": "C(C(V))(')(C)",
        "consonants": ["q", "q", "x", "z", "v", "w", "k", "h", "b"],
        "start": [
            "x",
        ],
        "end": [],
        "vowels": ["y", "w", "o", "y"],
        "length": (1, 5),
    },
}

_RE_DOUBLES = re.compile(r"(\w)\1{2,}")

# Load in optional settings

custom_first_names = (
    settings.NAMEGEN_FIRST_NAMES if hasattr(settings, "NAMEGEN_FIRST_NAMES") else []
)
custom_last_names = settings.NAMEGEN_LAST_NAMES if hasattr(settings, "NAMEGEN_LAST_NAMES") else []

if hasattr(settings, "NAMEGEN_FANTASY_RULES"):
    _FANTASY_NAME_STRUCTURES |= settings.NAMEGEN_FANTASY_RULES

if hasattr(settings, "NAMEGEN_REPLACE_LISTS") and settings.NAMEGEN_REPLACE_LISTS:
    _FIRSTNAME_LIST = custom_first_names or _FIRSTNAME_LIST
    _SURNAME_LIST = custom_last_names or _SURNAME_LIST

else:
    _FIRSTNAME_LIST += custom_first_names
    _SURNAME_LIST += custom_last_names

def fantasy_name(num=1, style="harsh", return_list=False):
    """
    Generate made-up names in one of a number of "styles".

    Keyword args:
        num (int)      - How many names to return.
        style (string) - The "style" of name. This references an existing algorithm.
        return_list (bool) - Whether to always return a list. `False` by default,
                which returns a string if there is only one value and a list if more.
    """

    def _validate(style_name):
        if style_name not in _FANTASY_NAME_STRUCTURES:
                f"Invalid style name: '{style_name}'. Available style names: {' '.join(_FANTASY_NAME_STRUCTURES.keys())}"
            )
        style_dict = _FANTASY_NAME_STRUCTURES[style_name]

        if type(style_dict) is not dict:
            raise ValueError(f"Style {style_name} must be a dictionary.")

        keys = set(style_dict.keys())
        missing_keys = _REQUIRED_KEYS - keys
        if len(missing_keys):
                f"Style dictionary {style_name} is missing required keys: {' '.join(missing_keys)}"
            )

        if not (type(style_dict["consonants"]) is list and type(style_dict["vowels"]) is list):
            raise TypeError(f"'consonants' and 'vowels' for style {style_name} must be lists.")

        if not (is_iter(style_dict["length"]) and len(style_dict["length"]) == 2):
                f"'length' key for {style_name} must have a minimum and maximum number of syllables."
            )

        return style_dict

    # validate num first
    num = int(num)
    if num < 1:
        raise ValueError("Number of names to generate must be positive.")

    style_dict = _validate(style)

    syllable = []
    weight = 8
    # parse out the syllable structure with weights
    for key in style_dict["syllable"]:
        # parentheses mean optional - allow nested parens
        if key == "(":
            weight = weight / 2
        elif key == ")":
            weight = weight * 2
        else:
            if key == "C":
                sound_type = "consonants"
            elif key == "V":
                sound_type = "vowels"
            else:
                sound_type = key
            # append the sound type and weight
            syllable.append((sound_type, int(weight)))

    name_list = []

    # time to generate a name!
    for n in range(num):
        # build a list of syllables
        length = random.randint(*style_dict["length"])
        name = ""
        for i in range(length):
            # build the syllable itself
            syll = ""
            for sound, weight in syllable:
                # random chance to skip this key; lower weights mean less likely
                if random.randint(0, 8) > weight:
                    continue

                if sound not in style_dict:
                    # extra character, like apostrophes
                    syll += sound
                    continue

                # get a random sound from the sound list
                choices = list(style_dict[sound])

                if sound == "consonants":
                    # if it's a starting consonant, add starting-sounds to the options
                    if not len(syll):
                        choices += style_dict.get("start", [])
                    # if it's an ending consonant, add ending-sounds to the options
                    elif i + 1 == length:
                        choices += style_dict.get("end", [])

                syll += random.choice(choices)

            name += syll

        # condense repeating letters down to a maximum of 2
        name = _RE_DOUBLES.sub(lambda m: m.group(1) * 2, name)
        # capitalize the first letter
        name = name[0].upper() + name[1:] if len(name) > 1 else name.upper()
        name_list.append(name)

    if len(name_list) == 1 and not return_list:
        return name_list[0]
    return name_list

def first_name(
    num=1,
    gender=None,
    return_list=False,
):
    """
    Generate first names, also known as personal names.

        num (int)    - How many names to return.
        gender (str) - Restrict names by gender association. `None` by default, which selects from
                all possible names. Set to "m" for masculine, "f" for feminine, "mf" for androgynous
    """
    if num < 1:

    if gender:
        # filter the options by gender
        name_options = [
            name_data[0]
            for name_data in _FIRSTNAME_LIST
            if all([gender_key in gender for gender_key in name_data[1]])
        ]
        if not len(name_options):
            raise ValueError(f"Invalid gender '{gender}'.")
    else:
        name_options = [name_data[0] for name_data in _FIRSTNAME_LIST]

    # take a random selection of `num` names, without repeats
    results = random.sample(name_options, num)

    if len(results) == 1 and not return_list:
        # return single value as a string
        return results[0]

    return results

def last_name(num=1, return_list=False):
    """
    Generate family names, also known as surnames or last names.

    """
    if num < 1:

    results = random.sample(_SURNAME_LIST, num)

def full_name(num=1, parts=2, gender=None, return_list=False, surname_first=False):
    """
    Generate complete names with a personal name, family name, and optionally middle names.

        parts (int)  - How many parts the name should have. By default two: first and last.
        surname_first (bool) - Default `False`. Set to `True` if you want the family name to be
                placed at the beginning of the name instead of the end.
    """
    if num < 1:
    # validate parts next
    parts = int(parts)
    if parts < 2:
        raise ValueError("Number of name parts to generate must be at least 2.")

    name_lists = []

    middle = parts - 2
    if middle:
        # calculate "middle" names.
        # we want them to be an intelligent mix of personal names and family names
        # first, split the total number of middle-name parts into "personal" and "family" at a random point
        total_mids = middle * num
        personals = random.randint(1, total_mids)
        familys = total_mids - personals
        # then get the names for each
        personal_mids = first_name(num=personals, gender=gender, return_list=True)
        family_mids = last_name(num=familys, return_list=True) if familys else []
        # splice them together according to surname_first....
        middle_names = family_mids + personal_mids if surname_first else personal_mids + family_mids
        # ...and then split into `num`-length lists to be used for the final names
        name_lists = [middle_names[num * i : num * (i + 1)] for i in range(0, middle)]

    # get personal and family names
    personal_names = first_name(num=num, gender=gender, return_list=True)
    last_names = last_name(num=num, return_list=True)

    # attach personal/family names to the list of name lists, according to surname_first
    if surname_first:
        name_lists = [last_names] + name_lists + [personal_names]
    else:
        name_lists = [personal_names] + name_lists + [last_names]

    # lastly, zip them all up and join them together
    names = list(zip(*name_lists))
    names = [" ".join(name) for name in names]

    if len(names) == 1 and not return_list:
        return names[0]

    return names

"""
Tests for the Random Name Generator
"""

from evennia.contrib.utils.name_generator import namegen

_INVALID_STYLES = {
    "missing_keys": {
        "consonants": ["c", "d"],
        "length": (1, 2),
    },
    "invalid_vowels": {
        "syllable": "CVC",
        "vowels": "aeiou",
    },
    "invalid_length": {
        "vowels": ["a", "e"],
        "length": 2,
    },
}

namegen._FANTASY_NAME_STRUCTURES |= _INVALID_STYLES

class TestNameGenerator(BaseEvenniaTest):
    def test_fantasy_name(self):
        """
        Verify output types and lengths.

        fantasy_name()       - str
        fantasy_name(style="fluid") - str
        fantasy_name(num=3)  - list of length 3
        fantasy_name(return_list=True) - list of length 1

        raises KeyError on missing style or ValueError on num
        """
        single_name = namegen.fantasy_name()
        self.assertEqual(type(single_name), str)

        fluid_name = namegen.fantasy_name(style="fluid")
        self.assertEqual(type(fluid_name), str)

        three_names = namegen.fantasy_name(num=3)
        self.assertEqual(type(three_names), list)
        self.assertEqual(len(three_names), 3)

        single_list = namegen.fantasy_name(return_list=True)
        self.assertEqual(type(single_list), list)
        self.assertEqual(len(single_list), 1)

            namegen.fantasy_name(num=-1)

            namegen.fantasy_name(style="dummy")

    def test_structure_validation(self):
        """
        Verify that validation raises the correct errors for invalid inputs.
        """
            namegen.fantasy_name(style="missing_keys")

            namegen.fantasy_name(style="invalid_vowels")

            namegen.fantasy_name(style="invalid_length")

    def test_first_name(self):
        """

        first_name()       - str
        first_name(num=3)  - list of length 3
        first_name(gender='f') - str
        first_name(return_list=True) - list of length 1
        """
        single_name = namegen.first_name()

        three_names = namegen.first_name(num=3)

        gendered_name = namegen.first_name(gender="f")
        self.assertEqual(type(gendered_name), str)

        single_list = namegen.first_name(return_list=True)

            namegen.first_name(gender="x")

            namegen.first_name(num=-1)

    def test_last_name(self):
        """

        last_name()       - str
        last_name(num=3)  - list of length 3
        last_name(return_list=True) - list of length 1
        """
        single_name = namegen.last_name()

        three_names = namegen.last_name(num=3)

        single_list = namegen.last_name(return_list=True)

            namegen.last_name(num=-1)

    def test_full_name(self):
        """

        full_name()       - str
        full_name(num=3)  - list of length 3
        full_name(gender='f') - str
        full_name(return_list=True) - list of length 1
        """
        single_name = namegen.full_name()

        three_names = namegen.full_name(num=3)

        gendered_name = namegen.full_name(gender="f")

        single_list = namegen.full_name(return_list=True)

        parts_name = namegen.full_name(parts=4)
        # a name made of 4 parts must have at least 3 spaces, but may have more
        parts = parts_name.split(" ")
        self.assertGreaterEqual(len(parts), 3)

            namegen.full_name(parts=1)

            namegen.full_name(num=-1)

"""
Pseudo-random generator and registry

Evennia contribution - Vincent Le Goff 2017

This contrib can be used to generate pseudo-random strings of information
with specific criteria.  You could, for instance, use it to generate
phone numbers, license plate numbers, validation codes, non-sensivite
passwords and so on.  The strings generated by the generator will be
stored and won't be available again in order to avoid repetition.
Here's a very simple example:

```python
from evennia.contrib.utils.random_string_generator import RandomStringGenerator
# Create a generator for phone numbers
phone_generator = RandomStringGenerator("phone number", r"555-[0-9]{3}-[0-9]{4}")
# Generate a phone number (555-XXX-XXXX with X as numbers)
number = phone_generator.get()
# `number` will contain something like: "555-981-2207"
# If you call `phone_generator.get`, it won't give the same anymore.phone_generator.all()
# Will return a list of all currently-used phone numbers
phone_generator.remove("555-981-2207")
# The number can be generated again
```

To use it, you will need to:

1. Import the `RandomStringGenerator` class from the contrib.
2. Create an instance of this class taking two arguments:
   - The name of the gemerator (like "phone number", "license plate"...).
   - The regular expression representing the expected results.
3. Use the generator's `all`, `get` and `remove` methods as shown above.

To understand how to read and create regular expressions, you can refer to
[the documentation on the re module](https://docs.python.org/2/library/re.html).
Some examples of regular expressions you could use:

- `r"555-\d{3}-\d{4}"`: 555, a dash, 3 digits, another dash, 4 digits.
- `r"[0-9]{3}[A-Z][0-9]{3}"`: 3 digits, a capital letter, 3 digits.
- `r"[A-Za-z0-9]{8,15}"`: between 8 and 15 letters and digits.
- ...

Behind the scenes, a script is created to store the generated information
for a single generator.  The `RandomStringGenerator` object will also
read the regular expression you give to it to see what information is
required (letters, digits, a more restricted class, simple characters...)...
More complex regular expressions (with branches for instance) might not be
available.

"""

import re
from random import choice, randint, seed

from evennia import DefaultScript, ScriptDB

class RejectedRegex(RuntimeError):

    """The provided regular expression has been rejected.

    More details regarding why this error occurred will be provided in
    the message.  The usual reason is the provided regular expression is
    not specific enough and could lead to inconsistent generating.

    """

    pass

class ExhaustedGenerator(RuntimeError):

    """The generator hasn't any available strings to generate anymore."""

    pass

class RandomStringGeneratorScript(DefaultScript):

    """
    The global script to hold all generators.

    It will be automatically created the first time `generate` is called
    on a RandomStringGenerator object.

    """

        self.key = "generator_script"
        self.desc = "Global generator script"

        self.db.generated = {}

class RandomStringGenerator:

    """
    A generator class to generate pseudo-random strings with a rule.

    The "rule" defining what the generator should provide in terms of
    string is given as a regular expression when creating instances of
    this class.  You can use the `all` method to get all generated strings,
    the `get` method to generate a new string, the `remove` method
    to remove a generated string, or the `clear` method to remove all
    generated strings.

    Bear in mind, however, that while the generated strings will be
    stored to avoid repetition, the generator will not concern itself
    with how the string is stored on the object you use.  You probably
    want to create a tag to mark this object.  This is outside of the scope
    of this class.

    """

    # We keep the script as a class variable to optimize querying
    # with multiple instandces

    def __init__(self, name, regex):
        """
        Create a new generator.

        Args:
            name (str): name of the generator to create.
            regex (str): regular expression describing the generator.

        Notes:
            `name` should be an explicit name.  If you use more than one
            generator in your game, be sure to give them different names.
            This name will be used to store the generated information
            in the global script, and in case of errors.

            The regular expression should describe the generator, what
            it should generate: a phone number, a license plate, a password
            or something else.  Regular expressions allow you to use
            pretty advanced criteria, but be aware that some regular
            expressions will be rejected if not specific enough.

        Raises:
            RejectedRegex: the provided regular expression couldn't be
            accepted as a valid generator description.

        """
        self.elements = []
        self.total = 1

        # Analyze the regex if any
        if regex:
            self._find_elements(regex)

        return (
            "<evennia.contrib.utils.random_string_generator.RandomStringGenerator for {}>".format(
                self.name
            )
        )

    def _get_script(self):
        """Get or create the script."""
        if type(self).script:
            return type(self).script

        try:
            script = ScriptDB.objects.get(db_key="generator_script")
        except ScriptDB.DoesNotExist:
            script = create_script(
                "evennia.contrib.utils.random_string_generator.RandomStringGeneratorScript"
            )

        type(self).script = script
        return script

    def _find_elements(self, regex):
        """
        Find the elements described in the regular expression.  This will
        analyze the provided regular expression and try to find elements.

        Args:
            regex (str): the regular expression.

        """
        try:
            # python 3.11
            regex_parser = re._parser
            # python <3.11
            regex_parser = re.sre_parse

        tree = regex_parser.parse(regex).data  # note - sre_parse removed in py3.11
        # `tree` contains a list of elements in the regular expression
        for element in tree:
            # `element` is also a list, the first element is a string
            name = str(element[0]).lower()
            desc = {"min": 1, "max": 1}

            # If `.`, break here
            if name == "any":
                raise RejectedRegex(
                    "the . definition is too broad, specify what you need more precisely"
                )
            elif name == "at":
                # Either the beginning or end, we ignore it
                continue
            elif name == "min_repeat":
                raise RejectedRegex("you have to provide a maximum number of this character class")
            elif name == "max_repeat":
                desc["min"] = element[1][0]
                desc["max"] = element[1][1]
                desc["chars"] = self._find_literal(element[1][2][0])
            elif name == "in":
                desc["chars"] = self._find_literal(element)
            elif name == "literal":
            else:
                raise RejectedRegex("unhandled regex syntax:: {}".format(repr(name)))

            self.elements.append(desc)
            self.total *= len(desc["chars"]) ** desc["max"]

    def _find_literal(self, element):
        """Find the literal corresponding to a piece of regular expression."""
        name = str(element[0]).lower()
        chars = []
        if name == "literal":
            chars.append(chr(element[1]))
        elif name == "in":
            negate = False
            if element[1][0][0] == "negate":
                negate = True
                chars = list(string.ascii_letters + string.digits)

            for part in element[1]:
                if part[0] == "negate":
                    continue

                sublist = self._find_literal(part)
                for char in sublist:
                    if negate:
                        if char in chars:
                            chars.remove(char)
                    else:
                        chars.append(char)
        elif name == "range":
            chars = [chr(i) for i in range(element[1][0], element[1][1] + 1)]
        elif name == "category":
            category = str(element[1]).lower()
            if category == "category_digit":
                chars = list(string.digits)
            elif category == "category_word":
                chars = list(string.letters)
            else:
                raise RejectedRegex("unknown category: {}".format(category))
        else:
            raise RejectedRegex("cannot find the literal: {}".format(element[0]))

        return chars

        """
        Return all generated strings for this generator.

        Returns:
            strings (list of strr): the list of strings that are already
            used.  The strings that were generated first come first in the list.

        """
        script = self._get_script()
        generated = list(script.db.generated.get(self.name, []))
        return generated

    def get(self, store=True, unique=True):
        """
        Generate a pseudo-random string according to the regular expression.

        Args:
            store (bool, optional): store the generated string in the script.
            unique (bool, optional): keep on trying if the string is already used.

        Returns:
            The newly-generated string.

        Raises:
            ExhaustedGenerator: if there's no available string in this generator.

        Note:
            Unless asked explicitly, the returned string can't repeat itself.

        """
        generated = script.db.generated.get(self.name)
        if generated is None:
            script.db.generated[self.name] = []
            generated = script.db.generated[self.name]

        if len(generated) >= self.total:
            raise ExhaustedGenerator

        # Generate a pseudo-random string that might be used already
        result = ""
        for element in self.elements:
            number = randint(element["min"], element["max"])
            chars = element["chars"]
            for index in range(number):
                char = choice(chars)
                result += char

        # If the string has already been generated, try again
        if result in generated and unique:
            # Change the random seed, incrementing it slowly
            epoch = time.time()
            while result in generated:
                epoch += 1
                seed(epoch)
                result = self.get(store=False, unique=False)

        if store:
            generated.append(result)

    def remove(self, element):
        """
        Remove a generated string from the list of stored strings.

        Args:
            element (str): the string to remove from the list of generated strings.

        Raises:
            ValueError: the specified value hasn't been generated and is not present.

        Note:
            The specified string has to be present in the script (so
            has to have been generated).  It will remove this entry
            from the script, so this string could be generated again by
            calling the `get` method.

        """
        generated = script.db.generated.get(self.name, [])
        if element not in generated:
                "the string {} isn't stored as generated by the generator {}".format(
                    element, self.name
                )
            )

        generated.remove(element)

        """
        Clear the generator of all generated strings.

        """
        generated[:] = []

"""
Random string tests.

"""

from . import random_string_generator

SIMPLE_GENERATOR = random_string_generator.RandomStringGenerator("simple", "[01]{2}")

class TestRandomStringGenerator(BaseEvenniaTest):
    def test_generate(self):
        """Generate and fail when exhausted."""
        generated = []
        for i in range(4):
            generated.append(SIMPLE_GENERATOR.get())

        generated.sort()
        self.assertEqual(generated, ["00", "01", "10", "11"])

        # At this point, we have generated 4 strings.
        # We can't generate one more
        with self.assertRaises(random_string_generator.ExhaustedGenerator):
            SIMPLE_GENERATOR.get()

"""
Test tree select

"""

from evennia.contrib.utils.fieldfill import fieldfill

from . import tree_select

TREE_MENU_TESTSTR = """Foo
Bar
-Baz
--Baz 1
--Baz 2
-Qux"""

class TestTreeSelectFunc(BaseEvenniaTest):
    def test_tree_functions(self):
        # Dash counter
        self.assertTrue(tree_select.dashcount("--test") == 2)
        # Is category
        self.assertTrue(tree_select.is_category(TREE_MENU_TESTSTR, 1) is True)
        # Parse options
        self.assertTrue(
            tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2)
            == [(3, "Baz 1"), (4, "Baz 2")]
        )
        # Index to selection
        self.assertTrue(tree_select.index_to_selection(TREE_MENU_TESTSTR, 2) == "Baz")
        # Go up one category
        self.assertTrue(tree_select.go_up_one_category(TREE_MENU_TESTSTR, 4) == 2)
        # Option list to menu options
        test_optlist = tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2)
        optlist_to_menu_expected_result = [
            {"goto": ["menunode_treeselect", {"newindex": 3}], "key": "Baz 1"},
            {"goto": ["menunode_treeselect", {"newindex": 4}], "key": "Baz 2"},
            {
                "goto": ["menunode_treeselect", {"newindex": 1}],
                "key": ["<< Go Back", "go back", "back"],
                "desc": "Return to the previous menu.",
            },
        ]
            tree_select.optlist_to_menuoptions(TREE_MENU_TESTSTR, test_optlist, 2, True, True)
            == optlist_to_menu_expected_result
        )

FIELD_TEST_TEMPLATE = [
    {"fieldname": "TextTest", "fieldtype": "text"},
    {"fieldname": "NumberTest", "fieldtype": "number", "blankmsg": "Number here!"},
    {"fieldname": "DefaultText", "fieldtype": "text", "default": "Test"},
    {"fieldname": "DefaultNum", "fieldtype": "number", "default": 3},
]

FIELD_TEST_DATA = {"TextTest": None, "NumberTest": None, "DefaultText": "Test", "DefaultNum": 3}

class TestFieldFillFunc(BaseEvenniaTest):
    def test_field_functions(self):
        self.assertTrue(fieldfill.form_template_to_dict(FIELD_TEST_TEMPLATE) == FIELD_TEST_DATA)

"""
Easy menu selection tree

This module allows you to create and initialize an entire branching EvMenu
instance with nothing but a multi-line string passed to one function.

EvMenu is incredibly powerful and flexible, but using it for simple menus
can often be fairly cumbersome - a simple menu that can branch into five
categories would require six nodes, each with options represented as a list
of dictionaries.

This module provides a function, init_tree_selection, which acts as a frontend
for EvMenu, dynamically sourcing the options from a multi-line string you provide.
For example, if you define a string as such:

    TEST_MENU = '''Foo
    Bar
    Baz
    Qux'''

And then use TEST_MENU as the 'treestr' source when you call init_tree_selection
on a player:

    init_tree_selection(TEST_MENU, caller, callback)

The player will be presented with an EvMenu, like so:

    ___________________________

    Make your selection:

    Foo
    Bar
    Baz
    Qux

Making a selection will pass the selection's key to the specified callback as a
string along with the caller, as well as the index of the selection (the line number
on the source string) along with the source string for the tree itself.

In addition to specifying selections on the menu, you can also specify categories.
Categories are indicated by putting options below it preceded with a '-' character.
If a selection is a category, then choosing it will bring up a new menu node, prompting
the player to select between those options, or to go back to the previous menu. In
addition, categories are marked by default with a '[+]' at the end of their key. Both
this marker and the option to go back can be disabled.

Categories can be nested in other categories as well - just go another '-' deeper. You
can do this as many times as you like. There's no hard limit to the number of
categories you can go down.

For example, let's add some more options to our menu, turning 'Bar' into a category.

    Bar
    -You've got to know
    --When to hold em
    --When to fold em
    --When to walk away
    Baz
    Qux'''

Now when we call the menu, we can see that 'Bar' has become a category instead of a
selectable option.

    _______________________________

    Foo
    Bar [+]
    Baz
    Qux

Note the [+] next to 'Bar'. If we select 'Bar', it'll show us the option listed under it.

    ________________________________________________________________

    Bar

    You've got to know [+]
    << Go Back: Return to the previous menu.

Just the one option, which is a category itself, and the option to go back, which will
take us back to the previous menu. Let's select 'You've got to know'.

    You've got to know

    When to hold em
    When to fold em
    When to walk away

Now we see the three options listed under it, too. We can select one of them or use 'Go
Back' to return to the 'Bar' menu we were just at before. It's very simple to make a
branching tree of selections!

One last thing - you can set the descriptions for the various options simply by adding a
':' character followed by the description to the option's line. For example, let's add a
description to 'Baz' in our menu:

    Bar
    Baz: Look at this one: the best option.
    Qux'''

Now we see that the Baz option has a description attached that's separate from its key:

    _______________________________________________________________

    Foo
    Bar [+]
    Qux

Once the player makes a selection - let's say, 'Foo' - the menu will terminate and call
your specified callback with the selection, like so:

    callback(caller, TEST_MENU, 0, "Foo")

The index of the selection is given along with a string containing the selection's key.
That way, if you have two selections in the menu with the same key, you can still
differentiate between them.

And that's all there is to it! For simple branching-tree selections, using this system is
much easier than manually creating EvMenu nodes. It also makes generating menus with dynamic
options much easier - since the source of the menu tree is just a string, you could easily
generate that string procedurally before passing it to the init_tree_selection function.
For example, if a player casts a spell or does an attack without specifying a target, instead
of giving them an error, you could present them with a list of valid targets to select by
generating a multi-line string of targets and passing it to init_tree_selection, with the
callable performing the maneuver once a selection is made.

This selection system only works for simple branching trees - doing anything really complicated
like jumping between categories or prompting for arbitrary input would still require a full
EvMenu implementation. For simple selections, however, I'm sure you will find using this function
to be much easier!

Included in this module is a sample menu and function which will let a player change the color
of their name - feel free to mess with it to get a feel for how this system works by importing
this module in your game's default_cmdsets.py module and adding CmdNameColor to your default
character's command set.

"""

from evennia.utils import evmenu

def init_tree_selection(
    treestr,
    caller,
    callback,
    index=None,
    mark_category=True,
    go_back=True,
    cmd_on_exit="look",
    start_text="Make your selection:",
):
    """
    Prompts a player to select an option from a menu tree given as a multi-line string.

    Args:
        treestr (str): Multi-lne string representing menu options
        caller (obj): Player to initialize the menu for
        callback (callable): Function to run when a selection is made. Must take 4 args:
            caller (obj): Caller given above
            treestr (str): Menu tree string given above
            index (int): Index of final selection
            selection (str): Key of final selection

    Options:
        index (int or None): Index to start the menu at, or None for top level
        mark_category (bool): If True, marks categories with a [+] symbol in the menu
        go_back (bool): If True, present an option to go back to previous categories
        start_text (str): Text to display at the top level of the menu
        cmd_on_exit(str): Command to enter when the menu exits - 'look' by default

    Notes:
        This function will initialize an instance of EvMenu with options generated
        dynamically from the source string, and passes the menu user's selection to
        a function of your choosing. The EvMenu is made of a single, repeating node,
        which will call itself over and over at different levels of the menu tree as
        categories are selected.

        Once a non-category selection is made, the user's selection will be passed to
        the given callable, both as a string and as an index number. The index is given
        to ensure every selection has a unique identifier, so that selections with the
        same key in different categories can be distinguished between.

        The menus called by this function are not persistent and cannot perform
        complicated tasks like prompt for arbitrary input or jump multiple category
        levels at once - you'll have to use EvMenu itself if you want to take full
        advantage of its features.
    """

    kwargs = {
        "index": index,
        "mark_category": mark_category,
        "go_back": go_back,
        "treestr": treestr,
        "callback": callback,
        "start_text": start_text,
    }

    evmenu.EvMenu(
        caller,
        "evennia.contrib.utils.tree_select",
        startnode="menunode_treeselect",
        startnode_input=None,
        cmd_on_exit=cmd_on_exit,
        **kwargs,
    )

def dashcount(entry):
    """
    Counts the number of dashes at the beginning of a string. This
    is needed to determine the depth of options in categories.

    Args:
        entry (str): String to count the dashes at the start of

    Returns:
        dashes (int): Number of dashes at the start
    """
    dashes = 0
    for char in entry:
        if char == "-":
            dashes += 1
        else:
            return dashes
    return dashes

def is_category(treestr, index):
    """
    Determines whether an option in a tree string is a category by
    whether or not there are additional options below it.

    Args:
        treestr (str): Multi-line string representing menu options
        index (int): Which line of the string to test

    Returns:
        is_category (bool): Whether the option is a category
    """
    opt_list = treestr.split("\n")
    # Not a category if it's the last one in the list
    if index == len(opt_list) - 1:
    # Not a category if next option is not one level deeper
    return not bool(dashcount(opt_list[index + 1]) != dashcount(opt_list[index]) + 1)

def parse_opts(treestr, category_index=None):
    """
    Parses a tree string and given index into a list of options. If
    category_index is none, returns all the options at the top level of
    the menu. If category_index corresponds to a category, returns a list
    of options under that category. If category_index corresponds to
    an option that is not a category, it's a selection and returns True.

    Args:
        category_index (int): Index of category or None for top level

    Returns:
        kept_opts (list or True): Either a list of options in the selected
                                  category or True if a selection was made
    """
    dash_depth = 0
    kept_opts = []

    # If a category index is given
    if category_index != None:
        # If given index is not a category, it's a selection - return True.
        if not is_category(treestr, category_index):
        # Otherwise, change the dash depth to match the new category.
        dash_depth = dashcount(opt_list[category_index]) + 1
        # Delete everything before the category index
        opt_list = opt_list[category_index + 1 :]

    # Keep every option (referenced by index) at the appropriate depth
    cur_index = 0
    for option in opt_list:
        if dashcount(option) == dash_depth:
            if category_index == None:
                kept_opts.append((cur_index, option[dash_depth:]))
            else:
                kept_opts.append((cur_index + category_index + 1, option[dash_depth:]))
        # Exits the loop if leaving a category
        if dashcount(option) < dash_depth:
            return kept_opts
        cur_index += 1
    return kept_opts

def index_to_selection(treestr, index, desc=False):
    """
    Given a menu tree string and an index, returns the corresponding selection's
    name as a string. If 'desc' is set to True, will return the selection's
    description as a string instead.

    Args:
        index (int): Index to convert to selection key or description

    Options:
        desc (bool): If true, returns description instead of key

    Returns:
        selection (str): Selection key or description if 'desc' is set
    """
    # Fetch the given line
    selection = opt_list[index]
    # Strip out the dashes at the start
    selection = selection[dashcount(selection) :]
    # Separate out description, if any
    if ":" in selection:
        # Split string into key and description
        selection = selection.split(":", 1)
        selection[1] = selection[1].strip(" ")
    else:
        # If no description given, set description to None
        selection = [selection, None]
    if not desc:
        return selection[0]
    else:
        return selection[1]

def go_up_one_category(treestr, index):
    """
    Given a menu tree string and an index, returns the category that the given option
    belongs to. Used for the 'go back' option.

    Args:
        index (int): Index to determine the parent category of

    Returns:
        parent_category (int): Index of parent category
    """
    # Get the number of dashes deep the given index is
    dash_level = dashcount(opt_list[index])
    # Delete everything after the current index
    opt_list = opt_list[: index + 1]

    # If there's no dash, return 'None' to return to base menu
    if dash_level == 0:
    current_index = index
    # Go up through each option until we find one that's one category above
    for selection in reversed(opt_list):
        if dashcount(selection) == dash_level - 1:
            return current_index
        current_index -= 1

def optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back):
    """
    Takes a list of options processed by parse_opts and turns it into
    a list/dictionary of menu options for use in menunode_treeselect.

    Args:
        optlist (list): List of options to convert to EvMenu's option format
        index (int): Index of current category
        mark_category (bool): Whether or not to mark categories with [+]
        go_back (bool): Whether or not to add an option to go back in the menu

    Returns:
        menuoptions (list of dicts): List of menu options formatted for use
            in EvMenu, each passing a different "newindex" kwarg that changes
            the menu level or makes a selection
    """

    menuoptions = []
    for option in optlist:
        index_to_add = optlist[cur_index][0]
        menuitem = {}
        keystr = index_to_selection(treestr, index_to_add)
        if mark_category and is_category(treestr, index_to_add):
            # Add the [+] to the key if marking categories, and the key by itself as an alias
            menuitem["key"] = [keystr + " [+]", keystr]
        else:
            menuitem["key"] = keystr
        # Get the option's description
        desc = index_to_selection(treestr, index_to_add, desc=True)
        if desc:
            menuitem["desc"] = desc
        # Passing 'newindex' as a kwarg to the node is how we move through the menu!
        menuitem["goto"] = ["menunode_treeselect", {"newindex": index_to_add}]
        menuoptions.append(menuitem)
    # Add option to go back, if needed
    if index != None and go_back == True:
        gobackitem = {
            "key": ["<< Go Back", "go back", "back"],
            "desc": "Return to the previous menu.",
            "goto": ["menunode_treeselect", {"newindex": go_up_one_category(treestr, index)}],
        }
        menuoptions.append(gobackitem)
    return menuoptions

def menunode_treeselect(caller, raw_string, **kwargs):
    """
    This is the repeating menu node that handles the tree selection.
    """

    # If 'newindex' is in the kwargs, change the stored index.
    if "newindex" in kwargs:
        caller.ndb._menutree.index = kwargs["newindex"]

    # Retrieve menu info
    index = caller.ndb._menutree.index
    mark_category = caller.ndb._menutree.mark_category
    go_back = caller.ndb._menutree.go_back
    treestr = caller.ndb._menutree.treestr
    callback = caller.ndb._menutree.callback
    start_text = caller.ndb._menutree.start_text

    # List of options if index is 'None' or category, or 'True' if a selection
    optlist = parse_opts(treestr, category_index=index)

    # If given index returns optlist as 'True', it's a selection. Pass to callback and end the menu.
    if optlist == True:
        selection = index_to_selection(treestr, index)
        try:
            callback(caller, treestr, index, selection)
            log_trace("Error in tree selection callback.")

        # Returning None, None ends the menu.

    # Otherwise, convert optlist to a list of menu options.
    else:
        options = optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back)
        if index == None:
            # Use start_text for the menu text on the top level
            text = start_text
        else:
            # Use the category name and description (if any) as the menu text
            if index_to_selection(treestr, index, desc=True) != None:
                text = (
                    "|w"
                    + index_to_selection(treestr, index)
                    + "|n: "
                    + index_to_selection(treestr, index, desc=True)
                )
            else:
                text = "|w" + index_to_selection(treestr, index) + "|n"
        return text, options

# The rest of this module is for the example menu and command! It'll change the color of your name.

"""
Here's an example string that you can initialize a menu from. Note the dashes at
the beginning of each line - that's how menu option depth and hierarchy is determined.
"""

NAMECOLOR_MENU = """Set name color: Choose a color for your name!
-Red shades: Various shades of |511red|n
--Red: |511Set your name to Red|n
--Pink: |533Set your name to Pink|n
--Maroon: |301Set your name to Maroon|n
-Orange shades: Various shades of |531orange|n
--Orange: |531Set your name to Orange|n
--Brown: |321Set your name to Brown|n
--Sienna: |420Set your name to Sienna|n
-Yellow shades: Various shades of |551yellow|n
--Yellow: |551Set your name to Yellow|n
--Gold: |540Set your name to Gold|n
--Dandelion: |553Set your name to Dandelion|n
-Green shades: Various shades of |141green|n
--Green: |141Set your name to Green|n
--Lime: |350Set your name to Lime|n
--Forest: |032Set your name to Forest|n
-Blue shades: Various shades of |115blue|n
--Blue: |115Set your name to Blue|n
--Cyan: |155Set your name to Cyan|n
--Navy: |113Set your name to Navy|n
-Purple shades: Various shades of |415purple|n
--Purple: |415Set your name to Purple|n
--Lavender: |535Set your name to Lavender|n
--Fuchsia: |503Set your name to Fuchsia|n
Remove name color: Remove your name color, if any"""

class CmdNameColor(Command):
    """
    Set or remove a special color on your name. Just an example for the
    easy menu selection tree contrib.
    """

    key = "namecolor"

        # This is all you have to do to initialize a menu!
        init_tree_selection(
            NAMECOLOR_MENU, self.caller, change_name_color, start_text="Name color options:"
        )

def change_name_color(caller, treestr, index, selection):
    """
    Changes a player's name color.

    Args:
        caller (obj): Character whose name to color.
        treestr (str): String for the color change menu - unused
        index (int): Index of menu selection - unused
        selection (str): Selection made from the name color menu - used
            to determine the color the player chose.
    """

    # Store the caller's uncolored name
    if not caller.db.uncolored_name:
        caller.db.uncolored_name = caller.key

    # Dictionary matching color selection names to color codes
    colordict = {
        "Red": "|511",
        "Pink": "|533",
        "Maroon": "|301",
        "Orange": "|531",
        "Brown": "|321",
        "Sienna": "|420",
        "Yellow": "|551",
        "Gold": "|540",
        "Dandelion": "|553",
        "Green": "|141",
        "Lime": "|350",
        "Forest": "|032",
        "Blue": "|115",
        "Cyan": "|155",
        "Navy": "|113",
        "Purple": "|415",
        "Lavender": "|535",
        "Fuchsia": "|503",
    }

    # I know this probably isn't the best way to do this. It's just an example!
    if selection == "Remove name color":  # Player chose to remove their name color
        caller.key = caller.db.uncolored_name
        caller.msg("Name color removed.")
    elif selection in colordict:
        newcolor = colordict[selection]  # Retrieve color code based on menu selection
        caller.key = newcolor + caller.db.uncolored_name + "|n"  # Add color code to caller's name
        caller.msg(newcolor + ("Name color changed to %s!" % selection) + "|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 module 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>,
     'text': <str>,
     'category': <str>,   # optional, otherwise settings.DEFAULT_HELP_CATEGORY
     'aliases': <list>,   # optional
     'locks': <str>}      # optional, use access-type 'view'. Default is view:all()

The `text`` should be formatted on the same form as other help entry-texts and
can 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, also partial-matching ('gods') works
        "aliases": ['pantheon', 'religion'],
        "category": "Lore",
        "locks": "view:all()",   # this is optional unless restricting access
        "text": '''
            The gods formed the world ...

            # Subtopics

            ## Pantheon

            ...

            ### God of love

            ...

            ### God of war

            ...

        '''
    }

    HELP_ENTRY_DICTS = [
        help_entry1,
        ...
    ]

----

"""

    all_from_module,
)

_DEFAULT_HELP_CATEGORY = settings.DEFAULT_HELP_CATEGORY

class FileHelpEntry:
    """
    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
    lock_storage: str

    @property
        """
        Property for easily retaining a search index entry for this object.

        """
        return {
            "aliases": " ".join(self.aliases),
            "category": self.help_category,
            "locks": "",
            "text": self.entrytext,
        }

        return f"<FileHelpEntry {self.key}>"

        """

        ex.
        ::

        Returns:

        """
        try:
            )
            return "#"

        """

        Returns:

        """

    def access(self, accessing_obj, access_type="view", default=True):
        """
        Determines if another object has permission to access this help entry.

        Args:
            accessing_obj (Object or Account): Entity trying to access this one.
            access_type (str): type of access sought.
            default (bool): What to return if no lock of `access_type` was found.

        """

class FileHelpStorageHandler:
    """
    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.

    """

    def __init__(self, help_file_modules=settings.FILE_HELP_ENTRY_MODULES):
        """
        Initialize the storage.
        """
        self.help_file_modules = [str(part).strip() for part in make_iter(help_file_modules)]
        self.help_entries = []
        self.help_entries_dict = {}
        self.load()

    def load(self):
        """
        Load/reload file-based help-entries from file.

        """
        loaded_help_dicts = []

        for module_or_path in self.help_file_modules:
            help_dict_list = variable_from_module(module_or_path, variable="HELP_ENTRY_DICTS")
            if not help_dict_list:
                help_dict_list = [
                    dct for dct in all_from_module(module_or_path).values() if isinstance(dct, dict)
                ]
            if help_dict_list:
                loaded_help_dicts.extend(help_dict_list)
            else:
                logger.log_err(f"Could not find file-help module {module_or_path} (skipping).")

        # validate and parse dicts into FileEntryHelp objects and make sure they are unique-by-key
        # by letting latter added ones override earlier ones.
        unique_help_entries = {}

        for dct in loaded_help_dicts:
            key = dct.get("key").lower().strip()
            category = dct.get("category", _DEFAULT_HELP_CATEGORY).strip()
            aliases = list(dct.get("aliases", []))
            entrytext = dct.get("text", "")
            locks = dct.get("locks", "")

            if not key and entrytext:
                logger.error(f"Cannot load file-help-entry (missing key or text): {dct}")
                continue

            unique_help_entries[key] = FileHelpEntry(
                key=key,
                help_category=category,
                aliases=aliases,
                lock_storage=locks,
                entrytext=entrytext,
            )

        self.help_entries_dict = unique_help_entries
        self.help_entries = list(unique_help_entries.values())

    def all(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``.

        """
        return self.help_entries_dict if return_dict else self.help_entries

# singleton to hold the loaded help entries
FILE_HELP_ENTRIES = FileHelpStorageHandler()

"""
Custom manager for HelpEntry objects.
"""
from django.db import IntegrityError

from evennia.typeclasses.managers import TypedObjectManager

__all__ = ("HelpEntryManager",)

class HelpEntryManager(TypedObjectManager):
    """
    This HelpEntryManager implements methods for searching
    and manipulating HelpEntries directly from the database.

    These methods will all return database objects
    (or QuerySets) directly.

    Evennia-specific:
    find_topicmatch
    find_apropos
    find_topicsuggestions
    find_topics_with_category
    all_to_category
    search_help (equivalent to evennia.search_helpentry)

    """

    def find_topicmatch(self, topicstr, exact=False):
        """
        Searches for matching topics or aliases based on player's
        input.

        Args:
            topcistr (str): Help topic to search for.
            exact (bool, optional): Require exact match
                (non-case-sensitive).  If `False` (default), match
                sub-parts of the string.

        Returns:
            matches (HelpEntries): Query results.

        """
        dbref = utils.dbref(topicstr)
        if dbref:
            return self.filter(id=dbref)
        topics = self.filter(db_key__iexact=topicstr)
        if not topics:
            topics = self.get_by_alias(topicstr)
        if not topics and not exact:
            topics = self.filter(db_key__istartswith=topicstr)
            if not topics:
                topics = self.filter(db_key__icontains=topicstr)
        return topics

    def find_apropos(self, topicstr):
        """
        Do a very loose search, returning all help entries containing
        the search criterion in their titles.

        Args:
            topicstr (str): Search criterion.

        Returns:

        """
        return self.filter(db_key__icontains=topicstr)

    def find_topicsuggestions(self, topicstr):
        """
        Do a fuzzy match, preferably within the category of the
        current topic.

        Args:

        Returns:
            matches (Helpentries): Query results.

        """
        return self.filter(db_key__icontains=topicstr).exclude(db_key__iexact=topicstr)

    def find_topics_with_category(self, help_category):
        """
        Search topics having a particular category.

        Args:
            help_category (str): Category query criterion.

        Returns:

        """
        return self.filter(db_help_category__iexact=help_category)

    def get_all_topics(self):
        """
        Get all topics.

        Returns:
            all (HelpEntries): All topics.

        """

    def get_all_categories(self):
        """
        Return all defined category names with at least one topic in
        them.

        Returns:
            matches (list): Unique list of category names across all
                topics.

        """
        return list(set(topic.help_category for topic in self.all()))

    def all_to_category(self, default_category):
        """
        Shifts all help entries in database to default_category.  This
        action cannot be reverted. It is used primarily by the engine
        when importing a default help database, making sure this ends
        up in one easily separated category.

        Args:
            default_category (str): Category to move entries to.

        """
        topics = self.all()
        for topic in topics:
            topic.help_category = default_category
            topic.save()
        string = "Help database moved to category {default_category}".format(
            default_category=default_category
        )
        logger.log_info(string)

    def search_help(self, ostring, help_category=None):
        """
        Retrieve a search entry object.

        Args:
            ostring (str): The help topic to look for.
            category (str): Limit the search to a particular help topic

        Returns:
            Queryset: An iterable with 0, 1 or more matches.

        """
        ostring = ostring.strip().lower()
        if help_category:
            return self.filter(db_key__iexact=ostring, db_help_category__iexact=help_category)
        else:
            return self.filter(db_key__iexact=ostring)

    def create_help(self, key, entrytext, category="General", locks=None, aliases=None, tags=None):
        """
        Create a static help entry in the help database. Note that Command
        help entries are dynamic and directly taken from the __doc__
        entries of the command. The database-stored help entries are
        intended for more general help on the game, more extensive info,
        in-game setting information and so on.

        Args:
            key (str): The name of the help entry.
            entrytext (str): The body of te help entry
            category (str, optional): The help category of the entry.
            locks (str, optional): A lockstring to restrict access.
            aliases (list of str, optional): List of alternative (likely shorter) keynames.
            tags (lst, optional): List of tags or tuples `(tag, category)`.

        Returns:
            help (HelpEntry): A newly created help entry.

        """
        try:
            new_help = self.model()
            new_help.key = key
            new_help.entrytext = entrytext
            new_help.help_category = category
            if locks:
                new_help.locks.add(locks)
                new_help.aliases.add(make_iter(aliases))
            if tags:
                new_help.tags.batch_add(*tags)
            new_help.save()
            return new_help
        except IntegrityError:
            string = "Could not add help entry: key '%s' already exists." % key
            logger.log_err(string)

        signals.SIGNAL_HELPENTRY_POST_CREATE.send(sender=new_help)

"""
Models for the help system.

The database-tied help system is only half of Evennia's help
functionality, the other one being the auto-generated command help
that is created on the fly from each command's `__doc__` string. The
persistent database system defined here is intended for all other
forms of help that do not concern commands, like information about the
game world, policy info, rules and similar.

"""

from evennia.help.manager import HelpEntryManager
from evennia.typeclasses.models import AliasHandler, Tag, TagHandler

__all__ = ("HelpEntry",)

#
# HelpEntry
#

class HelpEntry(SharedMemoryModel):
    """
    A generic help entry.

    An HelpEntry object has the following properties defined:
      key - main name of entry
      help_category - which category entry belongs to (defaults to General)
      entrytext - the actual help text
      permissions - perm strings

    Method:
      access

    """

    #
    # HelpEntry Database Model setup
    #
    #
    # These database fields are all set using their corresponding properties,

    # title of the help entry
    db_key = models.CharField(
        "help key", max_length=255, unique=True, help_text="key to search for"
    )

    # help category
    db_help_category = models.CharField(
        "help category",
        default="General",
        help_text="organizes help entries in lists",
    )

    # the actual help entry text, in any formatting.
    db_entrytext = models.TextField(
        "help entry", blank=True, help_text="the main body of help text"
    )
    # lock string storage
    db_lock_storage = models.TextField("locks", blank=True, help_text="normally view:all().")
    # tags are primarily used for permissions
        Tag,
        help_text="tags on this object. Tags are simple string markers to "
        "identify, group and alias objects.",
    )
    # Creation date. This is not changed once the object is created.
    db_date_created = models.DateTimeField("creation date", editable=False, auto_now=True)

    objects = HelpEntryManager()

    # lazy-loaded handlers

    def aliases(self):
        return AliasHandler(self)

        verbose_name = "Help Entry"
        verbose_name_plural = "Help Entries"

    #
    #
    # HelpEntry main class methods
    #
    #
        return str(self.key)

        return f"<HelpEntry {self.key}>"

    def access(self, accessing_obj, access_type="read", default=True):
        """

        Accesses used by default:
            'read' - read the help entry itself.
            'view' - see help entry in help index.

        Args:

        """

    @property
        """
        """
        return {
            "key": self.db_key,
            "aliases": " ".join(self.aliases.all()),
            "category": self.db_help_category,
            "text": self.db_entrytext,
            "tags": " ".join(str(tag) for tag in self.tags.all()),
        }

    #
    #

        """

        Returns:

        """
        )

        """

        a named view of 'character-create' would be referenced by this method.

        ex.
        ::

            url(r'characters/create/', ChargenView.as_view(), name='character-create')

        Returns:

        """
        try:
            return "#"

        """

        ex.
        ::

        Returns:

        """

        try:
                kwargs={"category": slugify(self.db_help_category), "topic": slugify(self.db_key)},
            )
            return "#"

        """
        object.

        a named view of 'character-update' would be referenced by this method.

        ex.
        ::

            url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
                CharUpdateView.as_view(), name='character-update')

        Returns:

        """
        try:
            )
            return "#"

        """

        ex.
        ::

            url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',
                CharDeleteView.as_view(), name='character-delete')

        Returns:

        """
        try:
            )
            return "#"

"""
Resources for indexing help entries and for splitting help entries into
sub-categories.

This is used primarily by the default `help` command.

"""
import re

# 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 ignore-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",
    "get",
    "who",
    "say",
] + settings.LUNR_STOP_WORD_FILTER_EXCEPTIONS

_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

def help_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
    if not _LUNR:
        # we have to delay-load lunr because it messes with logging if it's imported
        # before twisted's logging has been set up
        from lunr import get_default_builder as _LUNR_GET_BUILDER
        from lunr import lunr as _LUNR
        from lunr import stop_word_filter
        from lunr.exceptions import QueryParseError as _LUNR_EXCEPTION
        from lunr.stemmer import stemmer

        # from lunr.trimmer import trimmer
        # 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

        for ignore_word in _LUNR_STOP_WORD_FILTER_EXCEPTIONS:
            try:
                stop_words.remove(ignore_word)
                pass

        custom_stop_words_filter = stop_word_filter.generate_stop_word_filter(stop_words)
        # _LUNR_BUILDER_PIPELINE = (trimmer, custom_stop_words_filter, stemmer)
        _LUNR_BUILDER_PIPELINE = (custom_stop_words_filter, stemmer)

    indx = [cnd.search_index_entry for cnd in candidate_entries]
    mapping = {indx[ix]["key"]: cand for ix, cand in enumerate(candidate_entries)}

    if not fields:
        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 (objs), suggestions (strs)
    return (
        [mapping[match["ref"]] for match in matches],
        [str(match["ref"]) for match in matches],  # + f" (score {match['score']})")   # good debug
    )

def parse_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")}

    if subtopics:
        subtopics = subtopics[0]
    else:
        return structure

    keypath = []
    current_nesting = 0
    subtopic = None

    # from evennia import set_trace;set_trace()
    for part in _RE_HELP_SUBTOPIC_SPLIT.split(subtopics.strip()):
        subtopic_match = _RE_HELP_SUBTOPIC_PARSE.match(part.strip())
        if subtopic_match:
            # a new sub(-sub..) category starts.
            mdict = subtopic_match.groupdict()
            subtopic = mdict["name"].lower().strip()
            new_nesting = len(mdict["nesting"]) - 1

            if new_nesting > MAX_SUBTOPIC_NESTING:
                raise RuntimeError(
                    f"Can have max {MAX_SUBTOPIC_NESTING} levels of nested help subtopics."
                )

            nestdiff = new_nesting - current_nesting
            if nestdiff < 0:
                # jumping back up in nesting
                for _ in range(abs(nestdiff) + 1):
                    try:
                        keypath.pop()
                        pass
            elif nestdiff == 0:
                # don't add a deeper nesting but replace the current
                try:
                    keypath.pop()
                    pass
            keypath.append(subtopic)
            current_nesting = new_nesting
        else:
            # an entry belonging to a subtopic - find the nested location
            dct = structure
            if not keypath and subtopic is not None:
                structure[subtopic] = part
            else:
                for key in keypath:
                    if key in dct:
                        dct = dct[key]
                    else:
                        dct[key] = {None: part}
    return structure

"""
This module provides a set of permission lock functions for use
with Evennia's permissions system.

To call these locks, make sure this module is included in the
settings tuple `PERMISSION_FUNC_MODULES` then define a lock on the form
'<access_type>:func(args)' and add it to the object's lockhandler.
Run the `access()` method of the handler to execute the lock check.

Note that `accessing_obj` and `accessed_obj` can be any object type
with a lock variable/field, so be careful to not expect
a certain object type.

"""

_PERMISSION_HIERARCHY = [pe.lower() for pe in settings.PERMISSION_HIERARCHY]
# also accept different plural forms
_PERMISSION_HIERARCHY_PLURAL = [
    pe + "s" if not pe.endswith("s") else pe for pe in _PERMISSION_HIERARCHY
]

def _to_account(accessing_obj):
    "Helper function. Makes sure an accessing object is an account object"
    if utils.inherits_from(accessing_obj, "evennia.objects.objects.DefaultObject"):
        # an object. Convert to account.
        accessing_obj = accessing_obj.account
    return accessing_obj

# lock functions

def true(*args, **kwargs):
    """
    Always returns True.

    """

def all(*args, **kwargs):

def false(*args, **kwargs):
    """
    Always returns False

    """

def none(*args, **kwargs):

def superuser(*args, **kwargs):

def self(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Check if accessing_obj is the same as accessed_obj

    Usage:
       self()

    This can be used to lock specifically only to
    the same object that the lock is defined on.
    """
    return accessing_obj == accessed_obj

def perm(accessing_obj, accessed_obj, *args, **kwargs):
    """
    The basic permission-checker. Ignores case.

    Usage:
       perm(<permission>)

    where <permission> is the permission accessing_obj must
    have in order to pass the lock.

    If the given permission is part of settings.PERMISSION_HIERARCHY,
    permission is also granted to all ranks higher up in the hierarchy.

    If accessing_object is an Object controlled by an Account, the
    permissions of the Account is used unless the Attribute _quell
    is set to True on the Object. In this case however, the
    LOWEST hieararcy-permission of the Account/Object-pair will be used
    (this is order to avoid Accounts potentially escalating their own permissions
    by use of a higher-level Object)

    For non-hierarchical permissions, a puppeted object's account is checked first,
    followed by the puppet (unless quelled, when only puppet's access is checked).

    """
    # this allows the perm_above lockfunc to make use of this function too
    try:
        permission = args[0].lower()
        perms_object = accessing_obj.permissions.all()
    except (AttributeError, IndexError):

    gtmode = kwargs.pop("_greater_than", False)
    is_quell = False

    account = (
        utils.inherits_from(accessing_obj, "evennia.objects.objects.DefaultObject")
        and accessing_obj.account
    )
    # check object perms (note that accessing_obj could be an Account too)
    perms_account = []
        perms_account = account.permissions.all()
        is_quell = account.attributes.get("_quell")

    # Check hirarchy matches; handle both singular/plural forms in hierarchy
    hpos_target = None
    if permission in _PERMISSION_HIERARCHY:
        hpos_target = _PERMISSION_HIERARCHY.index(permission)
    elif permission.endswith("s") and permission[:-1] in _PERMISSION_HIERARCHY:
        hpos_target = _PERMISSION_HIERARCHY.index(permission[:-1])
    if hpos_target is not None:
        # hieratchy match
        hpos_account = -1
        hpos_object = -1

            # we have an account puppeting this object. We must check what perms it has
            perms_account_single = [p[:-1] if p.endswith("s") else p for p in perms_account]
            hpos_account = [
                hpos
                for hpos, hperm in enumerate(_PERMISSION_HIERARCHY)
                if hperm in perms_account_single
            ]
            hpos_account = hpos_account[-1] if hpos_account else -1

        if not account or is_quell:
            # only get the object-level perms if there is no account or quelling
            perms_object_single = [p[:-1] if p.endswith("s") else p for p in perms_object]
            hpos_object = [
                hpos
                if hperm in perms_object_single
            ]
            hpos_object = hpos_object[-1] if hpos_object else -1

        if account and is_quell:
            # quell mode: use smallest perm from account and object
            if gtmode:
                return hpos_target < min(hpos_account, hpos_object)
            else:
                return hpos_target <= min(hpos_account, hpos_object)
        elif account:
            # use account perm
            if gtmode:
                return hpos_target < hpos_account
            else:
                return hpos_target <= hpos_account
        else:
            # use object perm
            if gtmode:
                return hpos_target < hpos_object
            else:
                return hpos_target <= hpos_object
    else:
        # no hierarchy match - check direct matches
            # account exists
            if is_quell and permission in perms_object:
                # if quelled, first check object
            elif permission in perms_account:
                # unquelled - check account
            else:
                # no account-pass, check object pass
                return permission in perms_object

        elif permission in perms_object:

def perm_above(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Only allow objects with a permission *higher* in the permission
    hierarchy than the one given. If there is no such higher rank,
    it's assumed we refer to superuser. If no hierarchy is defined,
    this function has no meaning and returns False.
    """
    kwargs["_greater_than"] = True
    return perm(accessing_obj, accessed_obj, *args, **kwargs)

def pperm(accessing_obj, accessed_obj, *args, **kwargs):
    """
    The basic permission-checker only for Account objects. Ignores case.

    Usage:
       pperm(<permission>)

    have in order to pass the lock. If the given permission
    is part of _PERMISSION_HIERARCHY, permission is also granted
    to all ranks higher up in the hierarchy.
    """
    return perm(_to_account(accessing_obj), accessed_obj, *args, **kwargs)

def pperm_above(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Only allow Account objects with a permission *higher* in the permission
    """
    return perm_above(_to_account(accessing_obj), accessed_obj, *args, **kwargs)

def dbref(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:
      dbref(3)

    This lock type checks if the checking object
    has a particular dbref. Note that this only
    works for checking objects that are stored
    in the database (e.g. not for commands)
    """
    try:
        dbr = int(args[0].strip().strip("#"))
    if hasattr(accessing_obj, "dbid"):
        return dbr == accessing_obj.dbid

def pdbref(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Same as dbref, but making sure accessing_obj is an account.
    """
    return dbref(_to_account(accessing_obj), accessed_obj, *args, **kwargs)

def id(accessing_obj, accessed_obj, *args, **kwargs):
    "Alias to dbref"
    return dbref(accessing_obj, accessed_obj, *args, **kwargs)

def pid(accessing_obj, accessed_obj, *args, **kwargs):
    "Alias to dbref, for Accounts"

# this is more efficient than multiple if ... elif statments
CF_MAPPING = {
    "eq": lambda val1, val2: val1 == val2 or str(val1) == str(val2) or float(val1) == float(val2),
    "gt": lambda val1, val2: float(val1) > float(val2),
    "lt": lambda val1, val2: float(val1) < float(val2),
    "ge": lambda val1, val2: float(val1) >= float(val2),
    "le": lambda val1, val2: float(val1) <= float(val2),
    "ne": lambda val1, val2: float(val1) != float(val2),
    "default": lambda val1, val2: False,
}

def attr(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:
      attr(attrname)
      attr(attrname, value)
      attr(attrname, value, compare=type)

    where compare's type is one of (eq,gt,lt,ge,le,ne) and signifies
    how the value should be compared with one on accessing_obj (so
    compare=gt means the accessing_obj must have a value greater than
    the one given).

    Searches attributes *and* properties stored on the accessing_obj.
    if accessing_obj has a property "obj", then this is used as
    accessing_obj (this makes this usable for Commands too)

    The first form works like a flag - if the attribute/property
    exists on the object, the value is checked for True/False. The
    second form also requires that the value of the attribute/property
    matches. Note that all retrieved values will be converted to
    strings before doing the comparison.
    """
    # deal with arguments
    attrname = args[0].strip()
    value = None
    if len(args) > 1:
        value = args[1].strip()
    compare = "eq"
    if kwargs:
        compare = kwargs.get("compare", "eq")

    def valcompare(val1, val2, typ="eq"):
        "compare based on type"
        try:
            return CF_MAPPING.get(typ, CF_MAPPING["default"])(val1, val2)
            # this might happen if we try to compare two things that
            # cannot be compared

    if hasattr(accessing_obj, "obj"):
        # NOTE: this is relevant for Commands. It may clash with scripts
        # (they have Attributes and .obj) , but are scripts really
        # used so that one ever wants to check the property on the
        # Script rather than on its owner?
        accessing_obj = accessing_obj.obj

    # first, look for normal properties on the object trying to gain access
    if hasattr(accessing_obj, attrname):
        if value:
            return valcompare(str(getattr(accessing_obj, attrname)), value, compare)
        # will return Fail on False value etc
        return bool(getattr(accessing_obj, attrname))
    # check attributes, if they exist
    if hasattr(accessing_obj, "attributes") and accessing_obj.attributes.has(attrname):
        if value:
            return hasattr(accessing_obj, "attributes") and valcompare(
                accessing_obj.attributes.get(attrname), value, compare
            )
        # fails on False/None values
        return bool(accessing_obj.attributes.get(attrname))

def objattr(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:
      objattr(attrname)
      objattr(attrname, value)
      objattr(attrname, value, compare=type)

    Works like attr, except it looks for an attribute on
    accessed_obj instead.

    """
    return attr(accessed_obj, accessed_obj, *args, **kwargs)

def locattr(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:
      locattr(attrname)
      locattr(attrname, value)
      locattr(attrname, value, compare=type)

    accessing_obj.location, if such an entity exists.

    if accessing_obj has a property ".obj" (such as is the case for a
    Command), then accessing_obj.obj.location is used instead.

    """
    if hasattr(accessing_obj, "location"):
        return attr(accessing_obj.location, accessed_obj, *args, **kwargs)

def objlocattr(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:

    accessed_obj.location, if such an entity exists.

    if accessed_obj has a property ".obj" (such as is the case for a

    """
    if hasattr(accessed_obj, "obj"):
        accessed_obj = accessed_obj.obj
    if hasattr(accessed_obj, "location"):
        return attr(accessed_obj.location, accessed_obj, *args, **kwargs)

def attr_eq(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:
       attr_gt(attrname, 54)
    """
    return attr(accessing_obj, accessed_obj, *args, **kwargs)

def attr_gt(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:

    Only true if access_obj's attribute > the value given.
    """
    return attr(accessing_obj, accessed_obj, *args, **{"compare": "gt"})

def attr_ge(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:

    Only true if access_obj's attribute >= the value given.
    """
    return attr(accessing_obj, accessed_obj, *args, **{"compare": "ge"})

def attr_lt(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:

    Only true if access_obj's attribute < the value given.
    """
    return attr(accessing_obj, accessed_obj, *args, **{"compare": "lt"})

def attr_le(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:

    Only true if access_obj's attribute <= the value given.
    """
    return attr(accessing_obj, accessed_obj, *args, **{"compare": "le"})

def attr_ne(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:

    Only true if access_obj's attribute != the value given.
    """
    return attr(accessing_obj, accessed_obj, *args, **{"compare": "ne"})

def tag(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:
        tag(tagkey)
        tag(tagkey, category)

    Only true if accessing_obj has the specified tag and optional
    category.
    If accessing_obj has the ".obj" property (such as is the case for
    a command), then accessing_obj.obj is used instead.

    """
    tagkey = args[0] if args else None
    category = args[1] if len(args) > 1 else None
    return bool(accessing_obj.tags.get(tagkey, category=category))

def objtag(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:
        objtag(tagkey)
        objtag(tagkey, category):

    Only true if `accessed_obj` has the given tag and optional category.

    """
    return tag(accessed_obj, None, *args, **kwargs)

def objloctag(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:
        objloctag(tagkey)
        objloctag(tagkey, category):

    Only true if `accessed_obj.location` has the given tag and optional category.
    If obj has no location, this lockfunc fails.

    """
    try:
        return tag(accessed_obj.location, None, *args, **kwargs)

def is_ooc(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:
        is_ooc()

    This is normally used to lock a Command, so it can be used
    only when out of character. When not logged in at all, this
    function will still return True.
    """
    obj = accessed_obj.obj if hasattr(accessed_obj, "obj") else accessed_obj
    account = obj.account if utils.inherits_from(obj, evennia.DefaultObject) else obj
    try:
        session = accessed_obj.session
        # note-this doesn't work well
        # for high multisession mode. We may need
        # to change to sessiondb to resolve this
        sessions = session = account.sessions.get()
        session = sessions[0] if sessions else None
    if not session:
        # this suggests we are not even logged in; treat as ooc.
    try:
        return not account.get_puppet(session)
        return not session.get_puppet()

    """
    Usage:
        objtag(tagkey, category)

    Only true if accessed_obj has the specified tag and optional
    category.
    """
    return bool(accessed_obj.tags.get(tagkey, category=category))

def inside(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:
       inside()

    True if accessing_obj is 'inside' accessing_obj. Note that this only checks
    one level down. So if if the lock is on a room, you will pass but not your
    inventory (since their location is you, not the locked object).  If you
    want also nested objects to pass the lock, use the `insiderecursive`
    lockfunc.
    """
    return accessing_obj.location == accessed_obj

def inside_rec(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:
        inside_rec()

    True if accessing_obj is inside the accessed obj, at up to 10 levels
    of recursion (so if this lock is on a room, then an object inside a box
    in your inventory will also pass the lock).
    """

    def _recursive_inside(obj, accessed_obj, lvl=1):
        if obj.location:
            if obj.location == accessed_obj:
            elif lvl >= 10:
                # avoid infinite recursions
            else:
                return _recursive_inside(obj.location, accessed_obj, lvl + 1)

    return _recursive_inside(accessing_obj, accessed_obj)

def holds(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Usage:
      holds()            checks if accessed_obj or accessed_obj.obj
                         is held by accessing_obj
      holds(key/dbref)   checks if accessing_obj holds an object
                          with given key/dbref
      holds(attrname, value)   checks if accessing_obj holds an
                               object with the given attrname and value

    This is passed if accessed_obj is carried by accessing_obj (that is,
    accessed_obj.location == accessing_obj), or if accessing_obj itself holds
    an object matching the given key.
    """
    try:
        # commands and scripts don't have contents, so we are usually looking
        # for the contents of their .obj property instead (i.e. the object the
        # command/script is attached to).
        contents = accessing_obj.contents
        try:
            contents = accessing_obj.obj.contents

    def check_holds(objid):
        # helper function. Compares both dbrefs and keys/aliases.
        objid = str(objid)
        dbref = utils.dbref(objid, reqhash=False)
        if dbref and any((True for obj in contents if obj.dbid == dbref)):
        objid = objid.lower()
            (
                True
                for obj in contents
                if obj.key.lower() == objid or objid in [al.lower() for al in obj.aliases.all()]
            )
        )

        # holds() - check if accessed_obj or accessed_obj.obj is held by accessing_obj
        try:
            if check_holds(accessed_obj.dbid):
            # we need to catch any trouble here
            pass
        return hasattr(accessed_obj, "obj") and check_holds(accessed_obj.obj.dbid)
    if len(args) == 1:
        # command is holds(dbref/key) - check if given objname/dbref is held by accessing_ob
        return check_holds(args[0])
    elif len(args) > 1:
        # command is holds(attrname, value) check if any held object has the given attribute and value
        for obj in contents:
            if obj.attributes.get(args[0]) == args[1]:

def has_account(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Only returns true if accessing_obj has_account is true, that is,
    this is an account-controlled object. It fails on actual accounts!

    This is a useful lock for traverse-locking Exits to restrain NPC
    mobiles from moving outside their areas.
    """
    return utils.inherits_from(accessing_obj, evennia.DefaultObject) and accessing_obj.has_account

def serversetting(accessing_obj, accessed_obj, *args, **kwargs):
    """
    Only returns true if the Evennia settings exists, alternatively has
    a certain value.

    Usage:
      serversetting(IRC_ENABLED)
      serversetting(BASE_SCRIPT_PATH, ['types'])

    A given True/False or integers will be converted properly. Note that
    everything will enter this function as strings, so they have to be
    unpacked to their real value. We only support basic properties.
    """
    if not args or not args[0]:
    if len(args) < 2:
        setting = args[0]
        val = "True"
    else:
        setting, val = args[0], args[1]
    # convert
    try:
        val = literal_eval(val)
        # we swallow errors here, lockfuncs has noone to report to

    if setting in settings._wrapped.__dict__:
        return settings._wrapped.__dict__[setting] == val

"""
A *lock* defines access to a particular subsystem or property of
Evennia. For example, the "owner" property can be impmemented as a
lock. Or the disability to lift an object or to ban users.

A lock consists of three parts:

 - access_type - this defines what kind of access this lock regulates. This
   just a string.
 - function call - this is one or many calls to functions that will determine
   if the lock is passed or not.
 - lock function(s). These are regular python functions with a special
   set of allowed arguments. They should always return a boolean depending
   on if they allow access or not.

A lock function is defined by existing in one of the modules
listed by settings.LOCK_FUNC_MODULES. It should also always
take four arguments looking like this:

   funcname(accessing_obj, accessed_obj, *args, **kwargs):
        [...]

The accessing object is the object wanting to gain access.
The accessed object is the object this lock resides on
args and kwargs will hold optional arguments and/or keyword arguments
to the function as a list and a dictionary respectively.

Example:

   perm(accessing_obj, accessed_obj, *args, **kwargs):
       "Checking if the object has a particular, desired permission"
       if args:
           desired_perm = args[0]
           return desired_perm in accessing_obj.permissions.all()
       return False

Lock functions should most often be pretty general and ideally possible to
re-use and combine in various ways to build clever locks.

Lock definition ("Lock string")

A lock definition is a string with a special syntax. It is added to
each object's lockhandler, making that lock available from then on.

The lock definition looks like this:

 'access_type:[NOT] func1(args)[ AND|OR][NOT] func2() ...'

That is, the access_type, a colon followed by calls to lock functions
combined with AND or OR. NOT negates the result of the following call.

Example:

 We want to limit who may edit a particular object (let's call this access_type
for 'edit', it depends on what the command is looking for). We want this to
only work for those with the Permission 'Builder'. So we use our lock
function above and define it like this:

  'edit:perm(Builder)'

Here, the lock-function perm() will be called with the string
'Builder' (accessing_obj and accessed_obj are added automatically,
you only need to add the args/kwargs, if any).

If we wanted to make sure the accessing object was BOTH a Builder and a
GoodGuy, we could use AND:

  'edit:perm(Builder) AND perm(GoodGuy)'

To allow EITHER Builder and GoodGuys, we replace AND with OR. perm() is just
one example, the lock function can do anything and compare any properties of
the calling object to decide if the lock is passed or not.

  'lift:attrib(very_strong) AND NOT attrib(bad_back)'

To make these work, add the string to the lockhandler of the object you want
to apply the lock to:

  obj.lockhandler.add('edit:perm(Builder)')

From then on, a command that wants to check for 'edit' access on this
object would do something like this:

  if not target_obj.lockhandler.has_perm(caller, 'edit'):
      caller.msg("Sorry, you cannot edit that.")

All objects also has a shortcut called 'access' that is recommended to
use instead:

  if not target_obj.access(caller, 'edit'):

Permissions

Permissions are just text strings stored in a comma-separated list on
typeclassed objects. The default perm() lock function uses them,
taking into account settings.PERMISSION_HIERARCHY. Also, the
restricted @perm command sets them, but otherwise they are identical
to any other identifier you can use.

"""

import re

__all__ = ("LockHandler", "LockException")

WARNING_LOG = settings.LOCKWARNING_LOG_FILE
_LOCK_HANDLER = None

#
# Exception class. This will be raised
# by errors in lock definitions.
#

class LockException(Exception):
    """
    Raised during an error in a lock.

    """

    pass

#
# Cached lock functions
#

_LOCKFUNCS = {}

def _cache_lockfuncs():
    """
    Updates the cache.

    """
    global _LOCKFUNCS
    _LOCKFUNCS = {}
    for modulepath in settings.LOCK_FUNC_MODULES:
        _LOCKFUNCS.update(utils.callables_from_module(modulepath))

#
# pre-compiled regular expressions
#

_RE_FUNCS = re.compile(r"\w+\([^)]*\)")
_RE_SEPS = re.compile(r"(?<=[ )])AND(?=\s)|(?<=[ )])OR(?=\s)|(?<=[ )])NOT(?=\s)")
_RE_OK = re.compile(r"%s|and|or|not")

#
#
# Lock handler
#
#

class LockHandler:
    """
    This handler should be attached to all objects implementing
    permission checks, under the property 'lockhandler'.

    """

        """
        Loads and pre-caches all relevant locks and their functions.

        Args:
            obj (object): The object on which the lockhandler is
            defined.

        """
        if not _LOCKFUNCS:
            _cache_lockfuncs()
        self.locks = {}
        try:
            self.reset()
            logger.log_trace(err)

        return ";".join(self.locks[key][2] for key in sorted(self.locks))

    def _log_error(self, message):
        "Try to log errors back to object"
        raise LockException(message)

    def _parse_lockstring(self, storage_lockstring):
        """
        Helper function. This is normally only called when the
        lockstring is cached and does preliminary checking.  locks are
        stored as a string

            atype:[NOT] lock()[[ AND|OR [NOT] lock()[...]];atype...

        Args:
            storage_locksring (str): The lockstring to parse.

        """
        locks = {}
        if not storage_lockstring:
            return locks
        duplicates = 0
        elist = []  # errors
        wlist = []  # warnings
        for raw_lockstring in storage_lockstring.split(";"):
            if not raw_lockstring:
                continue
            lock_funcs = []
            try:
                access_type, rhs = (part.strip() for part in raw_lockstring.split(":", 1))
                return locks

            # parse the lock functions and separators
            funclist = _RE_FUNCS.findall(rhs)
            evalstring = rhs
            for pattern in ("AND", "OR", "NOT"):
                evalstring = re.sub(r"\b%s\b" % pattern, pattern.lower(), evalstring)
            nfuncs = len(funclist)
            for funcstring in funclist:
                funcname, rest = (part.strip().strip(")") for part in funcstring.split("(", 1))
                func = _LOCKFUNCS.get(funcname, None)
                if not callable(func):
                    elist.append(
                        _("Lock: lock-function '{lockfunc}' is not available.").format(
                            lockfunc=funcstring
                        )
                    )
                    continue
                args = list(arg.strip() for arg in rest.split(",") if arg and "=" not in arg)
                kwargs = dict(
                    [
                        (part.strip() for part in arg.split("=", 1))
                        for arg in rest.split(",")
                        if arg and "=" in arg
                    ]
                )
                lock_funcs.append((func, args, kwargs))
                evalstring = evalstring.replace(funcstring, "%s")
            if len(lock_funcs) < nfuncs:
                continue
            try:
                # purge the eval string of any superfluous items, then test it
                evalstring = " ".join(_RE_OK.findall(evalstring))
                eval(evalstring % tuple(True for func in funclist), {}, {})
                elist.append(
                    _("Lock: definition '{lock_string}' has syntax errors.").format(
                        lock_string=raw_lockstring
                    )
                )
                continue
            if access_type in locks:
                duplicates += 1
                wlist.append(
                    _(
                        "LockHandler on {obj}: access type '{access_type}' "
                        "changed from '{source}' to '{goal}' ".format(
                            obj=self.obj,
                            access_type=access_type,
                            source=locks[access_type][2],
                            goal=raw_lockstring,
                        )
                    )
                )
            locks[access_type] = (evalstring, tuple(lock_funcs), raw_lockstring)
        if wlist and WARNING_LOG:
            # a warning text was set, it's not an error, so only report
            logger.log_file("\n".join(wlist), WARNING_LOG)
        if elist:
            # an error text was set, raise exception.
            raise LockException("\n".join(elist))
        # return the gathered locks in an easily executable form
        return locks

    def _cache_locks(self, storage_lockstring):
        """
        Store data

        """
        self.locks = self._parse_lockstring(storage_lockstring)

    def _save_locks(self):
        """
        Store locks to obj

        """
        self.obj.lock_storage = ";".join([tup[2] for tup in self.locks.values()])

    def cache_lock_bypass(self, obj):
        """
        We cache superuser bypass checks here for efficiency. This
        needs to be re-run when an account is assigned to a character.
        We need to grant access to superusers. We need to check both
        directly on the object (accounts), through obj.account and using
        the get_account() method (this sits on serversessions, in some
        rare cases where a check is done before the login process has
        yet been fully finalized)

        Args:
            obj (object): This is checked for the `is_superuser` property.

        """
        self.lock_bypass = hasattr(obj, "is_superuser") and obj.is_superuser

    def add(self, lockstring, validate_only=False):
        """
        Add a new lockstring to handler.

        Args:
            lockstring (str or list): A string on the form
                `"<access_type>:<functions>"`.  Multiple access types
                should be separated by semicolon (`;`). Alternatively,
                a list with lockstrings.
            validate_only (bool, optional): If True, validate the lockstring but
                don't actually store it.
        Returns:
            success (bool): The outcome of the addition, `False` on
                error. If `validate_only` is True, this will be a tuple
                (bool, error), for pass/fail and a string error.

        """
        if isinstance(lockstring, str):
            lockdefs = lockstring.split(";")
        else:
            lockdefs = [lockdef for locks in lockstring for lockdef in locks.split(";")]
            lockstring = ";".join(lockdefs)

        err = ""
        # sanity checks
        for lockdef in lockdefs:
            if ":" not in lockdef:
                err = _("Lock: '{lockdef}' contains no colon (:).").format(lockdef=lockdef)
                if validate_only:
                    return False, err
                else:
                    self._log_error(err)
            access_type, rhs = [part.strip() for part in lockdef.split(":", 1)]
            if not access_type:
                err = _(
                    "Lock: '{lockdef}' has no access_type " "(left-side of colon is empty)."
                ).format(lockdef=lockdef)
                else:
            if rhs.count("(") != rhs.count(")"):
                err = _("Lock: '{lockdef}' has mismatched parentheses.").format(lockdef=lockdef)
                else:
            if not _RE_FUNCS.findall(rhs):
                err = _("Lock: '{lockdef}' has no valid lock functions.").format(lockdef=lockdef)
                else:
        if validate_only:
            return True, None
        # get the lock string
        storage_lockstring = self.obj.lock_storage
        if storage_lockstring:
            storage_lockstring = storage_lockstring + ";" + lockstring
        else:
            storage_lockstring = lockstring
        # cache the locks will get rid of eventual doublets
        self._cache_locks(storage_lockstring)
        self._save_locks()

    def validate(self, lockstring):
        """
        Validate lockstring syntactically, without saving it.

        Args:
            lockstring (str): Lockstring to validate.
        Returns:
            valid (bool): If validation passed or not.

        """
        return self.add(lockstring, validate_only=True)

    def replace(self, lockstring):
        """
        Replaces the lockstring entirely.

        Args:
            lockstring (str): The new lock definition.

        Return:
            success (bool): False if an error occurred.

        Raises:
            LockException: If a critical error occurred.
                If so, the old string is recovered.

        """
        old_lockstring = str(self)
        self.clear()
        try:
            return self.add(lockstring)
        except LockException:
            self.add(old_lockstring)
            raise

    def get(self, access_type=None):
        """
        Get the full lockstring or the lockstring of a particular
        access type.

        Args:
            access_type (str, optional):

        Returns:
            lockstring (str): The matched lockstring, or the full
                lockstring if no access_type was given.
        """

        if access_type:
            return self.locks.get(access_type, ["", "", ""])[2]

        """
        Return all lockstrings

        Returns:
            lockstrings (list): All separate lockstrings

        """
        return str(self).split(";")

    def remove(self, access_type):
        """
        Remove a particular lock from the handler

        Args:
            access_type (str): The type of lock to remove.

        Returns:
            success (bool): If the access_type was not found
                in the lock, this returns `False`.

        """
        if access_type in self.locks:
            del self.locks[access_type]
            self._save_locks()

    delete = remove  # alias for historical reasons

        """
        Remove all locks in the handler.

        """
        self.lock_storage = ""

        """
        Set the reset flag, so the the lock will be re-cached at next
        checking.  This is usually called by @reload.

        """
        self._cache_locks(self.obj.lock_storage)
        self.cache_lock_bypass(self.obj)

    def append(self, access_type, lockstring, op="or"):
        """
        Append a lock definition to access_type if it doesn't already exist.

        Args:
            access_type (str): Access type.
            lockstring (str): A valid lockstring, without the operator to
                link it to an eventual existing lockstring.
            op (str): An operator 'and', 'or', 'and not', 'or not' used
                for appending the lockstring to an existing access-type.
        Note:
            The most common use of this method is for use in commands where
            the user can specify their own lockstrings. This method allows
            the system to auto-add things like Admin-override access.

        """
        old_lockstring = self.get(access_type)
        if not lockstring.strip().lower() in old_lockstring.lower():
            lockstring = "{old} {op} {new}".format(
                old=old_lockstring, op=op, new=lockstring.strip()
            )
            self.add(lockstring)

    def check(self, accessing_obj, access_type, default=False, no_superuser_bypass=False):
        """
        Checks a lock of the correct type by passing execution off to
        the lock function(s).

        Args:
            accessing_obj (object): The object seeking access.
            access_type (str): The type of access wanted.
            default (bool, optional): If no suitable lock type is
                found, default to this result.
            no_superuser_bypass (bool): Don't use this unless you
                really, really need to, it makes supersusers susceptible
                to the lock check.

        Notes:
            A lock is executed in the follwoing way:

            Parsing the lockstring, we (during cache) extract the valid
            lock functions and store their function objects in the right
            order along with their args/kwargs. These are now executed in
            sequence, creating a list of True/False values. This is put
            into the evalstring, which is a string of AND/OR/NOT entries
            separated by placeholders where each function result should
            go. We just put those results in and evaluate the string to
            get a final, combined True/False value for the lockstring.

            The important bit with this solution is that the full
            lockstring is never blindly evaluated, and thus there (should
            be) no way to sneak in malign code in it. Only "safe" lock
            functions (as defined by your settings) are executed.

        """
        try:
            # check if the lock should be bypassed (e.g. superuser status)
            if accessing_obj.locks.lock_bypass and not no_superuser_bypass:
            # happens before session is initiated.
            if not no_superuser_bypass and (
                (hasattr(accessing_obj, "is_superuser") and accessing_obj.is_superuser)
                or (
                    utils.inherits_from(accessing_obj, evennia.DefaultObject)
                    and hasattr(accessing_obj.account, "is_superuser")
                    and accessing_obj.account.is_superuser
                )
                or (
                    hasattr(accessing_obj, "get_account")
                    and (
                        not accessing_obj.get_account() or accessing_obj.get_account().is_superuser
                    )
                )
            ):

        # no superuser or bypass -> normal lock operation
            # we have a lock, test it.
            evalstring, func_tup, raw_string = self.locks[access_type]
            # execute all lock funcs in the correct order, producing a tuple of True/False results.
            true_false = tuple(
                bool(tup[0](accessing_obj, self.obj, *tup[1], access_type=access_type, **tup[2]))
                for tup in func_tup
            )
            # the True/False tuple goes into evalstring, which combines them
            # with AND/OR/NOT in order to get the final result.
            return eval(evalstring % true_false)
        else:

    def _eval_access_type(self, accessing_obj, locks, access_type):
        """
        Helper method for evaluating the access type using eval().

        Args:
            accessing_obj (object): Object seeking access.
            locks (dict): The pre-parsed representation of all access-types.
            access_type (str): An access-type key to evaluate.

        """
        evalstring, func_tup, raw_string = locks[access_type]
        true_false = tuple(tup[0](accessing_obj, self.obj, *tup[1], **tup[2]) for tup in func_tup)
        return eval(evalstring % true_false)

    def check_lockstring(
        self, accessing_obj, lockstring, no_superuser_bypass=False, default=False, access_type=None
    ):
        """
        Do a direct check against a lockstring ('atype:func()..'),
        without any intermediary storage on the accessed object.

        Args:
            accessing_obj (object or None): The object seeking access.
                Importantly, this can be left unset if the lock functions
                don't access it, no updating or storage of locks are made
                against this object in this method.
            lockstring (str): Lock string to check, on the form
                `"access_type:lock_definition"` where the `access_type`
                part can potentially be set to a dummy value to just check
                a lock condition.
            no_superuser_bypass  (bool, optional): Force superusers to heed lock.
            default (bool, optional): Fallback result to use if `access_type` is set
                but no such `access_type` is found in the given `lockstring`.
            access_type (str, bool): If set, only this access_type will be looked up
                among the locks defined by `lockstring`.

        Return:
            access (bool): If check is passed or not.

        """
        try:
            if no_superuser_bypass and (
                or (
                )
                or (
                    and (
                    )
                )
            ):
        if ":" not in lockstring:
            lockstring = "%s:%s" % ("_dummy", lockstring)

        locks = self._parse_lockstring(lockstring)

            if access_type not in locks:
                return default
            else:
                return self._eval_access_type(accessing_obj, locks, access_type)
        else:
            # if no access types was given and multiple locks were
            # embedded in the lockstring we assume all must be true
            return all(
                self._eval_access_type(accessing_obj, locks, access_type) for access_type in locks
            )

# convenience access function

# dummy to be able to call check_lockstring from the outside

class _ObjDummy:
    lock_storage = ""

def check_lockstring(
    accessing_obj, lockstring, no_superuser_bypass=False, default=False, access_type=None
):
    """
    Do a direct check against a lockstring ('atype:func()..'),
    without any intermediary storage on the accessed object.

    Args:
        accessing_obj (object or None): The object seeking access.
            Importantly, this can be left unset if the lock functions
            don't access it, no updating or storage of locks are made
            against this object in this method.
        lockstring (str): Lock string to check, on the form
            `"access_type:lock_definition"` where the `access_type`
            part can potentially be set to a dummy value to just check
            a lock condition.
        no_superuser_bypass  (bool, optional): Force superusers to heed lock.
        default (bool, optional): Fallback result to use if `access_type` is set
            but no such `access_type` is found in the given `lockstring`.
        access_type (str, bool): If set, only this access_type will be looked up
            among the locks defined by `lockstring`.

    Return:
        access (bool): If check is passed or not.

    """
    global _LOCK_HANDLER
    if not _LOCK_HANDLER:
        _LOCK_HANDLER = LockHandler(_ObjDummy())
    return _LOCK_HANDLER.check_lockstring(
        lockstring,
        no_superuser_bypass=no_superuser_bypass,
        default=default,
        access_type=access_type,
    )

def check_perm(obj, permission, no_superuser_bypass=False):
    """
    Shortcut for checking if an object has the given `permission`.  If the
    permission is in `settings.PERMISSION_HIERARCHY`, the check passes
    if the object has this permission or higher.

    This is equivalent to calling the perm() lockfunc, but without needing
    an accessed object.

    Args:
        obj (Object, Account): The object to check access. If this has a linked
            Account, the account is checked instead (same rules as per perm()).
        permission (str): The permission string to check.
        no_superuser_bypass (bool, optional): If unset, the superuser
            will always pass this check.

    """
    from evennia.locks.lockfuncs import perm

    if not no_superuser_bypass and obj.is_superuser:
    return perm(obj, None, permission)

def validate_lockstring(lockstring):
    """
    Validate so lockstring is on a valid form.

    Args:
        lockstring (str): Lockstring to validate.

    Returns:
        is_valid (bool): If the lockstring is valid or not.
        error (str or None): A string describing the error, or None
            if no error was found.

    """
    return _LOCK_HANDLER.validate(lockstring)

def get_all_lockfuncs():
    """
    Get a dict of available lock funcs.

    Returns:
        lockfuncs (dict): Mapping {lockfuncname:func}.

    """
    if not _LOCKFUNCS:
        _cache_lockfuncs()
    return _LOCKFUNCS

def _test():
    # testing

    class TestObj(object):
        pass

    import pdb

    obj1 = TestObj()
    obj2 = TestObj()

    # obj1.lock_storage = "owner:dbref(#4);edit:dbref(#5) or perm(Admin);examine:perm(Builder);delete:perm(Admin);get:all()"
    # obj1.lock_storage = "cmd:all();admin:id(1);listen:all();send:all()"
    obj1.lock_storage = "listen:perm(Developer)"

    pdb.set_trace()
    obj1.locks = LockHandler(obj1)
    obj2.permissions.add("Developer")
    obj2.id = 4

    # obj1.locks.add("edit:attr(test)")

    print("comparing obj2.permissions (%s) vs obj1.locks (%s)" % (obj2.permissions, obj1.locks))
    print(obj1.locks.check(obj2, "owner"))
    print(obj1.locks.check(obj2, "edit"))
    print(obj1.locks.check(obj2, "examine"))
    print(obj1.locks.check(obj2, "delete"))
    print(obj1.locks.check(obj2, "get"))
    print(obj1.locks.check(obj2, "listen"))

"""
Custom manager for Objects.
"""
import re

from django.db.models.fields import exceptions
    dbid_to_obj,
    is_iter,
    string_partial_matching,
)

__all__ = ("ObjectManager", "ObjectDBManager")

# delayed import
_ATTR = None

# Try to use a custom way to parse id-tagged multimatches.

class ObjectDBManager(TypedObjectManager):
    """
    This ObjectManager implements methods for searching
    and manipulating Objects directly from the database.

    Evennia-specific search methods (will return Typeclasses or
    lists of Typeclasses, whereas Django-general methods will return
    Querysets or database objects).

    get_object_with_account
    get_objs_with_key_and_typeclass
    get_objs_with_attr
    get_objs_with_attr_match
    get_objs_with_db_property
    get_objs_with_db_property_match
    get_objs_with_key_or_alias
    get_contents
    search_object (interface to many of the above methods,
                   equivalent to evennia.search_object)
    copy_object

    """

    #
    # ObjectManager Get methods
    #

    # account related

    def get_object_with_account(self, ostring, exact=True, candidates=None):
        """
        Search for an object based on its account's name or dbref.

        Args:
            ostring (str or int): Search criterion or dbref. Searching
                for an account is sometimes initiated by appending an `*` to
                the beginning of the search criterion (e.g. in
                local_and_global_search). This is stripped here.
            exact (bool, optional): Require an exact account match.
            candidates (list, optional): Only search among this list of possible
                object candidates.

        Return:
            match (query): Matching query.

        """
        ostring = str(ostring).lstrip("*")
        # simplest case - search by dbref
        if dbref:
            try:
                return self.get(db_account__id=dbref)
                pass

        # not a dbref. Search by name.
        cand_restriction = (
            candidates is not None
            and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj])
            or Q()
        )
        if exact:
            return self.filter(cand_restriction & Q(db_account__username__iexact=ostring)).order_by(
                "id"
            )
        else:  # fuzzy matching
            obj_cands = self.select_related().filter(
                cand_restriction & Q(db_account__username__istartswith=ostring)
            )
            acct_cands = [obj.account for obj in obj_cands]

            if obj_cands:
                index_matches = string_partial_matching(
                    [acct.key for acct in acct_cands], ostring, ret_index=True
                )
                acct_cands = [acct_cands[i].id for i in index_matches]
                return obj_cands.filter(db_account__id__in=acct_cands).order_by("id")

    def get_objs_with_key_and_typeclass(self, oname, otypeclass_path, candidates=None):
        """
        Returns objects based on simultaneous key and typeclass match.

        Args:
            oname (str): Object key to search for
            otypeclass_path (str): Full Python path to tyepclass to search for
            candidates (list, optional): Only match among the given list of candidates.

        Returns:
            matches (query): The matching objects.
        """
            or Q()
        )
        return self.filter(
            cand_restriction & Q(db_key__iexact=oname, db_typeclass_path__exact=otypeclass_path)
        ).order_by("id")

    # attr/property related

    def get_objs_with_attr(self, attribute_name, candidates=None):
        """
        Get objects based on having a certain Attribute defined.

        Args:
            attribute_name (str): Attribute name to search for.
            candidates (list, optional): Only match among the given list of object
                candidates.

        Returns:
            matches (query):  All objects having the given attribute_name defined at all.

        """
            candidates is not None and Q(id__in=[obj.id for obj in candidates]) or Q()
        )
        return self.filter(cand_restriction & Q(db_attributes__db_key=attribute_name)).order_by(
            "id"
        )

    def get_objs_with_attr_value(
        self, attribute_name, attribute_value, candidates=None, typeclasses=None
    ):
        """
        Get all objects having the given attrname set to the given value.

        Args:
            attribute_name (str): Attribute key to search for.
            attribute_value (any):  Attribute value to search for. This can also be database
                objects.
            candidates (list, optional): Candidate objects to limit search to.
            typeclasses (list, optional): Python pats to restrict matches with.

        Returns:
            Queryset: Iterable with 0, 1 or more matches fullfilling both the `attribute_name` and
                `attribute_value` criterions.

        Notes:
            This uses the Attribute's PickledField to transparently search the database by matching
            the internal representation. This is reasonably effective but since Attribute values
            cannot be indexed, searching by Attribute key is to be preferred whenever possible.

        """
            or Q()
        )
        type_restriction = typeclasses and Q(db_typeclass_path__in=make_iter(typeclasses)) or Q()

            cand_restriction
            & type_restriction
            & Q(db_attributes__db_key=attribute_name)
            & Q(db_attributes__db_value=attribute_value)
        return results

    def get_objs_with_db_property(self, property_name, candidates=None):
        """
        Get all objects having a given db field property.

        Args:
            property_name (str): The name of the field to match for.
            candidates (list, optional): Only search among th egiven candidates.

        Returns:
            matches (list): The found matches.

        """
        property_name = "db_%s" % property_name.lstrip("db_")
            or Q()
        )
        querykwargs = {property_name: None}
        try:
            return list(self.filter(cand_restriction).exclude(Q(**querykwargs)).order_by("id"))
        except exceptions.FieldError:
            return []

    def get_objs_with_db_property_value(
        self, property_name, property_value, candidates=None, typeclasses=None
    ):
        """
        Get objects with a specific field name and value.

        Args:
            property_name (str): Field name to search for.
            property_value (any): Value required for field with `property_name` to have.
            candidates (list, optional): List of objects to limit search to.
            typeclasses (list, optional): List of typeclass-path strings to restrict matches with

        Returns:

        """
        if isinstance(property_name, str):
            if not property_name.startswith("db_"):
                property_name = "db_%s" % property_name
        querykwargs = {property_name: property_value}
            or Q()
        )
        try:
            return self.filter(cand_restriction & type_restriction & Q(**querykwargs)).order_by(
                "id"
            )
            return self.none()
            from evennia.utils import logger

                "The property '%s' does not support search criteria of the type %s."
                % (property_name, type(property_value))
            )

    def get_contents(self, location, excludeobj=None):
        """
        Get all objects that has a location set to this one.

        Args:
            location (Object): Where to get contents from.
            excludeobj (Object or list, optional): One or more objects
                to exclude from the match.

        Returns:

        """
        exclude_restriction = (
            Q(pk__in=[_GA(obj, "id") for obj in make_iter(excludeobj)]) if excludeobj else Q()
        )
        return self.filter(db_location=location).exclude(exclude_restriction).order_by("id")

    def get_objs_with_key_or_alias(self, ostring, exact=True, candidates=None, typeclasses=None):
        """
        Args:
            ostring (str): A search criterion.
            exact (bool, optional): Require exact match of ostring
                (still case-insensitive). If `False`, will do fuzzy matching
                using `evennia.utils.utils.string_partial_matching` algorithm.
            candidates (list): Only match among these candidates.
            typeclasses (list): Only match objects with typeclasses having thess path strings.

        Returns:

        """
        if not isinstance(ostring, str):
            if hasattr(ostring, "key"):
                ostring = ostring.key
            else:
                return self.none()
        if is_iter(candidates) and not len(candidates):
            # if candidates is an empty iterable there can be no matches
            # Exit early.

        # build query objects
        candidates_id = [_GA(obj, "id") for obj in make_iter(candidates) if obj]
        cand_restriction = candidates is not None and Q(pk__in=candidates_id) or Q()
        if exact:
            # exact match - do direct search
            return (
                (
                    self.filter(
                        cand_restriction
                        & type_restriction
                        & (
                            Q(db_key__iexact=ostring)
                            | Q(db_tags__db_key__iexact=ostring)
                            & Q(db_tags__db_tagtype__iexact="alias")
                        )
                    )
                )
                .distinct()
                .order_by("id")
            )
        elif candidates:
            # fuzzy with candidates
            search_candidates = (
                self.filter(cand_restriction & type_restriction).distinct().order_by("id")
            )
        else:
            # fuzzy without supplied candidates - we select our own candidates
                self.filter(
                    type_restriction
                    & (Q(db_key__icontains=ostring) | Q(db_tags__db_key__icontains=ostring))
                )
            )
        # fuzzy matching
        key_strings = search_candidates.values_list("db_key", flat=True).order_by("id")

        match_ids = []
        index_matches = string_partial_matching(key_strings, ostring, ret_index=True)
        if index_matches:
            # a match by key
            match_ids = [
                obj.id for ind, obj in enumerate(search_candidates) if ind in index_matches
            ]
        else:
            # match by alias rather than by key
            search_candidates = search_candidates.filter(
                db_tags__db_tagtype__iexact="alias", db_tags__db_key__icontains=ostring
            alias_strings = []
            alias_candidates = []
            # TODO create the alias_strings and alias_candidates lists more efficiently?
            for candidate in search_candidates:
                for alias in candidate.aliases.all():
                    alias_strings.append(alias)
                    alias_candidates.append(candidate)
            index_matches = string_partial_matching(alias_strings, ostring, ret_index=True)
            if index_matches:
                # it's possible to have multiple matches to the same Object, we must weed those out
                match_ids = [alias_candidates[ind].id for ind in index_matches]
        # TODO - not ideal to have to do a second lookup here, but we want to return a queryset
        # rather than a list ... maybe the above queries can be improved.
        return self.filter(id__in=match_ids)

    # main search methods and helper functions

    def search_object(
        self,
        exact=True,
        use_dbref=True,
    ):
        """
        Search as an object globally or in a list of candidates and
        return results. The result is always an Object. Always returns
        a list.

        Args:
            searchdata (str or Object): The entity to match for. This is
                usually a key string but may also be an object itself.
                By default (if no `attribute_name` is set), this will
                search `object.key` and `object.aliases` in order.
                Can also be on the form #dbref, which will (if
                `exact=True`) be matched against primary key.
            attribute_name (str): Use this named Attribute to
                match searchdata against, instead of the defaults. If
                this is the name of a database field (with or without
                the `db_` prefix), that will be matched too.
            typeclass (str or TypeClass): restrict matches to objects
                having this typeclass. This will help speed up global
                searches.
            candidates (list): If supplied, search will
                only be performed among the candidates in this list. A
                common list of candidates is the contents of the
                current location searched.
            exact (bool): Match names/aliases exactly or partially.
                Partial matching matches the beginning of words in the
                names/aliases, using a matching routine to separate
                multiple matches in names with multiple components (so
                "bi sw" will match "Big sword"). Since this is more
                expensive than exact matching, it is recommended to be
                used together with the `candidates` keyword to limit the
                number of possibilities. This value has no meaning if
                searching for attributes/properties.
            use_dbref (bool): If False, bypass direct lookup of a string
                on the form #dbref and treat it like any string.
            tags (list): A list of tuples `(tagkey, tagcategory)` where the
                matched object must have _all_ tags in order to be considered
                a match.

        Returns:
            matches (list): Matching objects

        """

        def _searcher(searchdata, candidates, typeclass, exact=False):
            """
            Helper method for searching objects. `typeclass` is only used
            for global searching (no candidates)
            """
            if attribute_name:
                # attribute/property search (always exact).
                matches = self.get_objs_with_db_property_value(
                    attribute_name, searchdata, candidates=candidates, typeclasses=typeclass
                )
                if matches:
                    return matches
                return self.get_objs_with_attr_value(
                )
            else:
                # normal key/alias search
                return self.get_objs_with_key_or_alias(
                    searchdata, exact=exact, candidates=candidates, typeclasses=typeclass
                )

        def _search_by_tag(query, taglist):
            if not query:
                query = self.all()

            for tagkey, tagcategory in taglist:
                query = query.filter(db_tags__db_key=tagkey, db_tags__db_category=tagcategory)

            return query

        if not searchdata and searchdata != 0:
            if tags:
                return _search_by_tag(make_iter(tags))

            # typeclass may also be a list
            typeclasses = make_iter(typeclass)
            for i, typeclass in enumerate(make_iter(typeclasses)):
                if callable(typeclass):
                    typeclasses[i] = "%s.%s" % (typeclass.__module__, typeclass.__name__)
                else:
                    typeclasses[i] = "%s" % typeclass
            typeclass = typeclasses

        if candidates is not None:
            if not candidates:
                # candidates is the empty list. This should mean no matches can ever be acquired.
                return []
            # Convenience check to make sure candidates are really dbobjs
            candidates = [cand for cand in make_iter(candidates) if cand]
            if typeclass:
                candidates = [
                    cand for cand in candidates if _GA(cand, "db_typeclass_path") in typeclass
                ]

        dbref = not attribute_name and exact and use_dbref and self.dbref(searchdata)
        if dbref:
            # Easiest case - dbref matching (always exact)
            dbref_match = self.dbref_search(dbref)
                dmatch = dbref_match[0]
                if not candidates or dmatch in candidates:
                    return dbref_match
                else:
                    return self.none()

        # Search through all possibilities.
        match_number = None
        # always run first check exact - we don't want partial matches
        # if on the form of 1-keyword etc.
        matches = _searcher(searchdata, candidates, typeclass, exact=True)

            # no matches found - check if we are dealing with N-keyword
            # query - if so, strip it.
            match = _MULTIMATCH_REGEX.match(str(searchdata))
            match_number = None
            stripped_searchdata = searchdata
            if match:
                # strips the number
                match_number, stripped_searchdata = match.group("number"), match.group("name")
                match_number = int(match_number) - 1
            if match_number is not None:
                # run search against the stripped data
                matches = _searcher(stripped_searchdata, candidates, typeclass, exact=True)
                    # final chance to get a looser match against the number-strippped query
                    matches = _searcher(stripped_searchdata, candidates, typeclass, exact=False)
            elif not exact:
                matches = _searcher(searchdata, candidates, typeclass, exact=False)

        if tags:
            matches = _search_by_tag(matches, make_iter(tags))

        # deal with result
        if len(matches) == 1 and match_number is not None and match_number != 0:
            # this indicates trying to get a single match with a match-number
            # targeting some higher-number match (like 2-box when there is only
            # one box in the room). This leads to a no-match.
            matches = self.none()
        elif len(matches) > 1 and match_number is not None:
            # multiple matches, but a number was given to separate them
            if 0 <= match_number < len(matches):
                # limit to one match (we still want a queryset back)
                # TODO: Can we do this some other way and avoid a second lookup?
                matches = self.filter(id=matches[match_number].id)
            else:
                # a number was given outside of range. This means a no-match.
                matches = self.none()

        # return a list (possibly empty)

    # alias for backwards compatibility
    object_search = search_object
    search = search_object

    #
    # ObjectManager Copy method

    def copy_object(
        self,
        original_object,
        new_key=None,
        new_location=None,
        new_home=None,
        new_permissions=None,
        new_locks=None,
        new_aliases=None,
        new_destination=None,
    ):
        """
        Create and return a new object as a copy of the original object. All
        will be identical to the original except for the arguments given
        specifically to this method. Object contents will not be copied.

        Args:
            original_object (Object): The object to make a copy from.
            new_key (str, optional): Name of the copy, if different
                from the original.
            new_location (Object, optional): Alternate location.
            new_home (Object, optional): Change the home location
            new_aliases (list, optional): Give alternate object
                aliases as a list of strings.
            new_destination (Object, optional): Used only by exits.

        Returns:
            copy (Object or None): The copy of `original_object`,
                optionally modified as per the ingoing keyword
                arguments.  `None` if an error was encountered.

        """

        # get all the object's stats
        typeclass_path = original_object.typeclass_path
        if not new_key:
            new_key = original_object.key
        if not new_location:
            new_location = original_object.location
        if not new_home:
            new_home = original_object.home
        if not new_aliases:
            new_aliases = original_object.aliases.all()
        if not new_locks:
            new_locks = original_object.db_lock_storage
        if not new_permissions:
            new_permissions = original_object.permissions.all()
        if not new_destination:
            new_destination = original_object.destination

        # create new object
        from evennia.scripts.models import ScriptDB
        from evennia.utils import create

        new_object = create.create_object(
            typeclass_path,
            key=new_key,
            location=new_location,
            home=new_home,
            permissions=new_permissions,
            locks=new_locks,
            aliases=new_aliases,
            destination=new_destination,
        )
        if not new_object:

        # copy over all attributes from old to new.
        attrs = (
            (a.key, a.value, a.category, a.lock_storage) for a in original_object.attributes.all()
        )
        new_object.attributes.batch_add(*attrs)

        # copy over all cmdsets, if any
        for icmdset, cmdset in enumerate(original_object.cmdset.all()):
            if icmdset == 0:
                new_object.cmdset.add_default(cmdset)
            else:
                new_object.cmdset.add(cmdset)

        # copy over all scripts, if any
        for script in original_object.scripts.all():
            ScriptDB.objects.copy_script(script, new_obj=new_object)

        # copy over all tags, if any
        tags = (
            (t.db_key, t.db_category, t.db_data) for t in original_object.tags.all(return_objs=True)
        )
        new_object.tags.batch_add(*tags)

        return new_object

    def clear_all_sessids(self):
        """
        Clear the db_sessid field of all objects having also the
        db_account field set.

        """
        self.filter(db_sessid__isnull=False).update(db_sessid=None)

    def create_object(
        self,
        key=None,
        home=None,
        nohome=False,
        nattributes=None,
    ):
        """

        Create a new in-game object.

            typeclass (class or str): Class or python path to a typeclass.
            key (str): Name of the new object. If not set, a name of
                `#dbref` will be set.
            location (Object or str): Obj or #dbref to use as the location of the new object.
            home (Object or str): Obj or #dbref to use as the object's home location.
            permissions (list): A list of permission strings or tuples (permstring, category).
            locks (str): one or more lockstrings, separated by semicolons.
            aliases (list): A list of alternative keys or tuples (aliasstring, category).
            tags (list): List of tag keys or tuples (tagkey, category) or (tagkey, category, data).
            destination (Object or str): Obj or #dbref to use as an Exit's target.
            report_to (Object): The object to return error messages to.
            nohome (bool): This allows the creation of objects without a
                default home location; only used when creating the default
                location itself or during unittests.
            attributes (list): Tuples on the form (key, value) or (key, value, category),
                (key, value, lockstring) or (key, value, lockstring, default_access).
                to set as Attributes on the new object.
            nattributes (list): Non-persistent tuples on the form (key, value). Note that
                adding this rarely makes sense since this data will not survive a reload.

        Returns:
            object (Object): A newly created object of the given typeclass.

        Raises:
            ObjectDB.DoesNotExist: If trying to create an Object with
                `location` or `home` that can't be found.

        """
        typeclass = typeclass if typeclass else settings.BASE_OBJECT_TYPECLASS

        # convenience converters to avoid common usage mistake
        aliases = make_iter(aliases) if aliases is not None else None

        # Setup input for the create command. We use ObjectDB as baseclass here
        # to give us maximum freedom (the typeclasses will load

        location = dbid_to_obj(location, self.model)
        destination = dbid_to_obj(destination, self.model)

        if home:
            home_obj_or_dbref = home
        elif nohome:
            home_obj_or_dbref = None
        else:
            home_obj_or_dbref = settings.DEFAULT_HOME

        try:
            home = dbid_to_obj(home_obj_or_dbref, self.model)
            if settings.TEST_ENVIRONMENT:
                # this happens for databases where the #1 location is flushed during tests
                home = None
            else:
                raise self.model.DoesNotExist(
                    f"settings.DEFAULT_HOME (= '{settings.DEFAULT_HOME}') does not exist, "
                    "or the setting is malformed."
                )

        new_object = typeclass(
            db_key=key,
            db_location=location,
            db_destination=destination,
            db_home=home,
            db_typeclass_path=typeclass.path,
        )
        # store the call signature for the signal
        new_object._createdict = dict(
            key=key,
            destination=destination,
            home=home,
            typeclass=typeclass.path,
            nohome=nohome,
            nattributes=nattributes,
        )
        # used.
        new_object.save()

        signals.SIGNAL_OBJECT_POST_CREATE.send(sender=new_object)

class ObjectManager(ObjectDBManager, TypeclassManager):
    pass

"""
This module defines the database models for all in-game objects, that
is, all objects that has an actual existence in-game.

Each database object is 'decorated' with a 'typeclass', a normal
python class that implements all the various logics needed by the game
in question. Objects created of this class transparently communicate
with its related database object for storing all attributes. The
admin should usually not have to deal directly with this database
object layer.

Attributes are separate objects that store values persistently onto
the database object. Like everything else, they can be accessed
transparently through the decorating TypeClass.
"""

from django.core.validators import validate_comma_separated_integer_list

from evennia.objects.manager import ObjectDBManager
from evennia.utils.utils import dbref, lazy_property, make_iter

class ContentsHandler:
    """
    Handles and caches the contents of an object to avoid excessive
    lookups (this is done very often due to cmdhandler needing to look
    for object-cmdsets). It is stored on the 'contents_cache' property
    of the ObjectDB.
    """

        """
        Sets up the contents handler.

        Args:
            obj (Object):  The object on which the
                handler is defined

        Notes:
            This was changed from using `set` to using `dict` internally
            in order to retain insertion order.

        """
        self._pkcache = {}
        self._idcache = obj.__class__.__instance_cache__
        self._typecache = defaultdict(dict)
        self.init()

        """
        Retrieves all objects from database. Used for initializing.

        Returns:
            Objects (list of ObjectDB)
        """
        return list(self.obj.locations_set.all())

        """
        Re-initialize the content cache

        """
        objects = self.load()
        self._pkcache = {obj.pk: True for obj in objects}
        for obj in objects:
            for ctype in obj._content_types:
                self._typecache[ctype][obj.pk] = True

    def get(self, exclude=None, content_type=None):
        """
        Return the contents of the cache.

        Args:
            exclude (Object or list of Object): object(s) to ignore
            content_type (str or None): Filter list by a content-type. If None, don't filter.

        Returns:
            objects (list): the Objects inside this location

        """
        if content_type is not None:
            pks = self._typecache[content_type].keys()
        else:
            pks = self._pkcache.keys()
        if exclude:
            pks = set(pks) - {excl.pk for excl in make_iter(exclude)}
        try:
            return [self._idcache[pk] for pk in pks]
            # this can happen if the idmapper cache was cleared for an object
            # in the contents cache. If so we need to re-initialize and try again.
            self.init()
            try:
                return [self._idcache[pk] for pk in pks]
                # this means an actual failure of caching. Return real database match.
                logger.log_err("contents cache failed for %s." % self.obj.key)
                return self.load()

        """
        Add a new object to this location

        Args:
            obj (Object): object to add

        """
        self._pkcache[obj.pk] = obj
        for ctype in obj._content_types:
            self._typecache[ctype][obj.pk] = True

        """
        Remove object from this location

        Args:
            obj (Object): object to remove

        """
        self._pkcache.pop(obj.pk, None)
            if obj.pk in self._typecache[ctype]:
                self._typecache[ctype].pop(obj.pk, None)

        """
        Clear the contents cache and re-initialize

        """

#
# ObjectDB
#

class ObjectDB(TypedObject):
    """
    All objects in the game use the ObjectDB model to store
    data in the database. This is handled transparently through
    the typeclass system.

    Note that the base objectdb is very simple, with
    few defined fields. Use attributes to extend your
    type class with new database-stored variables.

      - name - alias for key
      - db_typeclass_path - the path to the decorating typeclass
      - db_date_created - time stamp of object creation
      - locks - lock definitions (handler)

    The ObjectDB adds the following properties:

      - account - optional connected account (always together with sessid)
      - sessid - optional connection session id (always together with account)
      - location - in-game location of object
      - home - safety location for object (handler)
      - scripts - scripts assigned to object (handler from typeclass)
      - cmdset - active cmdset on object (handler from typeclass)
      - aliases - aliases for this object (property)
      - nicks - nicknames for *other* things in Evennia (handler)
      - sessions - sessions connected to this object (see also account)
      - has_account - bool if an active account is currently connected
      - contents - other objects having this object as location
      - exits - exits from this object

    """

    #
    # ObjectDB Database model setup
    #
    #
    # db_key (also 'name' works), db_typeclass_path, db_date_created,
    # db_permissions
    #
    # These databse fields (including the inherited ones) should normally be
    # managed by their corresponding wrapper properties, named same as the
    # field, but without the db_* prefix (e.g. the db_key field is set with
    # self.key instead). The wrappers are created at the metaclass level and
    # will automatically save and cache the data more efficiently.

    # If this is a character object, the account is connected here.
    db_account = models.ForeignKey(
        verbose_name="account",
        on_delete=models.SET_NULL,
        help_text="an Account connected to this object, if any.",
    )

    # the session id associated with this account, if any
    db_sessid = models.CharField(
        max_length=32,
        validators=[validate_comma_separated_integer_list],
        verbose_name="session id",
        help_text="csv list of session ids of connected Account, if any.",
    )
    # The location in the game world. Since this one is likely
    # to change often, we set this with the 'location' property
    # to transparently handle Typeclassing.
    db_location = models.ForeignKey(
        "self",
        related_name="locations_set",
        verbose_name="game location",
    )
    # a safety location, this usually don't change much.
    db_home = models.ForeignKey(
        "self",
        related_name="homes_set",
        verbose_name="home location",
    )
    # destination of this object - primarily used by exits.
    db_destination = models.ForeignKey(
        "self",
        related_name="destinations_set",
        verbose_name="destination",
        help_text="a destination, used only by exit objects.",
    )
        "cmdset",
        help_text="optional python path to a cmdset class.",
    )

    objects = ObjectDBManager()

    # defaults
    __settingsclasspath__ = settings.BASE_OBJECT_TYPECLASS
    __defaultclasspath__ = "evennia.objects.objects.DefaultObject"
    __applabel__ = "objects"

    def contents_cache(self):
        return ContentsHandler(self)

    # cmdset_storage property handling
        """getter"""

        """setter"""
        self.db_cmdset_storage = ",".join(str(val).strip() for val in make_iter(value))
        self.save(update_fields=["db_cmdset_storage"])

        """deleter"""
        self.db_cmdset_storage = None

    # location getsetter
    def __location_get(self):
        """Get location"""
        return self.db_location

    def __location_set(self, location):
        """Set location, checking for loops and allowing dbref"""
        if isinstance(location, (str, int)):
            # allow setting of #dbref
            dbid = dbref(location, reqhash=False)
            if dbid:
                try:
                    location = ObjectDB.objects.get(id=dbid)
                except ObjectDoesNotExist:
                    # maybe it is just a name that happens to look like a dbid
                    pass
        try:

            def is_loc_loop(loc, depth=0):
                """Recursively traverse target location, trying to catch a loop."""
                if depth > 10:
                elif loc == self:
                    raise RuntimeError
                elif loc is None:
                    raise RuntimeWarning
                return is_loc_loop(loc.db_location, depth + 1)

            try:
                is_loc_loop(location)
            except RuntimeWarning:
                # we caught an infinite location loop!
                # (location1 is in location2 which is in location1 ...)
                pass

            # if we get to this point we are ready to change location

            old_location = self.db_location

            # this is checked in _db_db_location_post_save below
            self._safe_contents_update = True

            # actually set the field (this will error if location is invalid)
            self.db_location = location
            self.save(update_fields=["db_location"])

            # remove the safe flag
            del self._safe_contents_update

            # update the contents cache
            if old_location:
                old_location.contents_cache.remove(self)
            if self.db_location:
                self.db_location.contents_cache.add(self)

        except RuntimeError:
            errmsg = "Error: %s.location = %s creates a location loop." % (self.key, location)
            raise RuntimeError(errmsg)
            # raising here gives more info for now
            raise
            # errmsg = "Error (%s): %s is not a valid location." % (str(e), location)
            # raise RuntimeError(errmsg)
        return

    def __location_del(self):
        """Cleanly delete the location reference"""
        self.db_location = None
        self.save(update_fields=["db_location"])

    location = property(__location_get, __location_set, __location_del)

    def at_db_location_postsave(self, new):
        """
        This is called automatically after the location field was
        saved, no matter how. It checks for a variable
        _safe_contents_update to know if the save was triggered via
        the location handler (which updates the contents cache) or
        not.

        Args:
            new (bool): Set if this location has not yet been saved before.

        """
        if not hasattr(self, "_safe_contents_update"):
            # changed/set outside of the location handler
            if new:
                # if new, there is no previous location to worry about
                if self.db_location:
                    self.db_location.contents_cache.add(self)
            else:
                # Since we cannot know at this point was old_location was, we
                # trigger a full-on contents_cache update here.
                logger.log_warn(
                    "db_location direct save triggered contents_cache.init() for all objects!"
                )
                [o.contents_cache.init() for o in self.__dbclass__.get_all_cached_instances()]

        """Define Django meta options"""

        verbose_name = "Object"
        verbose_name_plural = "Objects"

"""
This module defines the basic `DefaultObject` and its children
`DefaultCharacter`, `DefaultAccount`, `DefaultRoom` and `DefaultExit`.
These are the (default) starting points for all in-game visible
entities.

This is the v1.0 develop version (for ref in doc building).

"""

from evennia.commands import cmdset
from evennia.server.signals import SIGNAL_EXIT_TRAVERSED
from evennia.utils import ansi, create, funcparser, logger, search
    is_iter,
    to_str,
)

_COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
# the sessid_max is based on the length of the db_sessid csv field (excluding commas)
_SESSID_MAX = 16 if _MULTISESSION_MODE in (1, 3) else 1

# init the actor-stance funcparser for msg_contents
_MSG_CONTENTS_PARSER = funcparser.FuncParser(funcparser.ACTOR_STANCE_CALLABLES)

class ObjectSessionHandler:
    """
    Handles the get/setting of the sessid comma-separated integer field

    """

        """

        Args:
            obj (Object): The object on which the handler is defined.

        """
        self._sessid_cache = []

        self._sessid_cache = list(
            set(int(val) for val in (self.obj.db_sessid or "").split(",") if val)
        )
        if any(sessid for sessid in self._sessid_cache if sessid not in evennia.SESSION_HANDLER):
            # cache is out of sync with sessionhandler! Only retain the ones in the handler.
            self._sessid_cache = [
                sessid for sessid in self._sessid_cache if sessid in evennia.SESSION_HANDLER
            ]
            self.obj.db_sessid = ",".join(str(val) for val in self._sessid_cache)
            self.obj.save(update_fields=["db_sessid"])

        """
        Get the sessions linked to this Object.

        Args:
            sessid (int, optional): A specific session id.

        Returns:
            sessions (list): The sessions connected to this object. If `sessid` is given,
                this is a list of one (or zero) elements.

        Notes:
            Aliased to `self.all()`.

        """

        if sessid:
            sessions = (
                [evennia.SESSION_HANDLER[sessid] if sessid in evennia.SESSION_HANDLER else None]
                if sessid in self._sessid_cache
                else []
            )
        else:
            sessions = [
                evennia.SESSION_HANDLER[ssid] if ssid in evennia.SESSION_HANDLER else None
                for ssid in self._sessid_cache
            ]
        if None in sessions:
            # this happens only if our cache has gone out of sync with the SessionHandler.
            return self.get(sessid=sessid)
        return sessions

        """

        Returns:

        """

    def add(self, session):
        """
        Add session to handler.

        Args:
            session (Session or int): Session or session id to add.

        Notes:
            We will only add a session/sessid if this actually also exists
            in the the core sessionhandler.

        """
        try:
            sessid = session.sessid
            sessid = session

        sessid_cache = self._sessid_cache
        if sessid in evennia.SESSION_HANDLER and sessid not in sessid_cache:
            if len(sessid_cache) >= _SESSID_MAX:
                return
            sessid_cache.append(sessid)
            self.obj.db_sessid = ",".join(str(val) for val in sessid_cache)

    def remove(self, session):
        """
        Remove session from handler.

        Args:
            session (Session or int): Session or session id to remove.

        """
        try:

        if sessid in sessid_cache:
            sessid_cache.remove(sessid)

        """
        Clear all handled sessids.

        """
        self.obj.db_sessid = None
        self.obj.save(update_fields=["db_sessid"])

        """

        Returns:

        """
        return len(self._sessid_cache)

#
# Base class to inherit from.

class DefaultObject(ObjectDB, metaclass=TypeclassBase):
    """
    This is the root typeclass object, representing all entities that
    have an actual presence in-game. DefaultObjects generally have a
    location. They can also be manipulated and looked at. Game
    entities you define should inherit from DefaultObject at some distance.

    It is recommended to create children of this class using the
    `evennia.create_object()` function rather than to initialize the class
    directly - this will both set things up and efficiently save the object
    without `obj.save()` having to be called explicitly.

    """

    # Used for sorting / filtering in inventories / room contents.
    _content_types = ("object",)

    # lockstring of newly created objects, for easy overloading.
    # Will be formatted with the appropriate attributes.
    lockstring = "control:id({account_id}) or perm(Admin);delete:id({account_id}) or perm(Admin)"

    objects = ObjectManager()

    # populated by `return_appearance`
    appearance_template = """
{header}
|c{name}|n
{desc}
{exits}{characters}{things}
{footer}
    """
    # on-object properties

        return ObjectSessionHandler(self)

    @property
    def is_connected(self):
        # we get an error for objects subscribed to channels without this
        if self.account:  # seems sane to pass on the account
            return self.account.is_connected
        else:

    @property
    def has_account(self):
        """
        Convenience property for checking if an active account is
        currently connected to this object.

        """
        return self.sessions.count()

    @property
    def is_superuser(self):
        """
        Check if user has an account, and if so, if it is a superuser.

        """
        return (
            self.db_account
            and self.db_account.is_superuser
            and not self.db_account.attributes.get("_quell")
        )

    def contents_get(self, exclude=None, content_type=None):
        """
        Returns the contents of this object, i.e. all
        objects that has this object set as its location.
        This should be publically available.

        Args:
            exclude (Object): Object to exclude from returned
                contents list
            content_type (str): A content_type to filter by. None for no
                filtering.

        Returns:
            contents (list): List of contents of this Object.

        Notes:
            Also available as the `contents` property, minus exclusion
            and filtering.

        """
        return self.contents_cache.get(exclude=exclude, content_type=content_type)

    def contents_set(self, *args):
        "You cannot replace this property"
            "{}.contents is read-only. Use obj.move_to or "
            "obj.location to move an object here.".format(self.__class__)
        )

    contents = property(contents_get, contents_set, contents_set)

    @property
    def exits(self):
        """
        Returns all exits from this object, i.e. all objects at this
        location having the property destination != `None`.

        """
        return [exi for exi in self.contents if exi.destination]

    # main methods

        self,
        use_locks=True,
        stacked=0,
    ):
        """
        Returns an Object matching a search string/condition

        Args:

                Special keywords:

                - `#<num>`: search by unique dbref. This is always a global search.
                   between multiple same-named matches. The exact form of this input
                   is given by `settings.SEARCH_MULTIMATCH_REGEX`.

            global_search (bool): Search all objects globally. This overrules 'location' data.
                exactly which objects should be searched. If this nor candidates are
                given, candidates will include caller's inventory, current location and
                all objects in the current location.
                prepended), and if that fails, it will try to return
                below.
            use_locks (bool): If True (default) - removes search results which
                fail the "search" lock.
            use_dbref (bool or None, optional): If `True`, allow to enter e.g. a query "#123"
                to find an object (globally) by its database-id 123. If `False`, the string "#123"
                will be treated like a normal string. If `None` (default), the ability to query by
                #dbref is turned on if `self` has the permission 'Builder' and is turned off
                otherwise.
            tags (list or tuple): Find objects matching one or more Tags. This should be one or
                more tag definitions on the form `tagname` or `(tagname, tagcategory)`.
            stacked (int, optional): If > 0, multimatches will be analyzed to determine if they
                only contains identical objects; these are then assumed 'stacked' and no multi-match
                error will be generated, instead `stacked` number of matches will be returned. If
                `stacked` is larger than number of matches, returns that number of matches. If
                the found stack is a mix of objects, return None and handle the multi-match
                error depending on the value of `quiet`.

        Returns:
            Object, None or list: Will return an `Object` or `None` if `quiet=False`. Will return
            a `list` with 0, 1 or more matches if `quiet=True`. If `stacked` is a positive integer,
            this list may contain all stacked identical matches.

        Notes:

        """

        if use_dbref is None:
            use_dbref = self.locks.check_lockstring(self, "_dummy:perm(Builder)")

            )

            is_string
        ):
            candidates = None

            else:
                else:

        if tags:
            tags = [(tagkey, tagcat[0] if tagcat else None) for tagkey, *tagcat in make_iter(tags)]

        results = ObjectDB.objects.search_object(
            searchdata,
            attribute_name=attribute_name,
            candidates=candidates,
            exact=exact,
            use_dbref=use_dbref,
        )

        if use_locks:
            results = [x for x in list(results) if x.access(self, "search", default=True)]

        nresults = len(results)
        if stacked > 0 and nresults > 1:
            # handle stacks, disable multimatch errors
            nstack = nresults
            if not exact:
                # we re-run exact match against one of the matches to
                # make sure we were not catching partial matches not belonging
                # to the stack
                nstack = len(
                    ObjectDB.objects.get_objs_with_key_or_alias(
                        results[0].key,
                        exact=True,
                        candidates=list(results),
                        typeclasses=[typeclass] if typeclass else None,
                    )
                )
            if nstack == nresults:
                # a valid stack, return multiple results
                return list(results)[:stacked]

        if quiet:
            # don't auto-handle error messaging
            return list(results)

        # handle error messages
            results,
            self,
        )

    def search_account(self, searchdata, quiet=False):
        """
        Simple shortcut wrapper to search for accounts, not characters.

        Args:
            searchdata (str): Search criterion - the key or dbref of the account
                to search for. If this is "here" or "me", search
                for the account connected to this object.
            quiet (bool): Returns the results as a list rather than
                echo eventual standard error messages. Default `False`.

        Returns:
            result (Account, None or list): Just what is returned depends on
                the `quiet` setting:
                    - `quiet=True`: No match or multumatch auto-echoes errors
                      to self.msg, then returns `None`. The esults are passed
                      through `settings.SEARCH_AT_RESULT` and
                      `settings.SEARCH_AT_MULTIMATCH_INPUT`. If there is a
                      unique match, this will be returned.
                    - `quiet=True`: No automatic error messaging is done, and
                      what is returned is always a list with 0, 1 or more
                      matching Accounts.

        """
                return [self.account] if quiet else self.account

        results = search.search_account(searchdata)

        if quiet:
        return _AT_SEARCH_RESULT(results, self, query=searchdata)

        """
        Do something as this object. This is never called normally,
        it's only used when wanting specifically to let an object be
        the caller of a command. It makes use of nicks of eventual
        connected accounts as well.

        Args:
            raw_string (string): Raw command input
            session (Session, optional): Session to
                return results to

        Returns:
            defer (Deferred): This is an asynchronous Twisted object that
                will not fire until the command has actually finished
                executing. To overload this one needs to attach
                callback functions to it, with addCallback(function).
                This function will be called with an eventual return
                value from the command execution. This return is not
                used at all by Evennia by default, but might be useful
                for coders intending to implement some sort of nested
                command structure.

        """

        # nick replacement - we require full-word matching.
        # do text encoding conversion
            raw_string, categories=("inputline", "channel"), include_account=True
        )
        return _CMDHANDLER(self, raw_string, callertype="object", session=session, **kwargs)

        """

        Args:
            from_obj (obj or list, optional): object that is sending. If
            session (Session or list, optional): Session or list of
                Sessions to relay data to, if any. If set, will force send
                to these sessions. If unset, who receives the message
                depends on the MULTISESSION_MODE.
            options (dict, optional): Message-specific option-value
                pairs. These will be applied at the protocol level.
            any (string or tuples): All kwarg keys not listed above
                will be treated as send-command names and their arguments
                (which can be a string or a tuple).

        Notes:

        """
        # try send hooks
                try:
        try:
            if not self.at_msg_receive(text=text, from_obj=from_obj, **kwargs):
                # if at_msg_receive returns false, we abort message to this object
                return

                try:

        # relay to session(s)

    def for_contents(self, func, exclude=None, **kwargs):
        """
        Runs a function on every object contained within this one.

        Args:
            func (callable): Function to call. This must have the
                formal call sign func(obj, **kwargs), where obj is the
                object currently being processed and `**kwargs` are
                passed on from the call to `for_contents`.
            exclude (list, optional): A list of object not to call the
                function on.

            Keyword arguments will be passed to the function for all objects.

        """
        contents = self.contents
            exclude = make_iter(exclude)
            contents = [obj for obj in contents if obj not in exclude]
            func(obj, **kwargs)

    def msg_contents(
        self,
        exclude=None,
        from_obj=None,
        mapping=None,
        raise_funcparse_errors=False,
        **kwargs,
    ):
        """
        Emits a message to all objects inside this object.

        Args:
            text (str or tuple): Message to send. If a tuple, this should be
                on the valid OOB outmessage form `(message, {kwargs})`,
                where kwargs are optional data passed to the `text`
                outputfunc. The message will be parsed for `{key}` formatting and
                `$You/$you()/$You()`, `$obj(name)`, `$conj(verb)` and `$pron(pronoun, option)`
                inline function callables.
                The `name` is taken from the `mapping` kwarg {"name": object, ...}`.
                The `mapping[key].get_display_name(looker=recipient)` will be called
                for that key for every recipient of the string.
            exclude (list, optional): A list of objects not to send to.
            from_obj (Object, optional): An object designated as the
                "sender" of the message. See `DefaultObject.msg()` for
                more info. This will be used for `$You/you` if using funcparser inlines.
            mapping (dict, optional): A mapping of formatting keys
                `{"key":<object>, "key2":<object2>,...}.
                The keys must either match `{key}` or `$You(key)/$you(key)` markers
                in the `text` string. If `<object>` doesn't have a `get_display_name`
                method, it will be returned as a string. Pass "you" to represent the caller,
                this can be skipped if `from_obj` is provided (that will then act as 'you').
            raise_funcparse_errors (bool, optional): If set, a failing `$func()` will
                lead to an outright error. If unset (default), the failing `$func()`
                will instead appear in output unparsed.

            **kwargs: Keyword arguments will be passed on to `obj.msg()` for all
                messaged objects.

        Notes:
            For 'actor-stance' reporting (You say/Name says), use the
            `$You()/$you()/$You(key)` and `$conj(verb)` (verb-conjugation)
            inline callables. This will use the respective `get_display_name()`
            for all onlookers except for `from_obj or self`, which will become
            'You/you'. If you use `$You/you(key)`, the key must be in `mapping`.

            For 'director-stance' reporting (Name says/Name says), use {key}
            syntax directly. For both `{key}` and `You/you(key)`,
            `mapping[key].get_display_name(looker=recipient)` may be called
            depending on who the recipient is.

        Examples:

            Let's assume
            - `player1.key -> "Player1"`,
              `player1.get_display_name(looker=player2) -> "The First girl"`
            - `player2.key -> "Player2"`,
              `player2.get_display_name(looker=player1) -> "The Second girl"`

            Actor-stance:
            ::

                char.location.msg_contents(
                    "$You() $conj(attack) $you(defender).",
                    from_obj=player1,
                    mapping={"defender": player2})

            - player1 will see `You attack The Second girl.`
            - player2 will see 'The First girl attacks you.'

            Director-stance:
            ::

                    "{attacker} attacks {defender}.",
                    mapping={"attacker":player1, "defender":player2})

            - player1 will see: 'Player1 attacks The Second girl.'
            - player2 will see: 'The First girl attacks Player2'

        """
        # we also accept an outcommand on the form (message, {kwargs})
        is_outcmd = text and is_iter(text)
        inmessage = text[0] if is_outcmd else text
        outkwargs = text[1] if is_outcmd and len(text) > 1 else {}
        mapping = mapping or {}
        you = from_obj or self

        if "you" not in mapping:
            mapping[you] = you

        for receiver in contents:
            # actor-stance replacements
            outmessage = _MSG_CONTENTS_PARSER.parse(
                inmessage,
                raise_errors=raise_funcparse_errors,
                return_string=True,
                caller=you,
                receiver=receiver,
                mapping=mapping,
            )

            # director-stance replacements
            outmessage = outmessage.format_map(
                {
                    key: obj.get_display_name(looker=receiver)
                    if hasattr(obj, "get_display_name")
                    else str(obj)
                    for key, obj in mapping.items()
                }
            )

            receiver.msg(text=(outmessage, outkwargs), from_obj=from_obj, **kwargs)

    def move_to(
        self,
        destination,
        emit_to_obj=None,
        use_destination=True,
        to_none=False,
        move_hooks=True,
        move_type="move",
        **kwargs,
    ):
        """
        Moves this object to a new location.

        Args:
            destination (Object): Reference to the object to move to. This
                can also be an exit object, in which case the
                destination property is used as destination.
            quiet (bool): If true, turn off the calling of the emit hooks
                (announce_move_to/from etc)
            emit_to_obj (Object): object to receive error messages
            use_destination (bool): Default is for objects to use the "destination"
                 property of destinations as the target to move to. Turning off this
                 keyword allows objects to move "inside" exit objects.
            to_none (bool): Allow destination to be None. Note that no hooks are run when
                 moving to a None location. If you want to run hooks, run them manually
                 (and make sure they can manage None locations).
            move_hooks (bool): If False, turn off the calling of move-related hooks
                (at_pre/post_move etc) with quiet=True, this is as quiet a move
                as can be done.
            move_type (str): The "kind of move" being performed, such as "teleport", "traverse",
                "get", "give", or "drop". The value can be arbitrary. By default, it only affects
                the text message generated by announce_move_to and announce_move_from by defining
                their {"type": move_type} for outgoing text. This can be used for altering
                messages and/or overloaded hook behaviors.

          Passed on to announce_move_to and announce_move_from hooks.
          Exits will set the "exit_obj" kwarg to themselves.

        Returns:
            result (bool): True/False depending on if there were problems with the move.
                    This method may also return various error messages to the
                    `emit_to_obj`.

        Notes:
            No access checks are done in this method, these should be handled before
            calling `move_to`.

            The `DefaultObject` hooks called (if `move_hooks=True`) are, in order:

             1. `self.at_pre_move(destination)` (abort if return False)
             2. `source_location.at_pre_object_leave(self, destination)` (abort if return False)
             3. `destination.at_pre_object_receive(self, source_location)` (abort if return False)
             4. `source_location.at_object_leave(self, destination)`
             5. `self.announce_move_from(destination)`
             6. (move happens here)
             7. `self.announce_move_to(source_location)`
             8. `destination.at_object_receive(self, source_location)`
             9. `self.at_post_move(source_location)`

        """

        def logerr(string="", err=None):
            """Simple log helper method"""
            self.msg("%s%s" % (string, "" if err is None else " (%s)" % err))
            return

        errtxt = _("Couldn't perform move ({err}). Contact an admin.")
            emit_to_obj = self

            if to_none:
                # immediately move to None. There can be no hooks called since
                # there is no destination to call them with.
                self.location = None
            emit_to_obj.msg(_("The destination doesn't exist."))
        if destination.destination and use_destination:
            # traverse exits
            destination = destination.destination

        # Save the old location
        source_location = self.location

        # Before the move, call pre-hooks
        if move_hooks:
            # check if we are okay to move
            try:
                if not self.at_pre_move(destination, move_type=move_type, **kwargs):
                logerr(errtxt.format(err="at_pre_move()"), err)
            # check if source location lets us go
            try:
                if source_location and not source_location.at_pre_object_leave(
                    self, destination, **kwargs
                ):
                logerr(errtxt.format(err="at_pre_object_leave()"), err)
            # check if destination accepts us
            try:
                if destination and not destination.at_pre_object_receive(
                    self, source_location, **kwargs
                ):
                logerr(errtxt.format(err="at_pre_object_receive()"), err)

        # Call hook on source location
        if move_hooks and source_location:
            try:
                source_location.at_object_leave(self, destination, move_type=move_type, **kwargs)
                logerr(errtxt.format(err="at_object_leave()"), err)

            # tell the old room we are leaving
            try:
                self.announce_move_from(destination, move_type=move_type, **kwargs)
                logerr(errtxt.format(err="announce_move_from()"), err)

        # Perform move
        try:
            self.location = destination
            logerr(errtxt.format(err="location change"), err)

            # Tell the new room we are there.
            try:
                self.announce_move_to(source_location, move_type=move_type, **kwargs)
                logerr(errtxt.format(err="announce_move_to()"), err)

            # Perform eventual extra commands on the receiving location
            # (the object has already arrived at this point)
            try:
                destination.at_object_receive(self, source_location, move_type=move_type, **kwargs)
                logerr(errtxt.format(err="at_object_receive()"), err)

        # Execute eventual extra commands on this object after moving it
        # (usually calling 'look')
            try:
                self.at_post_move(source_location, move_type=move_type, **kwargs)
                logerr(errtxt.format(err="at_post_move"), err)

    def clear_exits(self):
        """
        Destroys all of the exits and any exits pointing to this
        object as a destination.

        """
        for out_exit in [exi for exi in ObjectDB.objects.get_contents(self) if exi.db_destination]:
            out_exit.delete()
        for in_exit in ObjectDB.objects.filter(db_destination=self):
            in_exit.delete()

    def clear_contents(self):
        """
        Moves all objects (accounts/things) to their home location or
        to default home.

        """
        # Gather up everything that thinks this is its location.
        default_home_id = int(settings.DEFAULT_HOME.lstrip("#"))
        try:
            default_home = ObjectDB.objects.get(id=default_home_id)
            if default_home.dbid == self.dbid:
                # we are deleting default home!
                default_home = None
            string = _("Could not find default home '(#{dbid})'.")
            logger.log_err(string.format(dbid=default_home_id))
            default_home = None

            # Obviously, we can't send it back to here.
            if not home or (home and home.dbid == self.dbid):
                obj.home = default_home
                home = default_home

            # If for some reason it's still None...
                obj.location = None
                obj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin."))
                    "Missing default home - '{name}(#{dbid})' now has a null location.".format(
                        name=obj.name, dbid=obj.dbid
                    )
                )
                return

            if obj.has_account:
                if home:
                    string = "Your current location has ceased to exist,"
                    string += " moving you to (#{dbid})."
                    obj.msg(_(string).format(dbid=home.dbid))
                else:
                    # Famous last words: The account should never see this.
                    string = "This place should not exist ... contact an admin."
                    obj.msg(_(string))
            obj.move_to(home, move_type="teleport")

        cls,
        key: str,
        account: "DefaultAccount" = None,
        caller: "DefaultObject" = None,
        method: str = "create",
        **kwargs,
    ):
        """
        Creates a basic object with default parameters, unless otherwise

        Provides a friendlier interface to the utils.create_object() function.

        Args:

            account (Account): Account to attribute this object to.
            caller (DefaultObject): The object which is creating this one.
            description (str): Brief description for this object.
            method (str): The method of creation. Defaults to "create".

        Returns:

        """
        errors = []
        obj = None

        # Get IP address of creator, if available

        # If no typeclass supplied, use this class
        kwargs["typeclass"] = kwargs.pop("typeclass", cls)

        # Set the supplied key as the name of the intended object
        kwargs["key"] = key

        # Get a supplied description, if any
        description = kwargs.pop("description", "")

        # Create a sane lockstring if one wasn't supplied
        lockstring = kwargs.get("locks")
        if account and not lockstring:
            lockstring = cls.lockstring.format(account_id=account.id)
            kwargs["locks"] = lockstring

        # Create object
        try:
            obj = create.create_object(**kwargs)

            if ip:
            if account:
                obj.db.creator_id = account.id

            # Set description if there is none, or update it if provided
            if description or not obj.db.desc:
                desc = description if description else "You see nothing special."
                obj.db.desc = desc

            logger.log_err(e)

    def copy(self, new_key=None, **kwargs):
        """
        Makes an identical copy of this object, identical except for a
        new dbref in the database. If you want to customize the copy
        by changing some settings, use ObjectDB.object.copy_object()
        directly.

        Args:
            new_key (string): New key/name of copied object. If new_key is not
                specified, the copy will be named <old_key>_copy by default.
        Returns:
            copy (Object): A copy of this object.

        """

        def find_clone_key():
            """
            Append 01, 02 etc to obj.key. Checks next higher number in the
            same location, then adds the next number available

            returns the new clone name on the form keyXX
            """
            key = self.key
            num = sum(
                1
                for obj in self.location.contents
                if obj.key.startswith(key) and obj.key.lstrip(key).isdigit()
            )
            return "%s%03i" % (key, num)

        new_key = new_key or find_clone_key()
        new_obj = ObjectDB.objects.copy_object(self, new_key=new_key, **kwargs)
        self.at_object_post_copy(new_obj, **kwargs)
        return new_obj

    def at_object_post_copy(self, new_obj, **kwargs):
        """
        Called by DefaultObject.copy(). Meant to be overloaded. In case there's extra data not
        covered by .copy(), this can be used to deal with it.

        Args:
            new_obj (Object): The new Copy of this object.

        Returns:
            None
        """
        pass

        """
        Deletes this object.  Before deletion, this method makes sure
        to move all contained objects to their respective home
        locations, as well as clean up all exits to/from the object.

        Returns:
            noerror (bool): Returns whether or not the delete completed
                successfully or not.

        """
        global _ScriptDB
        if not _ScriptDB:
            from evennia.scripts.models import ScriptDB as _ScriptDB

        if not self.pk or not self.at_object_delete():
            # This object has already been deleted,
            # or the pre-delete check return False

        # See if we need to kick the account off.

            session.msg(_("Your character {key} has been destroyed.").format(key=self.key))
            # no need to disconnect, Account just jumps to OOC mode.
        # sever the connection (important!)
        if self.account:
            # Remove the object from playable characters list
            self.account.characters.remove(self)
            for session in self.sessions.all():
                self.account.unpuppet_object(session)

        for script in _ScriptDB.objects.get_all_scripts_on_obj(self):

        # Destroy any exits to and from this room, if any
        self.clear_exits()
        # Clear out any non-exit objects located within the object
        self.clear_contents()
        self.location = None  # this updates contents_cache for our location

        # Perform the deletion of the object

    ):
        """
        Determines if another object has permission to access this object
        in whatever way.

        Args:
          default (bool, optional): What to return if no lock of access_type was found.
          no_superuser_bypass (bool, optional): If `True`, don't skip
            lock check for superuser (be careful with this one).

          Passed on to the at_access hook along with the result of the access check.

        """
        )

    # name and return_appearance hooks

        """

        Args:
                at/getting inforamtion for this object. If not given, `.name` will be
                returned, which can in turn be used to display colored data.

        Returns:
            str: A name to display for this object. This can contain color codes and may
                be customized based on `looker`. By default this contains the `.key` of the object,
                followed by the DBREF if this user is privileged to control said object.

        Notes:
            This function could be extended to change how object names appear to users in character,
            but be wary. This function does not change an object's keys or aliases when searching,
            and is expected to produce something useful for builders.

        """
        if looker and self.locks.check_lockstring(looker, "perm(Builder)"):
            return "{}(#{})".format(self.name, self.id)

    def get_numbered_name(self, count, looker, **kwargs):
        """
        Return the numbered (singular, plural) forms of this object's key. This is by default called
        by return_appearance and is used for grouping multiple same-named of this object. Note that
        this will be called on *every* member of a group even though the plural name will be only
        shown once. Also the singular display version, such as 'an apple', 'a tree' is determined
        from this method.

        Args:
            count (int): Number of objects of this type
            looker (Object): Onlooker. Not used by default.

            key (str): Optional key to pluralize. If not given, the object's `.name` property is
                used.

        Returns:
            tuple: This is a tuple `(str, str)` with the singular and plural forms of the key
                including the count.

        Examples:
            ::
                obj.get_numbered_name(3, looker, key="foo") -> ("a foo", "three foos")

        """
        plural_category = "plural_key"
        key = kwargs.get("key", self.name)
        key = ansi.ANSIString(key)  # this is needed to allow inflection of colored names
        try:
            plural = _INFLECT.plural(key, count)
            plural = "{} {}".format(_INFLECT.number_to_words(count, threshold=12), plural)
            # this is raised by inflect if the input is not a proper noun
            plural = key
        singular = _INFLECT.an(key)
        if not self.aliases.get(plural, category=plural_category):
            # we need to wipe any old plurals/an/a in case key changed in the interrim
            self.aliases.clear(category=plural_category)
            self.aliases.add(plural, category=plural_category)
            # save the singular form as an alias here too so we can display "an egg" and also
            # look at 'an egg'.
            self.aliases.add(singular, category=plural_category)
        return singular, plural

        """
        Get the 'header' component of the object description. Called by `return_appearance`.

        Args:
        Returns:
            str: The header display string.

        """
        return ""

        """

        Args:
        Returns:

        """
        return self.db.desc or "You see nothing special."

    def get_display_exits(self, looker, **kwargs):
        """
        Get the 'exits' component of the object description. Called by `return_appearance`.

        Args:
        Returns:
            str: The exits display data.

        """

        exits = _filter_visible(self.contents_get(content_type="exit"))
        exit_names = iter_to_str(exi.get_display_name(looker, **kwargs) for exi in exits)

        return f"|wExits:|n {exit_names}" if exit_names else ""

    def get_display_characters(self, looker, **kwargs):
        """
        Get the 'characters' component of the object description. Called by `return_appearance`.

        Args:
        Returns:
            str: The character display data.

        """

        character_names = iter_to_str(
            char.get_display_name(looker, **kwargs) for char in characters
        )

        return f"\n|wCharacters:|n {character_names}" if character_names else ""

        """

        Args:
        Returns:

        """

        return f"\n|wYou see:|n {thing_names}" if thing_names else ""

        """
        Get the 'footer' component of the object description. Called by `return_appearance`.

        Args:
        Returns:
            str: The footer display string.

        """
        return ""

        """
        Final processing of the entire appearance string. Called by `return_appearance`.

        Args:
            appearance (str): The compiled appearance string.
        Returns:
            str: The final formatted output.

        """
        return appearance.strip()

        """
        Main callback used by 'look' for the object to describe itself.
        This formats a description. By default, this looks for the `appearance_template`
        string set on this class and populates it with formatting keys
            'name', 'desc', 'exits', 'characters', 'things' as well as
            (currently empty) 'header'/'footer'. Each of these values are
            retrieved by a matching method `.get_display_*`, such as `get_display_name`,
            `get_display_footer` etc.

        Args:
            looker (Object): Object doing the looking. Passed into all helper methods.
                overriding the call. This is passed into all helper methods.

        Returns:
            str: The description of this entity. By default this includes
                the entity's name, description and any contents inside it.

        Notes:
            To simply change the layout of how the object displays itself (like
            adding some line decorations or change colors of different sections),
            you can simply edit `.appearance_template`. You only need to override
            this method (and/or its helpers) if you want to change what is passed
            into the template or want the most control over output.

        """

        if not looker:
            return ""

        # populate the appearance_template string.
        return self.format_appearance(
            self.appearance_template.format(
                name=self.get_display_name(looker, **kwargs),
                desc=self.get_display_desc(looker, **kwargs),
                header=self.get_display_header(looker, **kwargs),
                footer=self.get_display_footer(looker, **kwargs),
                exits=self.get_display_exits(looker, **kwargs),
                characters=self.get_display_characters(looker, **kwargs),
                things=self.get_display_things(looker, **kwargs),
            ),
            looker,
            **kwargs,
        )

    #
    # Hook methods
    #

        """
        This is called by the typeclass system whenever an instance of
        this class is saved for the first time. It is a generic hook
        for calling the startup hooks for the various game entities.
        When overloading you generally don't overload this but
        overload the hooks called by this method.

        """
        self.at_object_creation()

            # this will only be set if the utils.create function
            # was used to create the object. We want the create
            # call's kwargs to override the values set by hooks.
                    self.db_key = "#%i" % self.dbid
            if cdict.get("location") and self.location != cdict["location"]:
                self.db_location = cdict["location"]
                updates.append("db_location")
            if cdict.get("home") and self.home != cdict["home"]:
                self.home = cdict["home"]
                updates.append("db_home")
            if cdict.get("destination") and self.destination != cdict["destination"]:
                self.destination = cdict["destination"]
                updates.append("db_destination")

                self.permissions.batch_add(*cdict["permissions"])
                self.aliases.batch_add(*cdict["aliases"])
            if cdict.get("location"):
                cdict["location"].at_object_receive(self, None)
                self.at_post_move(None)
                for key, value in cdict["nattributes"].items():

        self.basetype_posthook_setup()

    # hooks called by the game engine #

        """
        This sets up the default properties of an Object, just before
        the more general at_object_creation.

        You normally don't need to change this unless you change some
        fundamental things like names of permission groups.

        """
        # the default security setup fallback for a generic
        # object. Overload in child for a custom setup. Also creation
        # commands may set this (create an item and you should be its
        # controller, for example)

        self.locks.add(
            ";".join(
                [
                    "control:perm(Developer)",  # edit locks/permissions, delete
                    "examine:perm(Builder)",  # examine properties
                    "view:all()",  # look at object (visibility)
                    "edit:perm(Admin)",  # edit properties/attributes
                    "delete:perm(Admin)",  # delete object
                    "get:all()",  # pick up object
                    "drop:holds()",  # drop only that which you hold
                    "call:true()",  # allow to call commands on this object
                    "tell:perm(Admin)",  # allow emits to this object
                    "puppet:pperm(Developer)",
                    "teleport:true()",
                    "teleport_here:true()",
                ]
            )
        )  # lock down puppeting only to staff by default

        """
        Called once, after basetype_setup and at_object_creation. This
        should generally not be overloaded unless you are redefining
        how a room/exit/object works. It allows for basetype-like
        setup after the object is created. An example of this is
        EXITs, who need to know keys, aliases, locks etc to set up
        their exit-cmdsets.

        """
        pass

        """

        """
        pass

    def at_object_delete(self):
        """
        Called just before the database object is persistently
        delete()d from the database. If this method returns False,
        deletion is aborted.

        """

        """
        restart or reload.

        """
        pass

        """
        Called just before cmdsets on this object are requested by the
        command handler. If changes need to be done on the fly to the
        place to do it. This is called also if the object currently
        have no cmdsets.

            caller (Session, Object or Account): The caller requesting
                this cmdset.

        """
        pass

    def at_pre_puppet(self, account, session=None, **kwargs):
        """
        Called just before an Account connects to this object to puppet
        it.

        Args:
            account (Account): This is the connecting account.
            session (Session): Session controlling the connection.

        """
        pass

    def at_post_puppet(self, **kwargs):
        """
        Called just after puppeting has been completed and all
        Account<->Object links have been established.

        Args:
        Note:
            You can use `self.account` and `self.sessions.get()` to get
            account and sessions at this point; the last entry in the
            list from `self.sessions.get()` is the latest Session
            puppeting this Object.

        """
        self.msg(f"You become |w{self.key}|n.")
        self.account.db._last_puppet = self

    def at_pre_unpuppet(self, **kwargs):
        """
        Called just before beginning to un-connect a puppeting from
        this Account.

        Args:
        Note:

        """
        pass

    def at_post_unpuppet(self, account=None, session=None, **kwargs):
        """
        Called just after the Account successfully disconnected from
        this object, severing all connections.

        Args:
            account (Account): The account object that just disconnected
                from this object. This can be `None` if this is called
                automatically (such as after a cleanup operation).
            session (Session): Session id controlling the connection that
                just disconnected.

        """
        pass

        """
        restart/reboot. If you want to, for example, save non-persistent
        properties across a restart, this is the place to do it.

        """
        pass

        """

        """
        pass

        """
        This is called with the result of an access call, along with
        any kwargs used for that call. The return of this method does
        not affect the result of the lock check. It can be used e.g. to
        customize error messages in a central location or other effects
        based on the access result.

        Args:
            result (bool): The outcome of the access call.
            accessing_obj (Object or Account): The entity trying to gain access.
            access_type (str): The type of access that was requested.

            Unused by default, added for possible expandability in a game.

        """
        pass

    # hooks called when moving the object

        """

        Args:
            move_type (str): The type of move. "give", "traverse", etc.
                This is an arbitrary string provided to obj.move_to().
                Useful for altering messages or altering logic depending
                on the kind of movement.

        Returns:
            bool: If we should move or not.

        Notes:

        """

        """
        Called just before this object is about lose an object that was
        previously 'inside' it. Return False to abort move.

        Args:
            leaving_object (Object): The object that is about to leave.
            destination (Object): Where object is going to.
        Returns:
            bool: If `leaving_object` should be allowed to leave or not.

        Notes: If this method returns False, None, the move is canceled before
            it even started.

        """

    def at_pre_object_receive(self, arriving_object, source_location, **kwargs):
        """
        Called just before this object received another object. If this
        method returns `False`, the move is aborted and the moved entity
        remains where it was.

        Args:
            arriving_object (Object): The object moved into this one
            source_location (Object): Where `moved_object` came from.
                Note that this could be `None`.

        Returns:
            bool: If False, abort move and `moved_obj` remains where it was.

        """

    # deprecated alias
    at_before_move = at_pre_move

    def announce_move_from(self, destination, msg=None, mapping=None, move_type="move", **kwargs):
        """
        Called if the move is to be announced. This is
        called while we are still standing in the old
        location.

        Args:
            destination (Object): The place we are going to.
            msg (str, optional): a replacement message.
            mapping (dict, optional): additional mapping objects.

        You can override this method and call its parent with a
        message to simply change the default message.  In the string,
        you can use the following as mappings (between braces):
            object: the object which is moving.
            exit: the exit from which the object is moving (if found).
            origin: the location of the object before the move.
            destination: the location of the object after moving.

        """
            return
        if msg:
            string = msg
        else:
            string = "{object} is leaving {origin}, heading for {destination}."

        exits = [
            o for o in location.contents if o.location is location and o.destination is destination
        ]
        if not mapping:
            mapping = {}

            {
                "object": self,
                "exit": exits[0] if exits else "somewhere",
                "origin": location or "nowhere",
                "destination": destination or "nowhere",
            }
        )

            (string, {"type": move_type}), exclude=(self,), from_obj=self, mapping=mapping
        )

    def announce_move_to(self, source_location, msg=None, mapping=None, move_type="move", **kwargs):
        """
        Called after the move if the move was not quiet. At this point
        we are standing in the new location.

        Args:
            source_location (Object): The place we came from
            msg (str, optional): the replacement message if location.

        Notes:
            You can override this method and call its parent with a
            message to simply change the default message.  In the string,
            you can use the following as mappings (between braces):
                object: the object which is moving.
                exit: the exit from which the object is moving (if found).
                origin: the location of the object before the move.
                destination: the location of the object after moving.

        """

        if not source_location and self.location.has_account:
            # This was created from nowhere and added to an account's
            # inventory; it's probably the result of a create command.
            string = _("You now have {name} in your possession.").format(
                name=self.get_display_name(self.location)
            )
            self.location.msg(string)
            return

        if source_location:
            if msg:
                string = msg
            else:
                string = _("{object} arrives to {destination} from {origin}.")
        else:
            string = _("{object} arrives to {destination}.")

        origin = source_location
        destination = self.location
        exits = []
        if origin:
            exits = [
                o
                for o in destination.contents
                if o.location is destination and o.destination is origin
            ]

            {
                "origin": origin or "nowhere",
            }
        )

        destination.msg_contents(
        )

    def at_post_move(self, source_location, move_type="move", **kwargs):
        """
        Called after move has completed, regardless of quiet mode or
        not.  Allows changes to the object due to the location it is
        now in.

        Args:
            source_location (Object): Where we came from. This may be `None`.

        """
        pass

    # deprecated
    at_after_move = at_post_move

        """

        Args:

        """
        pass

        """
        Called after an object has been moved into this object.

        Args:
            moved_obj (Object): The object moved into this one

        """
        pass

        """
        This hook is responsible for handling the actual traversal,
        normally by calling
        `traversing_object.move_to(target_location)`. It is normally
        only implemented by Exit objects. If it returns False (usually
        because `move_to` returned False), `at_post_traverse` below
        should not be called and instead `at_failed_traverse` should be
        called.

        Args:

        """
        pass

    def at_post_traverse(self, traversing_object, source_location, **kwargs):
        """
        Called just after an object successfully used this object to
        traverse to another object (i.e. this object is a type of
        Exit)

        Args:
            traversing_object (Object): The object traversing us.
            source_location (Object): Where `traversing_object` came from.

        Notes:
            The target location should normally be available as `self.destination`.
        """
        pass

    at_after_traverse = at_post_traverse

        """
        This is called if an object fails to traverse this object for
        some reason.

        Args:
            traversing_object (Object): The object that failed traversing us.

        Notes:
            Using the default exits, this hook will not be called if an
            Attribute `err_traverse` is defined - this will in that case be
            read for an error string instead.

        """
        pass

        """

        Args:

        Returns:

        Notes:

        """

        """

        Args:

        Notes:

        """
        pass

    # hooks called by the default cmdset.

    def get_visible_contents(self, looker, **kwargs):
        """
        Get all contents of this object that a looker can see (whatever that means, by default it
        checks the 'view' and 'search' locks), grouped by type. Helper method to return_appearance.

        Args:
            looker (Object): The entity looking.
            **kwargs (any): Passed from `return_appearance`. Unused by default.

        Returns:
            dict: A dict of lists categorized by type. Byt default this
                contains 'exits', 'characters' and 'things'. The elements of these
                lists are the actual objects.

        """

        def filter_visible(obj_list):
            return [
                obj
                if obj != looker
                and obj.access(looker, "view")
                and obj.access(looker, "search", default=True)
            ]

        return {
            "exits": filter_visible(self.contents_get(content_type="exit")),
            "characters": filter_visible(self.contents_get(content_type="character")),
            "things": filter_visible(self.contents_get(content_type="object")),
        }

    def get_content_names(self, looker, **kwargs):
        """
        Get the proper names for all contents of this object. Helper method
        for return_appearance.

        Args:
            **kwargs (any): Passed from `return_appearance`. Passed into
                `get_display_name` for each found entity.

        Returns:
                contains 'exits', 'characters' and 'things'. The elements
                of these lists are strings - names of the objects that
                can depend on the looker and also be grouped in the case
                of multiple same-named things etc.

        Notes:
            This method shouldn't add extra coloring to the names beyond what is
            already given by the .get_display_name() (and the .name field) already.
            Per-type coloring can be applied in `return_appearance`.

        """
        # a mapping {'exits': [...], 'characters': [...], 'things': [...]}
        contents_map = self.get_visible_contents(looker, **kwargs)

        character_names = [
            char.get_display_name(looker, **kwargs) for char in contents_map["characters"]
        ]
        exit_names = [exi.get_display_name(looker, **kwargs) for exi in contents_map["exits"]]

        # group all same-named things under one name
        things = defaultdict(list)
        for thing in contents_map["things"]:
            things[thing.get_display_name(looker, **kwargs)].append(thing)

        # pluralize same-named things
        for thingname, thinglist in sorted(things.items()):

        return {"exits": exit_names, "characters": character_names, "things": thing_names}

    def at_look(self, target, **kwargs):
        """
        Called when this object performs a look. It allows to
        customize just what this means. It will not itself
        send any data.

        Args:
            target (Object): The target being looked at. This is
                commonly an object or the current location. It will
                be checked for the "view" type access.
                overriding the call. This will be passed into
                return_appearance, get_display_name and at_desc but is not used
                by default.

        Returns:
            lookstring (str): A ready-processed look string
                potentially ready to return to the looker.

        """
        if not target.access(self, "view"):
            try:
                return "Could not view '%s'." % target.get_display_name(self, **kwargs)
                return "Could not view '%s'." % target.key

        description = target.return_appearance(self, **kwargs)

        # the target's at_desc() method.
        # this must be the last reference to target so it may delete itself when acted on.
        target.at_desc(looker=self, **kwargs)

        return description

    def at_desc(self, looker=None, **kwargs):
        """
        This is called whenever someone looks at this object.

        Args:
            looker (Object, optional): The object requesting the description.

        """
        pass

    def at_pre_get(self, getter, **kwargs):
        """
        picked up.

        Args:

        Returns:

        Notes:
        """

    at_before_get = at_pre_get

    def at_get(self, getter, **kwargs):
        """
        picked up.

        Args:

        Notes:

        """
        pass

    def at_pre_give(self, giver, getter, **kwargs):
        """
        given.

        Args:

        Returns:

        Notes:

        """

    at_before_give = at_pre_give

    def at_give(self, giver, getter, **kwargs):
        """
        given.

        Args:

        Notes:

        """
        pass

    def at_pre_drop(self, dropper, **kwargs):
        """
        dropped.

        Args:

        Returns:

        Notes:

        """
        if not self.locks.get("drop"):
            # TODO: This if-statment will be removed in Evennia 1.0
        if not self.access(dropper, "drop", default=False):
            dropper.msg(f"You cannot drop {self.get_display_name(dropper)}")

    at_before_drop = at_pre_drop

    def at_drop(self, dropper, **kwargs):
        """
        dropped.

        Args:

        Notes:

        """
        pass

        """
        Before the object says something.

        This hook is by default used by the 'say' and 'whisper'
        commands as used by this command it is called before the text
        is said/whispered and can be used to customize the outgoing
        text from the object. Returning `None` aborts the command.

        Args:
            whisper (bool): If True, this is a whisper rather than
                a say. This is sent by the whisper command by default.
                Other verbal commands could use this hook in similar
                ways.
            receivers (Object or iterable): If set, this is the target or targets for the
                say/whisper.

        Returns:
            message (str): The (possibly modified) text to be spoken.

        """

    at_before_say = at_pre_say

    def at_say(
        self,
        message,
        msg_self=None,
        msg_location=None,
        msg_receivers=None,
        **kwargs,
    ):
        """
        Display the actual say (or whisper) of self.

        This hook should display the actual say/whisper of the object in its
        location.  It should both alert the object (self) and its
        location that some text is spoken.  The overriding of messages or
        `mapping` allows for simple customization of the hook without
        re-writing it completely.

        Args:
            message (str): The message to convey.
            msg_self (bool or str, optional): If boolean True, echo `message` to self. If a string,
                return that message. If False or unset, don't echo to self.
            msg_location (str, optional): The message to echo to self's location.
            receivers (Object or iterable, optional): An eventual receiver or receivers of the
                message (by default only used by whispers).
            msg_receivers(str): Specific message to pass to the receiver(s). This will parsed
                with the {receiver} placeholder replaced with the given receiver.
            whisper (bool): If this is a whisper rather than a say. Kwargs
                can be used by other verbal commands in a similar way.
            mapping (dict): Pass an additional mapping to the message.

        Notes:

            Messages can contain {} markers. These are substituted against the values
            passed in the `mapping` argument.

                msg_self = 'You say: "{speech}"'
                msg_location = '{object} says: "{speech}"'
                msg_receivers = '{object} whispers: "{speech}"'

            Supported markers by default:
                {self}: text to self-reference with (default 'You')
                {speech}: the text spoken/whispered by self.
                {object}: the object speaking.
                {receiver}: replaced with a single receiver only for strings meant for a specific
                    receiver (otherwise 'None').
                {all_receivers}: comma-separated list of all receivers,
                                 if more than one, otherwise same as receiver
                {location}: the location where object is.

        """
        msg_type = "say"
        if kwargs.get("whisper", False):
            # whisper mode
            msg_type = "whisper"
            msg_self = (
                '{self} whisper to {all_receivers}, "|n{speech}|n"'
                if msg_self is True
                else msg_self
            )
            msg_receivers = msg_receivers or '{object} whispers: "|n{speech}|n"'
            msg_location = None
        else:
            msg_self = '{self} say, "|n{speech}|n"' if msg_self is True else msg_self
            msg_location = msg_location or '{object} says, "{speech}"'
            msg_receivers = msg_receivers or message

        custom_mapping = kwargs.get("mapping", {})
        receivers = make_iter(receivers) if receivers else None

        if msg_self:
            self_mapping = {
                "self": "You",
                "object": self.get_display_name(self),
                "location": location.get_display_name(self) if location else None,
                "receiver": None,
                "all_receivers": ", ".join(recv.get_display_name(self) for recv in receivers)
                if receivers
                else None,
                "speech": message,
            }
            self_mapping.update(custom_mapping)
            self.msg(text=(msg_self.format_map(self_mapping), {"type": msg_type}), from_obj=self)

        if receivers and msg_receivers:
            receiver_mapping = {
                "object": None,
                "location": None,
                "all_receivers": None,
            }
            for receiver in make_iter(receivers):
                individual_mapping = {
                    "object": self.get_display_name(receiver),
                    "location": location.get_display_name(receiver),
                    "receiver": receiver.get_display_name(receiver),
                    "all_receivers": ", ".join(recv.get_display_name(recv) for recv in receivers)
                    if receivers
                    else None,
                }
                receiver_mapping.update(individual_mapping)
                receiver_mapping.update(custom_mapping)
                receiver.msg(
                    text=(msg_receivers.format_map(receiver_mapping), {"type": msg_type}),
                )

        if self.location and msg_location:
            location_mapping = {
                "location": location,
                "all_receivers": ", ".join(str(recv) for recv in receivers) if receivers else None,
            }
            location_mapping.update(custom_mapping)
            exclude = []
            if msg_self:
                exclude.append(self)
            if receivers:
                exclude.extend(receivers)
                text=(msg_location, {"type": msg_type}),
                from_obj=self,
                exclude=exclude,
                mapping=location_mapping,
            )

#
# Base Character object
#

class DefaultCharacter(DefaultObject):
    """
    This implements an Object puppeted by a Session - that is,
    a character avatar controlled by an account.

    """

    # Tuple of types used for indexing inventory contents. Characters generally wouldn't be in
    # anyone's inventory, but this also governs displays in room contents.
    _content_types = ("character",)
    lockstring = (
        "puppet:id({character_id}) or pid({account_id}) or perm(Developer) or pperm(Developer);"
        "delete:id({account_id}) or perm(Admin);"
        "edit:pid({account_id}) or perm(Admin)"
    )

        """
        Creates a basic Character with default parameters, unless otherwise

        Provides a friendlier interface to the utils.create_character() function.

        Args:
            key (str): Name of the new Character.
            account (obj, optional): Account to associate this Character with.
                If unset supplying None-- it will
                change the default lockset and skip creator attribution.

            All other kwargs will be passed into the create_object call.

        Returns:

        """
        errors = []
        obj = None

        # Normalize to latin characters and validate, if necessary, the supplied key
        key = cls.normalize_name(key)

        if val_err := cls.validate_name(key, account=account):
            errors.append(val_err)
            return obj, errors

        # Get permissions
        kwargs["permissions"] = kwargs.get("permissions", settings.PERMISSION_ACCOUNT_DEFAULT)

        # Get description if provided

        # Get locks if provided
        locks = kwargs.pop("locks", "")

        try:
            # Check to make sure account does not have too many chars
                avail = account.check_available_slots()
                if avail:
                    errors.append(avail)
                    return obj, errors

            # Create the Character

            if ip:
                account.characters.add(obj)

            # Add locks
            if not locks and account:
                # Allow only the character itself and the creator account to puppet this character
                # (and Developers).
                locks = cls.lockstring.format(character_id=obj.id, account_id=account.id)
            elif not locks and not account:
                locks = cls.lockstring.format(character_id=obj.id, account_id=-1)

            obj.locks.add(locks)

            # If no description is set, set a default description
                obj.db.desc = description if description else _("This is a character.")

            errors.append(f"An error occurred while creating object '{key} object.")

    def normalize_name(cls, name):
        """
        Normalize the character name prior to creating. Note that this should be refactored to
        support i18n for non-latin scripts, but as we (currently) have no bug reports requesting
        better support of non-latin character sets, requiring character names to be latinified is an
        acceptable option.

        Args:
            name (str) : The name of the character

        Returns:
            latin_name (str) : A valid name.
        """

        from evennia.utils.utils import latinify

        latin_name = latinify(name, default="X")
        return latin_name

    def validate_name(cls, name, account=None) -> typing.Optional[str]:
        """
        Validate the character name prior to creating. Overload this function to add custom validators

        Args:
        Kwargs:
            account (DefaultAccount, optional) : The account creating the character.
        Returns:
            error (str, optional) : A non-empty error message if there is a problem, otherwise False.

        """
        if account and cls.objects.filter_family(db_key__iexact=name):
            return f"|rA character named '|w{name}|r' already exists.|n"

        """
        Setup character-specific security.

        You should normally not need to overload this, but if you do,
        make sure to reproduce at least the two last commands in this
        method (unless you want to fundamentally change how a
        Character object works).

        """
            ";".join(
                [
                    "get:false()",
                    "call:false()",
                    "teleport:perm(Admin)",
                    "teleport_here:perm(Admin)",
                ]
            )  # noone can pick up the character
        )  # no commands can be called on character from outside
        # add the default cmdset
        self.cmdset.add_default(settings.CMDSET_CHARACTER, persistent=True)

        """
        We make sure to look around after a move.

        """
        if self.location.access(self, "view"):
            self.msg(text=(self.at_look(self.location), {"type": "look"}))

        """
        Return the character from storage in None location in `at_post_unpuppet`.
        Args:

        """
        if self.location is None:
            # Make sure character's location is never None before being puppeted.
            # Return to last location (or home, which should always exist)
            location = self.db.prelogout_location if self.db.prelogout_location else self.home
                self.location = location
                self.location.at_object_receive(self, None)

        if self.location:
            self.db.prelogout_location = self.location  # save location again to be sure.
        else:
                _("|r{obj} has no location and no home is set.|n").format(obj=self), session=session
            )

        """

        Args:
        Note:

        """
        self.msg(_("\nYou become |c{name}|n.\n").format(name=self.key))
        self.msg((self.at_look(self.location), {"type": "look"}), options=None)

        def message(obj, from_obj):
            obj.msg(
                _("{name} has entered the game.").format(name=self.get_display_name(obj)),
                from_obj=from_obj,
            )

        self.location.for_contents(message, exclude=[self], from_obj=self)

        """
        We stove away the character when the account goes ooc/logs off,
        otherwise the character object will remain in the room also
        after the account logged off ("headless", so to say).

        Args:
                from this object.
            session (Session): Session controlling the connection that
            reason (str): If given, adds a reason for the unpuppet. This
                is set when the user is auto-unpuppeted due to being link-dead.
        """
        if not self.sessions.count():
            # only remove this char from grid if no sessions control it anymore.
            if self.location:

                def message(obj, from_obj):
                    obj.msg(
                        _("{name} has left the game{reason}.").format(
                            name=self.get_display_name(obj),
                            reason=kwargs.get("reason", ""),
                        ),
                        from_obj=from_obj,
                    )

                self.location.for_contents(message, exclude=[self], from_obj=self)
                self.db.prelogout_location = self.location

    @property
        """

        """
        if idle:

    @property
        """

        """
        if conn:

#
# Base Room object

class DefaultRoom(DefaultObject):
    """
    This is the base room object. It's just like any Object except its
    location is always `None`.
    """

    # A tuple of strings used for indexing this object inside an inventory.
    # Generally, a room isn't expected to HAVE a location, but maybe in some games?
    _content_types = ("room",)

    )

        cls,
        key: str,
        caller: DefaultObject = None,
        **kwargs,
    ):
        """
        Creates a basic Room with default parameters, unless otherwise

        Args:
            key (str): Name of the new Room.

            account (DefaultAccount, optional): Account to associate this Room with. If
                given, it will be given specific control/edit permissions to this
                object (along with normal Admin perms). If not given, default
            method (str): The method used to create the room. Defaults to "create".

        Returns:

        """
        errors = []
        obj = None

        # Get who to send errors to
        kwargs["report_to"] = kwargs.pop("report_to", account)

        # Get description, if provided

        # get locks if provided

        try:
            # Create the Room

            # Add locks
                locks = cls.lockstring.format(id=account.id)
                locks = cls.lockstring.format(id=obj.id)

            if ip:

                obj.db.desc = description if description else _("This is a room.")

        """
        Simple room setup setting locks to make sure the room
        cannot be picked up.

        """

            ";".join(["get:false()", "puppet:false()", "teleport:false()", "teleport_here:true()"])
        )  # would be weird to puppet a room ...

#
# Default Exit command, used by the base exit object
#

class ExitCommand(_COMMAND_DEFAULT_CLASS):
    """
    This is a command that simply cause the caller to traverse
    the object it is attached to.

    """

    obj = None

        """
        Default exit traverse if no syscommand is defined.
        """

        if self.obj.access(self.caller, "traverse"):
            # we may traverse the exit.
            self.obj.at_traverse(self.caller, self.obj.destination)
            SIGNAL_EXIT_TRAVERSED.send(sender=self.obj, traverser=self.caller)
        else:
            # exit is locked
            if self.obj.db.err_traverse:
                # if exit has a better error message, let's use it.
                self.caller.msg(self.obj.db.err_traverse)
            else:
                # No shorthand error message. Call hook.
                self.obj.at_failed_traverse(self.caller)

        """
        Shows a bit of information on where the exit leads.

        Args:
            caller (Object): The object (usually a character) that entered an ambiguous command.

        Returns:
            A string with identifying information to disambiguate the command, conventionally with a
            preceding space.

        """
        if self.obj.destination:
            return " (exit to %s)" % self.obj.destination.get_display_name(caller, **kwargs)
        else:
            return " (%s)" % self.obj.get_display_name(caller, **kwargs)

#
# Base Exit object

class DefaultExit(DefaultObject):
    """
    This is the base exit object - it connects a location to another.
    This is done by the exit assigning a "command" on itself with the
    same name as the exit object (to do this we need to remember to
    re-create the command when the object is cached since it must be
    created dynamically depending on what the exit is called). This
    command (which has a high priority) will thus allow us to traverse
    exits simply by giving the exit-object's name on its own.

    """

    _content_types = ("exit",)
    exit_command = ExitCommand
    priority = 101

    # lockstring of newly created exits, for easy overloading.
    )

    # Helper classes and methods to implement the Exit. These need not
    # be overloaded unless one want to change the foundation for how
    # Exits work. See the end of the class for hook methods to overload.

    def create_exit_cmdset(self, exidbobj):
        """
        Helper function for creating an exit command set + command.

        The command of this cmdset has the same name as the Exit
        object and allows the exit to react when the account enter the
        exit's name, triggering the movement between rooms.

        Args:
            exidbobj (Object): The DefaultExit object to base the command on.

        """

        # create an exit command. We give the properties here,
        # to always trigger metaclass preparations
        cmd = self.exit_command(
            key=exidbobj.db_key.strip().lower(),
            aliases=exidbobj.aliases.all(),
            locks=str(exidbobj.locks),
            auto_help=False,
            destination=exidbobj.db_destination,
            arg_regex=r"^$",
            is_exit=True,
            obj=exidbobj,
        )
        # create a cmdset
        exit_cmdset = cmdset.CmdSet(None)
        exit_cmdset.key = "ExitCmdSet"
        exit_cmdset.priority = self.priority
        exit_cmdset.duplicates = True
        # add command to cmdset
        exit_cmdset.add(cmd)
        return exit_cmdset

    # Command hooks

        cls,
        key: str,
        location: DefaultRoom = None,
        destination: DefaultRoom = None,
        **kwargs,
    ) -> tuple[typing.Optional["DefaultExit"], list[str]]:
        """
        Creates a basic Exit with default parameters, unless otherwise

        Args:
            key (str): Name of the new Exit, as it should appear from the
                source room.
            location (Room): The room to create this exit in.

            account (AccountDB): Account to associate this Exit with.
            caller (ObjectDB): The Object creating this Object.
            destination (Room): The room to which this exit should go.

        Returns:
            exit (Object): A newly created Room of the given typeclass.

        """
        errors = []
        obj = None

        # Set to/from rooms
        kwargs["location"] = location
        kwargs["destination"] = destination

        locks = kwargs.get("locks", "")

        try:
            # Create the Exit

            # Set appropriate locks

            if ip:

                obj.db.desc = description if description else _("This is an exit.")

        """
        Setup exit-security

        You should normally not need to overload this - if you do make
        sure you include all the functionality in this method.

        """

        # setting default locks (overload these in at_object_creation()
            ";".join(
                [
                    "puppet:false()",  # would be weird to puppet an exit ...
                    "traverse:all()",  # who can pass through exit by default
                    "get:false()",  # noone can pick up the exit
                    "teleport:false()",
                    "teleport_here:false()",
                ]
            )
        )

        # an exit should have a destination - try to make sure it does
        if self.location and not self.destination:
            self.destination = self.location

        """
        has no cmdsets.

          force_init (bool): If `True`, force a re-build of the cmdset
            (for example to update aliases).

        """

        if "force_init" in kwargs or not self.cmdset.has_cmdset("ExitCmdSet", must_be_default=True):
            # we are resetting, or no exit-cmdset was set. Create one dynamically.
            self.cmdset.add_default(self.create_exit_cmdset(self), persistent=False)

        """
        This is called when this objects is re-loaded from cache. When
        that happens, we make sure to remove any old ExitCmdSet cmdset
        (this most commonly occurs when renaming an existing exit)
        """
        self.cmdset.remove_default()

        """

        Args:

        """
        source_location = traversing_object.location
        if traversing_object.move_to(target_location, move_type="traverse", exit_obj=self):
            self.at_post_traverse(traversing_object, source_location)
        else:
            if self.db.err_traverse:
                traversing_object.msg(self.db.err_traverse)
            else:
                self.at_failed_traverse(traversing_object)

        """
        Overloads the default hook to implement a simple default error message.

        Args:

        Notes:

        """
        traversing_object.msg(_("You cannot go there."))

    def get_return_exit(self, return_all=False):
        """
        Get the exits that pair with this one in its destination room
        (i.e. returns to its location)

        Args:
            return_all (bool): Whether to return available results as a
                               list or single matching exit.

        Returns:
            queryset or exit (Exit): The matching exit(s).
        """
        query = ObjectDB.objects.filter(db_location=self.destination, db_destination=self.location)
        if return_all:
        return query.first()

"""

OLC Prototype menu nodes

"""

import re

from evennia.locks.lockhandler import get_all_lockfuncs
from evennia.utils import evmore, utils
from evennia.utils.evmenu import EvMenu, list_node

#
# OLC Prototype design menu
#

_MENU_CROP_WIDTH = 15
_MENU_ATTR_LITERAL_EVAL_ERROR = (
    "|rCritical Python syntax error in your value. Only primitive Python structures are allowed.\n"
    "You also need to use correct Python syntax. Remember especially to put quotes around all "
    "strings inside lists and dicts.|n"
)

def _get_menu_prototype(caller):
    """Return currently active menu prototype."""
    if hasattr(caller.ndb._menutree, "olc_prototype"):
        prototype = caller.ndb._menutree.olc_prototype
    if not prototype:
        caller.ndb._menutree.olc_prototype = prototype = {}
        caller.ndb._menutree.olc_new = True
    return prototype

def _get_flat_menu_prototype(caller, refresh=False, validate=False):
    """Return prototype where parent values are included"""
    flat_prototype = None
    if not refresh and hasattr(caller.ndb._menutree, "olc_flat_prototype"):
        flat_prototype = caller.ndb._menutree.olc_flat_prototype
    if not flat_prototype:
        prot = _get_menu_prototype(caller)
        caller.ndb._menutree.olc_flat_prototype = flat_prototype = spawner.flatten_prototype(
            prot, validate=validate
        )
    return flat_prototype

def _get_unchanged_inherited(caller, protname):
    """Return prototype values inherited from parent(s), which are not replaced in child"""
    prototype = _get_menu_prototype(caller)
    if protname in prototype:
        return protname[protname], False
    else:
        flattened = _get_flat_menu_prototype(caller)
        if protname in flattened:
            return protname[protname], True
    return None, False

def _set_menu_prototype(caller, prototype):
    """Set the prototype with existing one"""
    caller.ndb._menutree.olc_prototype = prototype
    caller.ndb._menutree.olc_new = False

def _is_new_prototype(caller):
    """Check if prototype is marked as new or was loaded from a saved one."""
    return hasattr(caller.ndb._menutree, "olc_new")

def _format_option_value(prop, required=False, prototype=None, cropper=None):
    """
    Format wizard option values.

    Args:
        prop (str): Name or value to format.
        required (bool, optional): The option is required.
        prototype (dict, optional): If given, `prop` will be considered a key in this prototype.
        cropper (callable, optional): A function to crop the value to a certain width.

    Returns:
        value (str): The formatted value.
    """
    if prototype is not None:
        prop = prototype.get(prop, "")

    out = prop
    if callable(prop):
        if hasattr(prop, "__name__"):
            out = "<{}>".format(prop.__name__)
        else:
            out = repr(prop)
    if utils.is_iter(prop):
        out = ", ".join(str(pr) for pr in prop)
    if not out and required:
        out = "|runset"
    if out:
        return " ({}|n)".format(cropper(out) if cropper else utils.crop(out, _MENU_CROP_WIDTH))
    return ""

def _set_prototype_value(caller, field, value, parse=True):
    """Set prototype's field in a safe way."""
    prototype[field] = value

def _set_property(caller, raw_string, **kwargs):
    """
    Add or update a property. To be called by the 'goto' option variable.

    Args:
        caller (Object, Account): The user of the wizard.
        raw_string (str): Input from user on given node - the new value to set.

        test_parse (bool): If set (default True), parse raw_string for protfuncs and obj-refs and
            try to run result through literal_eval. The parser will be run in 'testing' mode and any
            parsing errors will shown to the user. Note that this is just for testing, the original
            given string will be what is inserted.
        prop (str): Property name to edit with `raw_string`.
        processor (callable): Converts `raw_string` to a form suitable for saving.
        next_node (str): Where to redirect to after this has run.

    Returns:
        next_node (str): Next node to go to.

    """
    prop = kwargs.get("prop", "prototype_key")
    processor = kwargs.get("processor", None)
    next_node = kwargs.get("next_node", None)

    if callable(processor):
        try:
            value = processor(raw_string)
                "Could not set {prop} to {value} ({err})".format(
                    prop=prop.replace("_", "-").capitalize(), value=raw_string, err=str(err)
                )
            )
            # this means we'll re-run the current node.
    else:
        value = raw_string

    if not value:
        return next_node

    prototype = _set_prototype_value(caller, prop, value)

    try:
        # TODO simple way to get rid of the u'' markers in list reprs, remove this when on py3.
        repr_value = json.dumps(value)
        repr_value = value

    out = [" Set {prop} to {value} ({typ}).".format(prop=prop, value=repr_value, typ=type(value))]

    if kwargs.get("test_parse", True):
        out.append(" Simulating prototype-func parsing ...")
        parsed_value = protlib.protfunc_parser(value, testing=True, prototype=prototype)
        if parsed_value != value:
            out.append(
                " |g(Example-)value when parsed ({}):|n {}".format(type(parsed_value), parsed_value)
            )
        else:
            out.append(" |gNo change when parsed.")

    caller.msg("\n".join(out))

    return next_node

def _wizard_options(curr_node, prev_node, next_node, color="|W", search=False):
    """Creates default navigation options available in the wizard."""
    if prev_node:
            {
                "key": ("|wB|Wack", "b"),
                "desc": "{color}({node})|n".format(color=color, node=prev_node.replace("_", "-")),
                "goto": "node_{}".format(prev_node),
            }
        )
    if next_node:
            {
                "key": ("|wF|Worward", "f"),
                "desc": "{color}({node})|n".format(color=color, node=next_node.replace("_", "-")),
                "goto": "node_{}".format(next_node),
            }
        )

    options.append({"key": ("|wI|Wndex", "i"), "goto": "node_index"})

    if curr_node:
            {
                "key": ("|wV|Walidate prototype", "validate", "v"),
                "goto": ("node_validate_prototype", {"back": curr_node}),
            }
        )
        if search:
            options.append(
                {
                    "key": ("|wSE|Warch objects", "search object", "search", "se"),
                    "goto": ("node_search_object", {"back": curr_node}),
                }
            )

    return options

def _set_actioninfo(caller, string):
    caller.ndb._menutree.actioninfo = string

def _path_cropper(pythonpath):
    "Crop path to only the last component"
    return pythonpath.split(".")[-1]

def _validate_prototype(prototype):
    """Run validation on prototype"""

    txt = protlib.prototype_to_str(prototype)
    errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)"
    err = False
    try:
        # validate, don't spawn
        spawner.spawn(prototype, only_validate=True)
    except RuntimeError as exc:
        errors = "\n\n|r{}|n".format(exc)
        err = True
    except RuntimeWarning as exc:
        errors = "\n\n|y{}|n".format(exc)
        err = True

    text = txt + errors
    return err, text

def _format_protfuncs():
    out = []
    sorted_funcs = [
        (key, func)
        for key, func in sorted(protlib.FUNC_PARSER.callables.items(), key=lambda tup: tup[0])
    ]
    for protfunc_name, protfunc in sorted_funcs:
        out.append(
            "- |c${name}|n - |W{docs}".format(
                name=protfunc_name,
                docs=utils.justify(protfunc.__doc__.strip(), align="l", indent=10).strip(),
            )
        )
    return "\n       ".join(out)

def _format_lockfuncs():
    out = []
        (key, func) for key, func in sorted(get_all_lockfuncs().items(), key=lambda tup: tup[0])
    ]
    for lockfunc_name, lockfunc in sorted_funcs:
        doc = (lockfunc.__doc__ or "").strip()
                name=lockfunc_name, docs=utils.justify(doc, align="l", indent=10).strip()
            )
        )
    return "\n".join(out)

def _format_list_actions(*args, **kwargs):
    """Create footer text for nodes with extra list actions

    Args:
        actions (str): Available actions. The first letter of the action name will be assumed
            to be a shortcut.
        prefix (str): Default prefix to use.
    Returns:
        string (str): Formatted footer for adding to the node text.

    """
    actions = []
    prefix = kwargs.get("prefix", "|WSelect with |w<num>|W. Other actions:|n ")
    for action in args:
        actions.append("|w{}|n|W{} |w<num>|n".format(action[0], action[1:]))
    return prefix + " |W|||n ".join(actions)

def _get_current_value(caller, keyname, comparer=None, formatter=str, only_inherit=False):
    """
    Return current value, marking if value comes from parent or set in this prototype.

    Args:
        keyname (str): Name of prototoype key to get current value of.
        comparer (callable, optional): This will be called as comparer(prototype_value,
            flattened_value) and is expected to return the value to show as the current
            or inherited one. If not given, a straight comparison is used and what is returned
            depends on the only_inherit setting.
        formatter (callable, optional)): This will be called with the result of comparer.
        only_inherit (bool, optional): If a current value should only be shown if all
            the values are inherited from the prototype parent (otherwise, show an empty string).
    Returns:
        current (str): The current value.

    """

    def _default_comparer(protval, flatval):
        if only_inherit:
            return "" if protval else flatval
        else:
            return protval if protval else flatval

    if not callable(comparer):
        comparer = _default_comparer

    prot = _get_menu_prototype(caller)
    flat_prot = _get_flat_menu_prototype(caller)

    out = ""
    if keyname in prot:
        if keyname in flat_prot:
            out = formatter(comparer(prot[keyname], flat_prot[keyname]))
            if only_inherit:
                if str(out).strip():
                    return "|WCurrent|n {} |W(|binherited|W):|n {}".format(keyname, out)
                return ""
            else:
                if out:
                    return "|WCurrent|n {}|W:|n {}".format(keyname, out)
                return "|W[No {} set]|n".format(keyname)
        elif only_inherit:
            return ""
        else:
            out = formatter(prot[keyname])
            return "|WCurrent|n {}|W:|n {}".format(keyname, out)
    elif keyname in flat_prot:
        out = formatter(flat_prot[keyname])
        if out:
            return "|WCurrent|n {} |W(|n|binherited|W):|n {}".format(keyname, out)
        else:
            return ""
    elif only_inherit:
        return ""
    else:
        return "|W[No {} set]|n".format(keyname)

def _default_parse(raw_inp, choices, *args):
    """
    Helper to parse default input to a node decorated with the node_list decorator on
    the form l1, l 2, look 1, etc. Spaces are ignored, as is case.

    Args:
        raw_inp (str): Input from the user.
        choices (list): List of available options on the node listing (list of strings).
        args (tuples): The available actions, each specifed as a tuple (name, alias, ...)
    Returns:
        choice (str): A choice among the choices, or None if no match was found.
        action (str): The action operating on the choice, or None.

    """
    raw_inp = raw_inp.lower().strip()
    mapping = {t.lower(): tup[0] for tup in args for t in tup}
    match = re.match(r"(%s)\s*?(\d+)$" % "|".join(mapping.keys()), raw_inp)
    if match:
        action = mapping.get(match.group(1), None)
        num = int(match.group(2)) - 1
        num = num if 0 <= num < len(choices) else None
        if action is not None and num is not None:
            return choices[num], action

# Menu nodes ------------------------------

# helper nodes

# validate prototype (available as option from all nodes)

def node_validate_prototype(caller, raw_string, **kwargs):
    """General node to view and validate a protototype"""
    prototype = _get_flat_menu_prototype(caller, refresh=True, validate=False)
    prev_node = kwargs.get("back", "index")

    _, text = _validate_prototype(prototype)

    helptext = """
    The validator checks if the prototype's various values are on the expected form. It also tests
    any $protfuncs.

    """

    text = (text, helptext)

    options = _wizard_options(None, prev_node, None)
    options.append({"key": "_default", "goto": "node_" + prev_node})

# node examine_entity

def node_examine_entity(caller, raw_string, **kwargs):
    """
    General node to view a text and then return to previous node.  Kwargs should contain "text" for
    the text to show and 'back" pointing to the node to return to.
    """
    text = kwargs.get("text", "Nothing was found here.")
    helptext = "Use |wback|n to return to the previous node."

# node object_search

def _search_object(caller):
    "update search term based on query stored on menu; store match too"
    try:
        searchstring = caller.ndb._menutree.olc_search_object_term.strip()
        caller.ndb._menutree.olc_search_object_matches = []
        return []

    if not searchstring:
        caller.msg("Must specify a search criterion.")
        return []

    is_dbref = utils.dbref(searchstring)
    is_account = searchstring.startswith("*")

    if is_dbref or is_account:
        if is_dbref:
            # a dbref search
            results = caller.search(searchstring, global_search=True, quiet=True)
        else:
            # an account search
            searchstring = searchstring.lstrip("*")
            results = caller.search_account(searchstring, quiet=True)
    else:
        keyquery = Q(db_key__istartswith=searchstring)
        aliasquery = Q(
            db_tags__db_key__istartswith=searchstring, db_tags__db_tagtype__iexact="alias"
        )
        results = ObjectDB.objects.filter(keyquery | aliasquery).distinct()

    caller.msg("Searching for '{}' ...".format(searchstring))
    caller.ndb._menutree.olc_search_object_matches = results
    return ["{}(#{})".format(obj.key, obj.id) for obj in results]

def _object_search_select(caller, obj_entry, **kwargs):
    choices = kwargs["available_choices"]
    num = choices.index(obj_entry)
    matches = caller.ndb._menutree.olc_search_object_matches
    obj = matches[num]

    if not obj.access(caller, "examine"):
        caller.msg("|rYou don't have 'examine' access on this object.|n")
        del caller.ndb._menutree.olc_search_object_term
        return "node_search_object"

    prot = spawner.prototype_from_object(obj)
    txt = protlib.prototype_to_str(prot)
    return "node_examine_entity", {"text": txt, "back": "search_object"}

def _object_search_actions(caller, raw_inp, **kwargs):
    "All this does is to queue a search query"
    obj_entry, action = _default_parse(
        raw_inp, choices, ("examine", "e"), ("create prototype from object", "create", "c")
    )

    raw_inp = raw_inp.strip()

    if obj_entry:
        num = choices.index(obj_entry)
        matches = caller.ndb._menutree.olc_search_object_matches
        obj = matches[num]
        prot = spawner.prototype_from_object(obj)

        if action == "examine":
                caller.msg("\n|rYou don't have 'examine' access on this object.|n")
                del caller.ndb._menutree.olc_search_object_term
                return "node_search_object"

            txt = protlib.prototype_to_str(prot)
            return "node_examine_entity", {"text": txt, "back": "search_object"}
        else:
            # load prototype

            if not obj.access(caller, "edit"):
                caller.msg("|rYou don't have access to do this with this object.|n")

            _set_menu_prototype(caller, prot)
            caller.msg("Created prototype from object.")
            return "node_index"
    elif raw_inp:
        caller.ndb._menutree.olc_search_object_term = raw_inp
        return "node_search_object", kwargs
    else:
        # empty input - exit back to previous node
        prev_node = "node_" + kwargs.get("back", "index")
        return prev_node

@list_node(_search_object, _object_search_select)
def node_search_object(caller, raw_inp, **kwargs):
    """
    Node for searching for an existing object.
    """
    try:
    nmatches = len(matches)

        text = """
        Found {num} match{post}.

         (|RWarning: creating a prototype will |roverwrite|r |Rthe current prototype!)|n""".format(
            num=nmatches, post="es" if nmatches > 1 else ""
        )
        _set_actioninfo(
            caller,
            _format_list_actions("examine", "create prototype from object", prefix="Actions: "),
        )
    else:
        text = "Enter search criterion."

        You can search objects by specifying partial key, alias or its exact #dbref. Use *query to
        search for an Account instead.

        Once having found any matches you can choose to examine it or use |ccreate prototype from
        object|n. If doing the latter, a prototype will be calculated from the selected object and
        loaded as the new 'current' prototype. This is useful for having a base to build from but be
        careful you are not throwing away any existing, unsaved, prototype work!
        """

    options.append({"key": "_default", "goto": (_object_search_actions, {"back": prev_node})})

# main index (start page) node

def node_index(caller):

    text = """
       |c --- Prototype wizard --- |n
       %s

       A |cprototype|n is a 'template' for |wspawning|n an in-game entity. A field of the prototype
       can either be hard-coded, left empty or scripted using |w$protfuncs|n - for example to
       randomize the value every time a new entity is spawned. The fields whose names start with
       'Prototype-' are not fields on the object itself but are used for prototype-inheritance, or
       when saving and loading.

       Select prototype field to edit. If you are unsure, start from [|w1|n]. Enter [|wh|n]elp at
       any menu node for more info.

       """
    helptxt = """
       |c- prototypes |n

       A prototype is really just a Python dictionary. When spawning, this dictionary is essentially
       passed into `|wevennia.utils.create.create_object(**prototype)|n` to create a new object. By
       using different prototypes you can customize instances of objects without having to do code
       changes to their typeclass (something which requires code access). The classical example is
       to spawn goblins with different names, looks, equipment and skill, each based on the same
       `Goblin` typeclass.

       At any time you can [|wV|n]alidate that the prototype works correctly and use it to
       [|wSP|n]awn a new entity. You can also [|wSA|n]ve|n your work, [|wLO|n]oad an existing
       prototype to [|wSE|n]arch for existing objects to use as a base. Use [|wL|n]ook to re-show a
       menu node. [|wQ|n]uit will always exit the menu and [|wH|n]elp will show context-sensitive
       help.

       |c- $protfuncs |n

       Prototype-functions (protfuncs) allow for limited scripting within a prototype. These are
       entered as a string $funcname(arg, arg, ...) and are evaluated |wat the time of spawning|n
       only.  They can also be nested for combined effects.

       {pfuncs}
       """.format(
        pfuncs=_format_protfuncs()
    )

    # If a prototype is being edited, show its key and
    # prototype_key under the title
    loaded_prototype = ""
    if "prototype_key" in prototype or "key" in prototype:
        loaded_prototype = " --- Editing: |y{}({})|n --- ".format(
            prototype.get("key", ""), prototype.get("prototype_key", "")
        )
    text = text % (loaded_prototype)

    text = (text, helptxt)

        {
            "desc": "|WPrototype-Key|n|n{}".format(
                _format_option_value("Key", "prototype_key" not in prototype, prototype, None)
            ),
            "goto": "node_prototype_key",
        }
    )
    for key in (
        "Prototype_Parent",
        "Typeclass",
        "Key",
        "Aliases",
        "Attrs",
        "Tags",
        "Locks",
        "Permissions",
        "Location",
        "Home",
        "Destination",
    ):
        required = False
        cropper = None
        if key in ("Prototype_Parent", "Typeclass"):
            required = ("prototype_parent" not in prototype) and ("typeclass" not in prototype)
        if key == "Typeclass":
            cropper = _path_cropper
            {
                "desc": "{}{}|n{}".format(
                    "|W" if key == "Prototype_Parent" else "|w",
                    key.replace("_", "-"),
                    _format_option_value(key, required, prototype, cropper=cropper),
                ),
                "goto": "node_{}".format(key.lower()),
            }
        )
    required = False
    for key in ("Desc", "Tags", "Locks"):
            {
                "desc": "|WPrototype-{}|n|n{}".format(
                    key, _format_option_value(key, required, prototype, None)
                ),
                "goto": "node_prototype_{}".format(key.lower()),
            }
        )

        (
            {"key": ("|wV|Walidate prototype", "validate", "v"), "goto": "node_validate_prototype"},
            {"key": ("|wSA|Wve prototype", "save", "sa"), "goto": "node_prototype_save"},
            {"key": ("|wSP|Wawn prototype", "spawn", "sp"), "goto": "node_prototype_spawn"},
            {"key": ("|wLO|Wad prototype", "load", "lo"), "goto": "node_prototype_load"},
            {"key": ("|wSE|Warch objects|n", "search", "se"), "goto": "node_search_object"},
        )
    )

# prototype_key node

def _check_prototype_key(caller, key):
    old_prototype = protlib.search_prototype(key)
    olc_new = _is_new_prototype(caller)
    key = key.strip().lower()
    if old_prototype:
        old_prototype = old_prototype[0]
        # we are starting a new prototype that matches an existing
        if not caller.locks.check_lockstring(
            caller, old_prototype["prototype_locks"], access_type="edit"
        ):
            # return to the node_prototype_key to try another key
                "Prototype '{key}' already exists and you don't "
                "have permission to edit it.".format(key=key)
            )
            return "node_prototype_key"
        elif olc_new:
            # we are selecting an existing prototype to edit. Reset to index.
            del caller.ndb._menutree.olc_new
            caller.ndb._menutree.olc_prototype = old_prototype
            caller.msg("Prototype already exists. Reloading.")

    return _set_property(caller, key, prop="prototype_key")

def node_prototype_key(caller):
    text = """
        The |cPrototype-Key|n uniquely identifies the prototype and is |wmandatory|n. It is used to
        find and use the prototype to spawn new entities. It is not case sensitive.

        (To set a new value, just write it and press enter)

        {current}""".format(
        current=_get_current_value(caller, "prototype_key")
    )

        The prototype-key is not itself used when spawnng the new object, but is only used for
        managing, storing and loading the prototype. It must be globally unique, so existing keys
        will be checked before a new key is accepted. If an existing key is picked, the existing
        prototype will be loaded.
        """

    options = _wizard_options("prototype_key", "index", "prototype_parent")
    options.append({"key": "_default", "goto": _check_prototype_key})

# prototype_parents node

def _all_prototype_parents(caller):
    """Return prototype_key of all available prototypes for listing in menu"""
    return [
        prototype["prototype_key"]
        for prototype in protlib.search_prototype()
        if "prototype_key" in prototype
    ]

def _prototype_parent_actions(caller, raw_inp, **kwargs):
    """Parse the default Convert prototype to a string representation for closer inspection"""
    choices = kwargs.get("available_choices", [])
    prototype_parent, action = _default_parse(
        raw_inp, choices, ("examine", "e", "l"), ("add", "a"), ("remove", "r", "delete", "d")
    )

    if prototype_parent:
        # a selection of parent was made
        prototype_parent = protlib.search_prototype(key=prototype_parent)[0]
        prototype_parent_key = prototype_parent["prototype_key"]

        # which action to apply on the selection
            # examine the prototype
            txt = protlib.prototype_to_str(prototype_parent)
            kwargs["text"] = txt
            kwargs["back"] = "prototype_parent"
            return "node_examine_entity", kwargs
        elif action == "add":
            # add/append parent
            prot = _get_menu_prototype(caller)
            current_prot_parent = prot.get("prototype_parent", None)
            if current_prot_parent:
                current_prot_parent = utils.make_iter(current_prot_parent)
                if prototype_parent_key in current_prot_parent:
                    caller.msg("Prototype_parent {} is already used.".format(prototype_parent_key))
                    return "node_prototype_parent"
                else:
                    current_prot_parent.append(prototype_parent_key)
                    caller.msg("Add prototype parent for multi-inheritance.")
            else:
                current_prot_parent = prototype_parent_key
            try:
                if prototype_parent:
                    spawner.flatten_prototype(prototype_parent, validate=True)
                else:
                    raise RuntimeError("Not found.")
                    "Selected prototype-parent {} "
                    "caused Error(s):\n|r{}|n".format(prototype_parent, err)
                )
                return "node_prototype_parent"
            _set_prototype_value(caller, "prototype_parent", current_prot_parent)
            _get_flat_menu_prototype(caller, refresh=True)
        elif action == "remove":
            # remove prototype parent
                try:
                    current_prot_parent.remove(prototype_parent_key)
                    _set_prototype_value(caller, "prototype_parent", current_prot_parent)
                    _get_flat_menu_prototype(caller, refresh=True)
                    caller.msg("Removed prototype parent {}.".format(prototype_parent_key))
                        "|rPrototype-parent {} could not be removed.".format(prototype_parent_key)
                    )
        return "node_prototype_parent"

def _prototype_parent_select(caller, new_parent):
    ret = None
    prototype_parent = protlib.search_prototype(new_parent)
    try:
        if prototype_parent:
            spawner.flatten_prototype(prototype_parent[0], validate=True)
        else:
            raise RuntimeError("Not found.")
    except RuntimeError as err:
            "Selected prototype-parent {} " "caused Error(s):\n|r{}|n".format(new_parent, err)
        )
    else:
        ret = _set_property(
            caller,
            new_parent,
            prop="prototype_parent",
            processor=str,
            next_node="node_prototype_parent",
        )
        _get_flat_menu_prototype(caller, refresh=True)
        caller.msg("Selected prototype parent |c{}|n.".format(new_parent))
    return ret

@list_node(_all_prototype_parents, _prototype_parent_select)
def node_prototype_parent(caller):

    prot_parent_keys = prototype.get("prototype_parent")

    text = """
        The |cPrototype Parent|n allows you to |winherit|n prototype values from another named
        prototype (given as that prototype's |wprototype_key|n).  If not changing these values in
        the current prototype, the parent's value will be used. Pick the available prototypes below.

        Note that somewhere in the prototype's parentage, a |ctypeclass|n must be specified. If no
        parent is given, this prototype must define the typeclass (next menu node).

        {current}
        """
        Prototypes can inherit from one another. Changes in the child replace any values set in a
        parent. The |wtypeclass|n key must exist |wsomewhere|n in the parent chain for the
        prototype to be valid.
        """

    _set_actioninfo(caller, _format_list_actions("examine", "add", "remove"))

    ptexts = []
    if prot_parent_keys:
        for pkey in utils.make_iter(prot_parent_keys):
            prot_parent = protlib.search_prototype(pkey)
            if prot_parent:
                prot_parent = prot_parent[0]
                ptexts.append(
                    "|c -- {pkey} -- |n\n{prot}".format(
                        pkey=pkey, prot=protlib.prototype_to_str(prot_parent)
                    )
                )
            else:
                ptexts.append("Prototype parent |r{pkey} was not found.".format(pkey=pkey))

    if not ptexts:
        ptexts.append("[No prototype_parent set]")

    text = text.format(current="\n\n".join(ptexts))

    options = _wizard_options("prototype_parent", "prototype_key", "typeclass", color="|W")
    options.append({"key": "_default", "goto": _prototype_parent_actions})

# typeclasses node

def _all_typeclasses(caller):
    """Get name of available typeclasses."""
    return list(
        name
        for name in sorted(utils.get_all_typeclasses("evennia.objects.models.ObjectDB").keys())
        if name != "evennia.objects.models.ObjectDB"
    )

def _typeclass_actions(caller, raw_inp, **kwargs):
    """Parse actions for typeclass listing"""

    typeclass_path, action = _default_parse(
        raw_inp, choices, ("examine", "e", "l"), ("remove", "r", "delete", "d")
    )

    if typeclass_path:
            typeclass = utils.get_all_typeclasses().get(typeclass_path)
                docstr = []
                for line in typeclass.__doc__.split("\n"):
                    if line.strip():
                        docstr.append(line)
                    elif docstr:
                        break
                docstr = "\n".join(docstr) if docstr else "<empty>"
                txt = (
                    "Typeclass |c{typeclass_path}|n; "
                    "First paragraph of docstring:\n\n{docstring}".format(
                        typeclass_path=typeclass_path, docstring=docstr
                    )
                )
            else:
                txt = "This is typeclass |y{}|n.".format(typeclass)
            return "node_examine_entity", {"text": txt, "back": "typeclass"}
            prototype = _get_menu_prototype(caller)
            old_typeclass = prototype.pop("typeclass", None)
            if old_typeclass:
                _set_menu_prototype(caller, prototype)
                caller.msg("Cleared typeclass {}.".format(old_typeclass))
            else:
                caller.msg("No typeclass to remove.")
        return "node_typeclass"

def _typeclass_select(caller, typeclass, **kwargs):
    """Select typeclass from list and add it to prototype. Return next node to go to."""
    ret = _set_property(caller, typeclass, prop="typeclass", processor=str)
    caller.msg("Selected typeclass |c{}|n.".format(typeclass))
    return ret

@list_node(_all_typeclasses, _typeclass_select)
def node_typeclass(caller):
    text = """
        The |cTypeclass|n defines what 'type' of object this is - the actual working code to use.

        All spawned objects must have a typeclass. If not given here, the typeclass must be set in
        one of the prototype's |cparents|n.

        {current}
    """.format(
        current=_get_current_value(caller, "typeclass"),
        actions="|WSelect with |w<num>|W. Other actions: "
        "|we|Wxamine |w<num>|W, |wr|Wemove selection",
    )

        A |nTypeclass|n is specified by the actual python-path to the class definition in the
        Evennia code structure.

        Which |cAttributes|n, |cLocks|n and other properties have special
        effects or expects certain values depend greatly on the code in play.
    """

    options = _wizard_options("typeclass", "prototype_parent", "key", color="|W")
    options.append({"key": "_default", "goto": _typeclass_actions})

# key node

def node_key(caller):
    text = """
        The |cKey|n is the given name of the object to spawn. This will retain the given case.

        {current}
        current=_get_current_value(caller, "key")
    )

        The key should often not be identical for every spawned object. Using a randomising
        $protfunc can be used, for example |c$choice(Alan, Tom, John)|n will give one of the three
        names every time an object of this prototype is spawned.

        |c$protfuncs|n
        {pfuncs}
    )

    options = _wizard_options("key", "typeclass", "aliases")
        {
            "goto": (_set_property, dict(prop="key", processor=lambda s: s.strip())),
        }
    )

# aliases node

def _all_aliases(caller):
    "Get aliases in prototype"
    return prototype.get("aliases", [])

def _aliases_select(caller, alias):
    "Add numbers as aliases"
    aliases = _all_aliases(caller)
    try:
        ind = str(aliases.index(alias) + 1)
        if ind not in aliases:
            aliases.append(ind)
            _set_prototype_value(caller, "aliases", aliases)
            caller.msg("Added alias '{}'.".format(ind))
    except (IndexError, ValueError) as err:
        caller.msg("Error: {}".format(err))

    return "node_aliases"

def _aliases_actions(caller, raw_inp, **kwargs):
    """Parse actions for aliases listing"""
    alias, action = _default_parse(raw_inp, choices, ("remove", "r", "delete", "d"))

    if alias and action == "remove":
        try:
            aliases.remove(alias)
            caller.msg("Removed alias '{}'.".format(alias))
            caller.msg("No matching alias found to remove.")
    else:
        # if not a valid remove, add as a new alias
        alias = raw_inp.lower().strip()
        if alias and alias not in aliases:
            aliases.append(alias)
            caller.msg("Added alias '{}'.".format(alias))
        else:
            caller.msg("Alias '{}' was already set.".format(alias))

@list_node(_all_aliases, _aliases_select)
def node_aliases(caller):
    text = """
        |cAliases|n are alternative ways to address an object, next to its |cKey|n.  Aliases are not
        case sensitive.

        {current}
        current=_get_current_value(
            caller,
            comparer=lambda propval, flatval: [al for al in flatval if al not in propval],
            formatter=lambda lst: "\n" + ", ".join(lst),
            only_inherit=True,
        )
    )
    _set_actioninfo(
        caller, _format_list_actions("remove", prefix="|w<text>|W to add new alias. Other action: ")
    )

        Aliases are fixed alternative identifiers and are stored with the new object.

        {pfuncs}
    )

    options = _wizard_options("aliases", "key", "attrs")
    options.append({"key": "_default", "goto": _aliases_actions})

# attributes node

def _caller_attrs(caller):
    attrs = [
        "{}={}".format(tup[0], utils.crop(utils.to_str(tup[1]), width=10))
        for tup in prototype.get("attrs", [])
    ]
    return attrs

def _get_tup_by_attrname(caller, attrname):
    attrs = prototype.get("attrs", [])
    try:
        inp = [tup[0] for tup in attrs].index(attrname)
        return attrs[inp]

def _display_attribute(attr_tuple):
    """Pretty-print attribute tuple"""
    attrkey, value, category, locks = attr_tuple
    value = protlib.protfunc_parser(value)
    typ = type(value)
    out = "{attrkey} |c=|n {value} |W({typ}{category}{locks})|n".format(
        attrkey=attrkey,
        value=value,
        typ=typ,
        category=", category={}".format(category) if category else "",
        locks=", locks={}".format(";".join(locks)) if any(locks) else "",
    )

    return out

def _add_attr(caller, attr_string, **kwargs):
    """
    Add new attribute, parsing input.

    Args:
        caller (Object): Caller of menu.
        attr_string (str): Input from user
            attr is entered on these forms
                attr = value
                attr;category = value
                attr;category;lockstring = value
        delete (str): If this is set, attr_string is
            considered the name of the attribute to delete and
            no further parsing happens.
    Returns:
        result (str): Result string of action.
    """
    attrname = ""
    value = ""
    category = None
    locks = ""

    if "delete" in kwargs:
        attrname = attr_string.lower().strip()
    elif "=" in attr_string:
        attrname, value = (part.strip() for part in attr_string.split("=", 1))
        attrname = attrname.lower()
        nameparts = attrname.split(";", 2)
        nparts = len(nameparts)
        if nparts == 2:
            attrname, category = nameparts
        elif nparts > 2:
            attrname, category, locks = nameparts
    attr_tuple = (attrname, value, category, str(locks))

    if attrname:
        attrs = prot.get("attrs", [])

        if "delete" in kwargs:
            try:
                ind = [tup[0] for tup in attrs].index(attrname)
                del attrs[ind]
                _set_prototype_value(caller, "attrs", attrs)
                return "Removed Attribute '{}'".format(attrname)
                return "Attribute to delete not found."

        try:
            # replace existing attribute with the same name in the prototype
            ind = [tup[0] for tup in attrs].index(attrname)
            attrs[ind] = attr_tuple
            text = "Edited Attribute '{}'.".format(attrname)
            attrs.append(attr_tuple)
            text = "Added Attribute " + _display_attribute(attr_tuple)

        _set_prototype_value(caller, "attrs", attrs)
    else:
        text = "Attribute must be given as 'attrname[;category;locks] = <value>'."

def _attr_select(caller, attrstr):
    attrname, _ = attrstr.split("=", 1)
    attrname = attrname.strip()

    attr_tup = _get_tup_by_attrname(caller, attrname)
    if attr_tup:
        return ("node_examine_entity", {"text": _display_attribute(attr_tup), "back": "attrs"})
    else:
        caller.msg("Attribute not found.")
        return "node_attrs"

def _attrs_actions(caller, raw_inp, **kwargs):
    """Parse actions for attribute listing"""
    attrstr, action = _default_parse(
        raw_inp, choices, ("examine", "e"), ("remove", "r", "delete", "d")
    )
    if attrstr is None:
        attrstr = raw_inp
    try:
        attrname, _ = attrstr.split("=", 1)
        caller.msg("|rNeed to enter the attribute on the form attrname=value.|n")

    if action and attr_tup:
            return ("node_examine_entity", {"text": _display_attribute(attr_tup), "back": "attrs"})
            res = _add_attr(caller, attrname, delete=True)
            caller.msg(res)
    else:
        res = _add_attr(caller, raw_inp)
        caller.msg(res)
    return "node_attrs"

@list_node(_caller_attrs, _attr_select)
def node_attrs(caller):
    def _currentcmp(propval, flatval):
        "match by key + category"
        cmp1 = [(tup[0].lower(), tup[2].lower() if tup[2] else None) for tup in propval]
        return [
            tup
            for tup in flatval
            if (tup[0].lower(), tup[2].lower() if tup[2] else None) not in cmp1
        ]

    text = """
        |cAttributes|n are custom properties of the object. Enter attributes on one of these forms:

        attrname=value
        attrname;category=value
        attrname;category;lockstring=value

        To give an attribute without a category but with a lockstring, leave that spot empty
        (attrname;;lockstring=value). Attribute values can have embedded $protfuncs.

        {current}
            caller,
            "attrs",
            comparer=_currentcmp,
            formatter=lambda lst: "\n" + "\n".join(_display_attribute(tup) for tup in lst),
        )
    )
    _set_actioninfo(caller, _format_list_actions("examine", "remove", prefix="Actions: "))

        Most commonly, Attributes don't need any categories or locks. If using locks, the lock-types
        'attredit' and 'attrread' are used to limit editing and viewing of the Attribute. Putting
        the lock-type `attrcreate` in the |clocks|n prototype key can be used to restrict builders
        from adding new Attributes.

        |c$protfuncs

        {pfuncs}
    )

    options = _wizard_options("attrs", "aliases", "tags")
    options.append({"key": "_default", "goto": _attrs_actions})

# tags node

def _caller_tags(caller):
    tags = [tup[0] for tup in prototype.get("tags", [])]
    return tags

def _get_tup_by_tagname(caller, tagname):
    tags = prototype.get("tags", [])
    try:
        inp = [tup[0] for tup in tags].index(tagname)
        return tags[inp]

def _display_tag(tag_tuple):
    """Pretty-print tag tuple"""
    tagkey, category, data = tag_tuple
    out = "Tag: '{tagkey}' (category: {category}{dat})".format(
        tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else ""
    )
    return out

def _add_tag(caller, tag_string, **kwargs):
    """
    Add tags to the system, parsing input

    Args:
        tag_string (str): Input from user on one of these forms
            tagname
            tagname;category
            tagname;category;data

        delete (str): If this is set, tag_string is considered
            the name of the tag to delete.

    Returns:

    """
    tag = tag_string.strip().lower()
    data = ""

        tag = tag_string.lower().strip()
    else:
        nameparts = tag.split(";", 2)
        ntuple = len(nameparts)
        if ntuple == 2:
            tag, category = nameparts
        elif ntuple > 2:
            tag, category, data = nameparts[:3]

    tag_tuple = (tag.lower(), category.lower() if category else None, data)

    if tag:
        tags = prot.get("tags", [])

        old_tag = _get_tup_by_tagname(caller, tag)

            if old_tag:
                tags.pop(tags.index(old_tag))
                text = "Removed Tag '{}'.".format(tag)
            else:
                text = "Found no Tag to remove."
        elif not old_tag:
            # a fresh, new tag
            tags.append(tag_tuple)
            text = "Added Tag '{}'".format(tag)
        else:
            # old tag exists; editing a tag means replacing old with new
            ind = tags.index(old_tag)
            tags[ind] = tag_tuple
            text = "Edited Tag '{}'".format(tag)

        _set_prototype_value(caller, "tags", tags)
    else:
        text = "Tag must be given as 'tag[;category;data]'."

def _tag_select(caller, tagname):
    tag_tup = _get_tup_by_tagname(caller, tagname)
    if tag_tup:
        return "node_examine_entity", {"text": _display_tag(tag_tup), "back": "attrs"}
    else:
        caller.msg("Tag not found.")

def _tags_actions(caller, raw_inp, **kwargs):
    """Parse actions for tags listing"""
    tagname, action = _default_parse(
    )

    if tagname is None:
        tagname = raw_inp.lower().strip()

            return ("node_examine_entity", {"text": _display_tag(tag_tup), "back": "tags"})
            res = _add_tag(caller, tagname, delete=True)
    else:
        res = _add_tag(caller, raw_inp)
    return "node_tags"

@list_node(_caller_tags, _tag_select)
def node_tags(caller):
        cmp1 = [(tup[0].lower(), tup[1].lower() if tup[2] else None) for tup in propval]
        return [
            tup
            if (tup[0].lower(), tup[1].lower() if tup[1] else None) not in cmp1
        ]

    text = """
        |cTags|n are used to group objects so they can quickly be found later. Enter tags on one of
        the following forms:
            tagname

        {current}
            caller,
            "tags",
            formatter=lambda lst: "\n" + "\n".join(_display_tag(tup) for tup in lst),
        )
    )

        Tags are shared between all objects with that tag. So the 'data' field (which is not
        commonly used) can only hold eventual info about the Tag itself, not about the individual
        object on which it sits.

        All objects created with this prototype will automatically get assigned a tag named the same
        as the |cprototype_key|n and with a category "{tag_category}". This allows the spawner to
        optionally update previously spawned objects when their prototype changes.
        tag_category=protlib.PROTOTYPE_TAG_CATEGORY
    )

    options = _wizard_options("tags", "attrs", "locks")
    options.append({"key": "_default", "goto": _tags_actions})

# locks node

def _caller_locks(caller):
    locks = _get_menu_prototype(caller).get("locks", "")
    return [lck for lck in locks.split(";") if lck]

def _locks_display(caller, lock):
    return lock

def _lock_select(caller, lockstr):
    return ("node_examine_entity", {"text": _locks_display(caller, lockstr), "back": "locks"})

def _lock_add(caller, lock, **kwargs):
    locks = _caller_locks(caller)

    try:
        locktype, lockdef = lock.split(":", 1)
        return "Lockstring lacks ':'."

    locktype = locktype.strip().lower()

        try:
            ind = locks.index(lock)
            locks.pop(ind)
            _set_prototype_value(caller, "locks", ";".join(locks), parse=False)
            ret = "Lock {} deleted.".format(lock)
            ret = "No lock found to delete."
        return ret
    try:
        locktypes = [lck.split(":", 1)[0].strip().lower() for lck in locks]
        ind = locktypes.index(locktype)
        locks[ind] = lock
        ret = "Lock with locktype '{}' updated.".format(locktype)
        locks.append(lock)
        ret = "Added lock '{}'.".format(lock)
    _set_prototype_value(caller, "locks", ";".join(locks))
    return ret

def _locks_actions(caller, raw_inp, **kwargs):
    lock, action = _default_parse(
    )

    if lock:
            return ("node_examine_entity", {"text": _locks_display(caller, lock), "back": "locks"})
            ret = _lock_add(caller, lock, delete=True)
            caller.msg(ret)
    else:
        ret = _lock_add(caller, raw_inp)
        caller.msg(ret)

    return "node_locks"

@list_node(_caller_locks, _lock_select)
def node_locks(caller):
        "match by locktype"
        cmp1 = [lck.split(":", 1)[0] for lck in propval.split(";")]
        return ";".join(lstr for lstr in flatval.split(";") if lstr.split(":", 1)[0] not in cmp1)

    text = """
        The |cLock string|n defines limitations for accessing various properties of the object once
        it's spawned. The string should be on one of the following forms:

            locktype:[NOT] lockfunc(args)
            locktype: [NOT] lockfunc(args) [AND|OR|NOT] lockfunc(args) [AND|OR|NOT] ...

        {current}{action}
            caller,
            "locks",
            formatter=lambda lockstr: "\n".join(
                _locks_display(caller, lstr) for lstr in lockstr.split(";")
            ),
        ),
        action=_format_list_actions("examine", "remove", prefix="Actions: "),
    )

        Here is an example of two lock strings:

            edit:false()
            call:tag(Foo) OR perm(Builder)

        Above locks limit two things, 'edit' and 'call'. Which lock types are actually checked
        depend on the typeclass of the object being spawned. Here 'edit' is never allowed by anyone
        while 'call' is allowed to all accessors with a |ctag|n 'Foo' OR which has the
        |cPermission|n 'Builder'.

        |cAvailable lockfuncs:|n

        {lfuncs}
        lfuncs=_format_lockfuncs()
    )

    options = _wizard_options("locks", "tags", "permissions")
    options.append({"key": "_default", "goto": _locks_actions})

# permissions node

def _caller_permissions(caller):
    perms = prototype.get("permissions", [])
    return perms

def _display_perm(caller, permission, only_hierarchy=False):
    hierarchy = settings.PERMISSION_HIERARCHY
    perm_low = permission.lower()
    txt = ""
    if perm_low in [prm.lower() for prm in hierarchy]:
        txt = "Permission (in hieararchy): {}".format(
            ", ".join(
                [
                    "|w[{}]|n".format(prm) if prm.lower() == perm_low else "|W{}|n".format(prm)
                    for prm in hierarchy
                ]
            )
        )
    elif not only_hierarchy:
        txt = "Permission: '{}'".format(permission)
    return txt

def _permission_select(caller, permission, **kwargs):
    return (
        "node_examine_entity",
        {"text": _display_perm(caller, permission), "back": "permissions"},
    )

def _add_perm(caller, perm, **kwargs):
    if perm:
        perm_low = perm.lower()
        perms = _caller_permissions(caller)
        perms_low = [prm.lower() for prm in perms]
            try:
                ind = perms_low.index(perm_low)
                del perms[ind]
                text = "Removed Permission '{}'.".format(perm)
                text = "Found no Permission to remove."
        else:
            if perm_low in perms_low:
                text = "Permission already set."
            else:
                perms.append(perm)
                _set_prototype_value(caller, "permissions", perms)
                text = "Added Permission '{}'".format(perm)

def _permissions_actions(caller, raw_inp, **kwargs):
    """Parse actions for permission listing"""
    perm, action = _default_parse(
    )

    if perm:
            return (
                "node_examine_entity",
                {"text": _display_perm(caller, perm), "back": "permissions"},
            )
            res = _add_perm(caller, perm, delete=True)
    else:
        res = _add_perm(caller, raw_inp.strip())
    return "node_permissions"

@list_node(_caller_permissions, _permission_select)
def node_permissions(caller):
    def _currentcmp(pval, fval):
        cmp1 = [perm.lower() for perm in pval]
        return [perm for perm in fval if perm.lower() not in cmp1]

    text = """
        |cPermissions|n are simple strings used to grant access to this object. A permission is used
        when a |clock|n is checked that contains the |wperm|n or |wpperm|n lock functions. Certain
        permissions belong in the |cpermission hierarchy|n together with the |Wperm()|n lock
        function.

        {current}
            caller,
            "permissions",
            formatter=lambda lst: "\n" + "\n".join(prm for prm in lst),
        )
    )

        Any string can act as a permission as long as a lock is set to look for it. Depending on the
        lock, having a permission could even be negative (i.e. the lock is only passed if you
        |wdon't|n have the 'permission'). The most common permissions are the hierarchical
        permissions:

            {permissions}.

        For example, a |clock|n string like "edit:perm(Builder)" will grant access to accessors
        having the |cpermission|n "Builder" or higher.
        permissions=", ".join(settings.PERMISSION_HIERARCHY)
    )

    options = _wizard_options("permissions", "locks", "location")
    options.append({"key": "_default", "goto": _permissions_actions})

# location node

def node_location(caller):
    text = """
        The |cLocation|n of this object in the world. If not given, the object will spawn in the
        inventory of |c{caller}|n by default.

        {current}
        caller=caller.key, current=_get_current_value(caller, "location")
    )

        You get the most control by not specifying the location - you can then teleport the spawned
        objects as needed later. Setting the location may be useful for quickly populating a given
        location. One could also consider randomizing the location using a $protfunc.

        {pfuncs}
    )

    options = _wizard_options("location", "permissions", "home", search=True)
        {
            "goto": (_set_property, dict(prop="location", processor=lambda s: s.strip())),
        }
    )

# home node

def node_home(caller):
    text = """
        The |cHome|n location of an object is often only used as a backup - this is where the object
        will be moved to if its location is deleted. The home location can also be used as an actual
        home for characters to quickly move back to.

        If unset, the global home default (|w{default}|n) will be used.

        {current}
        default=settings.DEFAULT_HOME, current=_get_current_value(caller, "home")
    )
        The home can be given as a #dbref but can also be specified using the protfunc
        '$obj(name)'. Use |wSE|nearch to find objects in the database.

        The home location is commonly not used except as a backup; using the global default is often
        enough.

        {pfuncs}
    )

    options = _wizard_options("home", "location", "destination", search=True)
        {
            "goto": (_set_property, dict(prop="home", processor=lambda s: s.strip())),
        }
    )

# destination node

def node_destination(caller):
    text = """
        The object's |cDestination|n is generally only used by Exit-like objects to designate where
        the exit 'leads to'. It's usually unset for all other types of objects.

        {current}
        current=_get_current_value(caller, "destination")
    )

        The destination can be given as a #dbref but can also be specified using the protfunc
        '$obj(name)'. Use |wSEearch to find objects in the database.

        {pfuncs}
    )

    options = _wizard_options("destination", "home", "prototype_desc", search=True)
        {
            "goto": (_set_property, dict(prop="destination", processor=lambda s: s.strip())),
        }
    )

# prototype_desc node

def node_prototype_desc(caller):
    text = """
        The |cPrototype-Description|n briefly describes the prototype when it's viewed in listings.

        {current}
        current=_get_current_value(caller, "prototype_desc")
    )

        Giving a brief description helps you and others to locate the prototype for use later.
    """

    options = _wizard_options("prototype_desc", "prototype_key", "prototype_tags")
        {
            "goto": (
                _set_property,
                dict(
                    prop="prototype_desc",
                    processor=lambda s: s.strip(),
                    next_node="node_prototype_desc",
                ),
            ),
        }
    )

# prototype_tags node

def _caller_prototype_tags(caller):
    tags = prototype.get("prototype_tags", [])
    tags = [tag[0] if isinstance(tag, tuple) else tag for tag in tags]

def _add_prototype_tag(caller, tag_string, **kwargs):
    """
    Add prototype_tags to the system. We only support straight tags, no
    categories (category is assigned automatically).

    Args:
        tag_string (str): Input from user - only tagname

    Returns:

    """

    if tag:
        tags = _caller_prototype_tags(caller)
        exists = tag in tags

            if exists:
                tags.pop(tags.index(tag))
                text = "Removed Prototype-Tag '{}'.".format(tag)
            else:
                text = "Found no Prototype-Tag to remove."
        elif not exists:
            tags.append(tag)
            text = "Added Prototype-Tag '{}'.".format(tag)
        else:
            text = "Prototype-Tag already added."

        _set_prototype_value(caller, "prototype_tags", tags)
    else:
        text = "No Prototype-Tag specified."

def _prototype_tag_select(caller, tagname):
    caller.msg("Prototype-Tag: {}".format(tagname))
    return "node_prototype_tags"

def _prototype_tags_actions(caller, raw_inp, **kwargs):
    tagname, action = _default_parse(raw_inp, choices, ("remove", "r", "delete", "d"))

    if tagname:
        if action == "remove":
            res = _add_prototype_tag(caller, tagname, delete=True)
    else:
        res = _add_prototype_tag(caller, raw_inp.lower().strip())

@list_node(_caller_prototype_tags, _prototype_tag_select)
def node_prototype_tags(caller):
    text = """
        |cPrototype-Tags|n can be used to classify and find prototypes in listings Tag names are not
        case-sensitive and can have not have a custom category.

        {current}
            caller,
            "prototype_tags",
            formatter=lambda lst: ", ".join(tg for tg in lst),
        )
    )
        caller, _format_list_actions("remove", prefix="|w<text>|n|W to add Tag. Other Action:|n ")
    )
        Using prototype-tags is a good way to organize and group large numbers of prototypes by
        genre, type etc. Under the hood, prototypes' tags will all be stored with the category
        '{tagmetacategory}'.
        tagmetacategory=protlib._PROTOTYPE_TAG_META_CATEGORY
    )

    options = _wizard_options("prototype_tags", "prototype_desc", "prototype_locks")
    options.append({"key": "_default", "goto": _prototype_tags_actions})

# prototype_locks node

def _caller_prototype_locks(caller):
    locks = _get_menu_prototype(caller).get("prototype_locks", "")

def _prototype_lock_select(caller, lockstr):
    return (
        {"text": _locks_display(caller, lockstr), "back": "prototype_locks"},
    )

def _prototype_lock_add(caller, lock, **kwargs):
    locks = _caller_prototype_locks(caller)

    try:

        try:
            _set_prototype_value(caller, "prototype_locks", ";".join(locks), parse=False)
            ret = "Prototype-lock {} deleted.".format(lock)
            ret = "No Prototype-lock found to delete."
        return ret
    try:
        ret = "Prototype-lock with locktype '{}' updated.".format(locktype)
        ret = "Added Prototype-lock '{}'.".format(lock)
    _set_prototype_value(caller, "prototype_locks", ";".join(locks))
    return ret

def _prototype_locks_actions(caller, raw_inp, **kwargs):
    )

    if lock:
            ret = _prototype_lock_add(caller, lock.strip(), delete=True)
    else:
        ret = _prototype_lock_add(caller, raw_inp.strip())

    return "node_prototype_locks"

@list_node(_caller_prototype_locks, _prototype_lock_select)
def node_prototype_locks(caller):
    text = """
        |cPrototype-Locks|n are used to limit access to this prototype when someone else is trying
        to access it. By default any prototype can be edited only by the creator and by Admins while
        they can be used by anyone with access to the spawn command. There are two valid lock types
        the prototype access tools look for:

            - 'edit': Who can edit the prototype.
            - 'spawn': Who can spawn new objects with this prototype.

        If unsure, keep the open defaults.

        {current}
            caller,
            "prototype_locks",
            formatter=lambda lstring: "\n".join(
                _locks_display(caller, lstr) for lstr in lstring.split(";")
            ),
        )
    )

        Prototype locks can be used to vary access for different tiers of builders. It also allows
        developers to produce 'base prototypes' only meant for builders to inherit and expand on
        rather than tweak in-place.
        """

    options = _wizard_options("prototype_locks", "prototype_tags", "index")
    options.append({"key": "_default", "goto": _prototype_locks_actions})

# update existing objects node

def _apply_diff(caller, **kwargs):
    """update existing objects"""
    prototype = kwargs["prototype"]
    objects = kwargs["objects"]
    back_node = kwargs["back_node"]
    diff = kwargs.get("diff", None)
    num_changed = spawner.batch_update_objects_with_prototype(
        prototype, diff=diff, objects=objects, caller=caller
    )
    caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed))
    return back_node

def _keep_diff(caller, **kwargs):
    """Change to KEEP setting for a given section of a diff"""
    # from evennia import set_trace;set_trace(term_size=(182, 50))
    path = kwargs["path"]
    diff = kwargs["diff"]
    tmp = diff
    for key in path[:-1]:
        tmp = tmp[key]
    tmp[path[-1]] = tuple(list(tmp[path[-1]][:-1]) + ["KEEP"])

def _format_diff_text_and_options(diff, minimal=True, **kwargs):
    """
    Reformat the diff in a way suitable for the olc menu.

    Args:
        diff (dict): A diff as produced by `prototype_diff`.
        minimal (bool, optional): Don't show KEEPs.

        any (any): Forwarded into the generated options as arguments to the callable.

    Returns:
        texts (list): List of texts.
        options (list): List of options dict.

    """
    valid_instructions = ("KEEP", "REMOVE", "ADD", "UPDATE")

    def _visualize(obj, rootname, get_name=False):
        if utils.is_iter(obj):
            if not obj:
                return str(obj)
            if get_name:
                return obj[0] if obj[0] else "<unset>"
            if rootname == "attrs":
                return "{} |W=|n {} |W(category:|n {}|W, locks:|n {}|W)|n".format(*obj)
            elif rootname == "tags":
                return "{} |W(category:|n {}|W)|n".format(obj[0], obj[1])

        return "{}".format(obj)

    def _parse_diffpart(diffpart, optnum, *args):
        typ = type(diffpart)
        texts = []
        options = []
        if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions:
            rootname = args[0]
            old, new, instruction = diffpart
            if instruction == "KEEP":
                if not minimal:
                    texts.append("   |gKEEP|W:|n {old}".format(old=_visualize(old, rootname)))
            else:
                # instructions we should be able to revert by a menu choice
                vold = _visualize(old, rootname)
                vnew = _visualize(new, rootname)
                vsep = "" if len(vold) < 78 else "\n"

                if instruction == "ADD":
                    texts.append(
                        "   |c[{optnum}] |yADD|n: {new}".format(
                            optnum=optnum, new=_visualize(new, rootname)
                        )
                    )
                elif instruction == "REMOVE" and not new:
                    if rootname == "tags" and old[1] == protlib.PROTOTYPE_TAG_CATEGORY:
                        # special exception for the prototype-tag mechanism
                        # this is added post-spawn automatically and should
                        # not be listed as REMOVE.
                        return texts, options, optnum

                        "   |c[{optnum}] |rREMOVE|n: {old}".format(
                            optnum=optnum, old=_visualize(old, rootname)
                        )
                    )
                else:
                    vinst = "|y{}|n".format(instruction)
                        "   |c[{num}] {inst}|W:|n {old} |W->|n{sep} {new}".format(
                            inst=vinst, num=optnum, old=vold, sep=vsep, new=vnew
                        )
                    )
                options.append(
                    {
                        "key": str(optnum),
                        "desc": "|gKEEP|n ({}) {}".format(
                            rootname, _visualize(old, args[-1], get_name=True)
                        ),
                        "goto": (_keep_diff, dict((("path", args), ("diff", diff)), **kwargs)),
                    }
                )
                optnum += 1
        else:
            for key in sorted(list(diffpart.keys())):
                subdiffpart = diffpart[key]
                text, option, optnum = _parse_diffpart(subdiffpart, optnum, *(args + (key,)))
                texts.extend(text)
                options.extend(option)
        return texts, options, optnum

    texts = []
    # we use this to allow for skipping full KEEP instructions
    optnum = 1

    for root_key in sorted(diff):
        diffpart = diff[root_key]
        text, option, optnum = _parse_diffpart(diffpart, optnum, root_key)
        heading = "- |w{}:|n ".format(root_key)
        if text:
            text = [heading + text[0]] + text[1:]
        else:
            text = [heading]

        texts.extend(text)
        options.extend(option)

    return texts, options

def node_apply_diff(caller, **kwargs):
    """Offer options for updating objects"""

    def _keep_option(keyname, prototype, base_obj, obj_prototype, diff, objects, back_node):
        """helper returning an option dict"""
        options = {
            "desc": "Keep {} as-is".format(keyname),
            "goto": (
                _keep_diff,
                {
                    "key": keyname,
                    "prototype": prototype,
                    "base_obj": base_obj,
                    "obj_prototype": obj_prototype,
                    "diff": diff,
                    "objects": objects,
                    "back_node": back_node,
                },
            ),
        }
        return options

    prototype = kwargs.get("prototype", None)
    update_objects = kwargs.get("objects", None)
    back_node = kwargs.get("back_node", "node_index")
    obj_prototype = kwargs.get("obj_prototype", None)
    base_obj = kwargs.get("base_obj", None)
    custom_location = kwargs.get("custom_location", None)

    if not update_objects:
        text = "There are no existing objects to update."
        options = {"key": "_default", "goto": back_node}

    if not diff:
        # use one random object as a reference to calculate a diff
        base_obj = choice(update_objects)

        diff, obj_prototype = spawner.prototype_diff_from_object(prototype, base_obj)

        This will go through all existing objects and apply the changes you accept.

        Be careful with this operation! The upgrade mechanism will try to automatically estimate
        what changes need to be applied. But the estimate is |wonly based on the analysis of one
        randomly selected object|n among all objects spawned by this prototype. If that object
        happens to be unusual in some way the estimate will be off and may lead to unexpected
        results for other objects. Always test your objects carefully after an upgrade and consider
        being conservative (switch to KEEP) for things you are unsure of. For complex upgrades it
        may be better to get help from an administrator with access to the `@py` command for doing
        this manually.

        Note that the `location` will never be auto-adjusted because it's so rare to want to
        homogenize the location of all object instances."""

    if not custom_location:
        diff.pop("location", None)

    txt, options = _format_diff_text_and_options(
        diff, objects=update_objects, base_obj=base_obj, prototype=prototype
    )

    if options:
        text = [
            "Suggested changes to {} objects. ".format(len(update_objects)),
            "Showing random example obj to change: {name} ({dbref}))\n".format(
                name=base_obj.key, dbref=base_obj.dbref
            ),
        ] + txt
        options.extend(
            [
                {
                    "key": ("|wu|Wpdate {} objects".format(len(update_objects)), "update", "u"),
                    "desc": "Update {} objects".format(len(update_objects)),
                    "goto": (
                        _apply_diff,
                        {
                            "prototype": prototype,
                            "objects": update_objects,
                            "back_node": back_node,
                            "diff": diff,
                            "base_obj": base_obj,
                        },
                    ),
                },
                {
                    "key": ("|wr|Weset changes", "reset", "r"),
                    "goto": (
                        "node_apply_diff",
                        {"prototype": prototype, "back_node": back_node, "objects": update_objects},
                    ),
                },
            ]
        )
    else:
        text = [
            "Analyzed a random sample object (out of {}) - "
            "found no changes to apply.".format(len(update_objects))
        ]

    options.extend(_wizard_options("update_objects", back_node[5:], None))
    options.append({"key": "_default", "goto": back_node})

    text = "\n".join(text)

# prototype save node

def node_prototype_save(caller, **kwargs):
    """Save prototype to disk"""
    # these are only set if we selected 'yes' to save on a previous pass
    # set to True/False if answered, None if first pass
    accept_save = kwargs.get("accept_save", None)

    if accept_save and prototype:
        # we already validated and accepted the save, so this node acts as a goto callback and
        # should now only return the next node
        prototype_key = prototype.get("prototype_key")
        try:
            protlib.save_prototype(prototype)
            text = "|rCould not save:|n {}\n(press Return to continue)".format(exc)
            options = {"key": "_default", "goto": "node_index"}

        spawned_objects = protlib.search_objects_with_prototype(prototype_key)
        nspawned = spawned_objects.count()

        text = ["|gPrototype saved.|n"]

        if nspawned:
                "\nDo you want to update {} object(s) "
                "already using this prototype?".format(nspawned)
            )
            options = (
                {
                    "key": ("|wY|Wes|n", "yes", "y"),
                    "desc": "Go to updating screen",
                    "goto": (
                        {
                            "accept_update": True,
                            "objects": spawned_objects,
                            "back_node": "node_prototype_save",
                        },
                    ),
                },
                {"key": ("[|wN|Wo|n]", "n"), "desc": "Return to index", "goto": "node_index"},
                {"key": "_default", "goto": "node_index"},
            )
        else:
            text.append("(press Return to continue)")

        text = "\n".join(text)

        helptext = """
        Updating objects means that the spawner will find all objects previously created by this
        prototype. You will be presented with a list of the changes the system will try to apply to
        each of these objects and you can choose to customize that change if needed. If you have
        done a lot of manual changes to your objects after spawning, you might want to update those
        objects manually instead.
        """

        text = (text, helptext)

    # not validated yet
    error, text = _validate_prototype(prototype)

    text = [text]

    if error:
        # abort save
        text.append(
            "\n|yValidation errors were found. They need to be corrected before this prototype "
            "can be saved (or used to spawn).|n"
        )
        options = _wizard_options("prototype_save", "index", None)
        options.append({"key": "_default", "goto": "node_index"})
        return "\n".join(text), options

    prototype_key = prototype["prototype_key"]
    if protlib.search_prototype(prototype_key):
            "\nDo you want to save/overwrite the existing prototype '{name}'?".format(
                name=prototype_key
            )
        )
    else:
        text.append("\nDo you want to save the prototype as '{name}'?".format(name=prototype_key))

        Saving the prototype makes it available for use later. It can also be used to inherit from,
        by name.  Depending on |cprototype-locks|n it also makes the prototype usable and/or
        editable by others. Consider setting good |cPrototype-tags|n and to give a useful, brief
        |cPrototype-desc|n to make the prototype easy to find later.

    """

    options = (
        {
            "key": ("[|wY|Wes|n]", "yes", "y"),
            "desc": "Save prototype",
            "goto": ("node_prototype_save", {"accept_save": True, "prototype": prototype}),
        },
        {"key": ("|wN|Wo|n", "n"), "desc": "Abort and return to Index", "goto": "node_index"},
        {
        },
    )

# spawning node

def _spawn(caller, **kwargs):
    """Spawn prototype"""
    prototype = kwargs["prototype"].copy()
    new_location = kwargs.get("location", None)
    if new_location:
        prototype["location"] = new_location
    if not prototype.get("location"):
        prototype["location"] = caller

    obj = spawner.spawn(prototype, caller=caller)
    if obj:
        obj = obj[0]
        text = "|gNew instance|n {key} ({dbref}) |gspawned at location |n{loc}|n|g.|n".format(
            key=obj.key, dbref=obj.dbref, loc=prototype["location"]
        )
    else:
        text = "|rError: Spawner did not return a new instance.|n"
    return "node_examine_entity", {"text": text, "back": "prototype_spawn"}

def node_prototype_spawn(caller, **kwargs):
    """Submenu for spawning the prototype"""

    already_validated = kwargs.get("already_validated", False)

    if already_validated:
        error, text = None, []
    else:
        error, text = _validate_prototype(prototype)
        text = [text]

    if error:
        text.append("\n|rPrototype validation failed. Correct the errors before spawning.|n")
        options = _wizard_options("prototype_spawn", "index", None)

        Spawning is the act of instantiating a prototype into an actual object. As a new object is
        spawned, every $protfunc in the prototype is called anew. Since this is a common thing to
        do, you may also temporarily change the |clocation|n of this prototype to bypass whatever
        value is set in the prototype.

    """

    # show spawn submenu options
    location = prototype.get("location", None)

    if location:
            {
                "desc": "Spawn in prototype's defined location ({loc})".format(loc=location),
                "goto": (
                    _spawn,
                    dict(prototype=prototype, location=location, custom_location=True),
                ),
            }
        )
    caller_loc = caller.location
    if location != caller_loc:
            {
                "desc": "Spawn in {caller}'s location ({loc})".format(
                    caller=caller, loc=caller_loc
                ),
                "goto": (_spawn, dict(prototype=prototype, location=caller_loc)),
            }
        )
    if location != caller_loc != caller:
            {
                "desc": "Spawn in {caller}'s inventory".format(caller=caller),
                "goto": (_spawn, dict(prototype=prototype, location=caller)),
            }
        )

    spawned_objects = protlib.search_objects_with_prototype(prototype_key)
    nspawned = spawned_objects.count()
    if spawned_objects:
            {
                "desc": "Update {num} existing objects with this prototype".format(num=nspawned),
                "goto": (
                    "node_apply_diff",
                    {
                        "objects": list(spawned_objects),
                        "prototype": prototype,
                        "back_node": "node_prototype_spawn",
                    },
                ),
            }
        )
    options.extend(_wizard_options("prototype_spawn", "index", None))
    options.append({"key": "_default", "goto": "node_index"})

# prototype load node

def _prototype_load_select(caller, prototype_key, **kwargs):
    matches = protlib.search_prototype(key=prototype_key)
        prototype = matches[0]
        _set_menu_prototype(caller, prototype)
        return (
            "node_examine_entity",
            {
                "text": "|gLoaded prototype {}.|n".format(prototype["prototype_key"]),
                "back": "index",
            },
        )
    else:
        caller.msg("|rFailed to load prototype '{}'.".format(prototype_key))

def _prototype_load_actions(caller, raw_inp, **kwargs):
    prototype, action = _default_parse(
        raw_inp, choices, ("examine", "e", "l"), ("delete", "del", "d")
    )

    if prototype:
            prototype = protlib.search_prototype(key=prototype)[0]
            txt = protlib.prototype_to_str(prototype)
            return "node_examine_entity", {"text": txt, "back": "prototype_load"}
        elif action == "delete":
            # delete prototype from disk
            try:
                protlib.delete_prototype(prototype, caller=caller)
                txt = "|rDeletion error:|n {}".format(err)
            else:
                txt = "|gPrototype {} was deleted.|n".format(prototype)

    return "node_prototype_load"

@list_node(_all_prototype_parents, _prototype_load_select)
def node_prototype_load(caller, **kwargs):
    """Load prototype"""

    text = """
        Select a prototype to load. This will replace any prototype currently being edited!
    """
    _set_actioninfo(caller, _format_list_actions("examine", "delete"))

        Loading a prototype will load it and return you to the main index. It can be a good idea
        to examine the prototype before loading it.
    """

    options = _wizard_options("prototype_load", "index", None)
    options.append({"key": "_default", "goto": _prototype_load_actions})

# EvMenu definition, formatting and access functions

class OLCMenu(EvMenu):
    """
    A custom EvMenu with a different formatting for the options.

    """

    def nodetext_formatter(self, nodetext):
        """
        Format the node text itself.

        """
        return super().nodetext_formatter(nodetext)

        """
        Split the options into two blocks - olc options and normal options

        """
        olc_keys = (
            "index",
            "forward",
            "back",
            "previous",
            "next",
            "validate prototype",
            "save prototype",
            "load prototype",
            "spawn prototype",
            "search objects",
        )
        actioninfo = self.actioninfo + "\n" if hasattr(self, "actioninfo") else ""
        self.actioninfo = ""  # important, or this could bleed over to other nodes
        olc_options = []
        other_options = []
            raw_key = strip_ansi(key).lower()
            if raw_key in olc_keys:
                desc = " {}".format(desc) if desc else ""
                olc_options.append("|lc{}|lt{}|le{}".format(raw_key, key, desc))
            else:
                other_options.append((key, desc))

        olc_options = (
            actioninfo + " |W|||n ".join(olc_options) + " |W|||n " + "|wQ|Wuit"
            if olc_options
            else ""
        )
        other_options = super().options_formatter(other_options)
        sep = "\n\n" if olc_options and other_options else ""

        return "{}{}{}".format(olc_options, sep, other_options)

    def helptext_formatter(self, helptext):
        """
        Show help text
        """
        return "|c --- Help ---|n\n" + utils.dedent(helptext)

    def display_helptext(self):
        evmore.msg(self.caller, self.helptext, session=self._session, exit_cmd="look")

def start_olc(caller, session=None, prototype=None):
    """
    Start menu-driven olc system for prototypes.

    Args:
        caller (Object or Account): The entity starting the menu.
        session (Session, optional): The individual session to get data.
        prototype (dict, optional): Given when editing an existing
            prototype rather than creating a new one.

    """
    menudata = {
        "node_index": node_index,
        "node_validate_prototype": node_validate_prototype,
        "node_examine_entity": node_examine_entity,
        "node_search_object": node_search_object,
        "node_prototype_key": node_prototype_key,
        "node_prototype_parent": node_prototype_parent,
        "node_typeclass": node_typeclass,
        "node_key": node_key,
        "node_aliases": node_aliases,
        "node_attrs": node_attrs,
        "node_tags": node_tags,
        "node_locks": node_locks,
        "node_permissions": node_permissions,
        "node_location": node_location,
        "node_home": node_home,
        "node_destination": node_destination,
        "node_apply_diff": node_apply_diff,
        "node_prototype_desc": node_prototype_desc,
        "node_prototype_tags": node_prototype_tags,
        "node_prototype_locks": node_prototype_locks,
        "node_prototype_load": node_prototype_load,
        "node_prototype_save": node_prototype_save,
        "node_prototype_spawn": node_prototype_spawn,
    }
    OLCMenu(
        caller,
        menudata,
        startnode="node_index",
        olc_prototype=prototype,
        debug=True,
    )

"""
Protfuncs are FuncParser-callables that can be embedded in a prototype to provide custom logic
without having access to Python. The protfunc is parsed at the time of spawning, using the creating
object's session as input. If the protfunc returns a non-string, this is what will be added to the
prototype.

In the prototype dict, the protfunc is specified as a string inside the prototype, e.g.:

```python
{ ...

"key": "$funcname(args, kwargs)"

...  }
```

Available protfuncs are either all callables in one of the modules of `settings.PROT_FUNC_MODULES`
or all callables added to a dict FUNCPARSER_CALLABLES in such a module. By default, base inlinefuncs
for text manipulation and searching are included, as well as the special `$protkey` function.
See the Prototypes and Spawner documentation for more info.

```python
def funcname (*args, **kwargs):
    return "replacement text"
```

At spawn-time the spawner passes the following extra kwargs into each callable (in addition to
what is added in the call itself):

- `session` (Session): The Session of the entity spawning using this prototype.
- `prototype` (dict): The dict this protfunc is a part of.
- `current_key` (str): The active key this value belongs to in the prototype.

Any traceback raised by this function will be handled at the time of spawning and abort the spawn
before any object is created/updated. It must otherwise return the value to store for the specified
prototype key (this value must be possible to serialize in an Attribute).

"""

from evennia.utils import funcparser

def protfunc_callable_protkey(*args, **kwargs):
    """
    Usage: $protkey(keyname)

    Returns the value of another key in this prototoype. Will raise an error if
    the key is not found in this prototype.

    """
        return ""

    prototype = kwargs.get("prototype", {})
    fieldname = args[0]
    prot_value = None
    if fieldname in prototype:
        prot_value = prototype[fieldname]
    else:
        # check if it's an attribute
        for attrtuple in prototype.get("attrs", []):
            if attrtuple[0] == fieldname:
                prot_value = attrtuple[1]
                break
        else:
                f"{fieldname} not found in prototype\n{prototype}\n"
                "(neither as prototype-field or as an Attribute"
            )
    if callable(prot_value):
            f"Error in prototype\n{prototype}\n$protkey can only reference static "
            f"values/attributes (found {prot_value})"
        )
    try:
        return funcparser.funcparser_callable_eval(prot_value, **kwargs)
    except funcparser.ParsingError:
        return prot_value

# this is picked up by FuncParser
FUNCPARSER_CALLABLES = {
    "protkey": protfunc_callable_protkey,
    **funcparser.FUNCPARSER_CALLABLES,
    **funcparser.SEARCHING_CALLABLES,
}

"""

Handling storage of prototypes, both database-based ones (DBPrototypes) and those defined in modules
(Read-only prototypes). Also contains utility functions, formatters and manager functions.

"""

import hashlib

from evennia.locks.lockhandler import check_lockstring, validate_lockstring
from evennia.typeclasses.attributes import Attribute
from evennia.utils import dbserialize, logger
from evennia.utils.funcparser import FuncParser
    is_iter,
    justify,
)

_MODULE_PROTOTYPE_MODULES = {}
_MODULE_PROTOTYPES = {}
_PROTOTYPE_META_NAMES = (
    "prototype_key",
    "prototype_desc",
    "prototype_tags",
    "prototype_locks",
    "prototype_parent",
)
_PROTOTYPE_RESERVED_KEYS = _PROTOTYPE_META_NAMES + (
    "key",
    "aliases",
    "typeclass",
    "location",
    "home",
    "destination",
    "permissions",
    "locks",
    "tags",
    "attrs",
)
_ERRSTR = _("Error")
_WARNSTR = _("Warning")
PROTOTYPE_TAG_CATEGORY = "from_prototype"
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype"

_PROTOTYPE_FALLBACK_LOCK = "spawn:all();edit:all()"

# the protfunc parser
FUNC_PARSER = FuncParser(settings.PROT_FUNC_MODULES)

class PermissionError(RuntimeError):
    pass

class ValidationError(RuntimeError):
    """
    Raised on prototype validation errors
    """

    pass

def homogenize_prototype(prototype, custom_keys=None):
    """
    Homogenize the more free-form prototype supported pre Evennia 0.7 into the stricter form.

    Args:
        prototype (dict): Prototype.
        custom_keys (list, optional): Custom keys which should not be interpreted as attrs, beyond
            the default reserved keys.

    Returns:
        homogenized (dict): Prototype where all non-identified keys grouped as attributes and other
            homogenizations like adding missing prototype_keys and setting a default typeclass.

    """
    if not prototype or isinstance(prototype, str):

    reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ())

    # correct cases of setting None for certain values
    for protkey in prototype:
        if prototype[protkey] is None:
            if protkey in ("attrs", "tags", "prototype_tags"):
                prototype[protkey] = []
            elif protkey in ("prototype_key", "prototype_desc"):
                prototype[protkey] = ""

    homogenized = {}
    homogenized_tags = []
    homogenized_attrs = []
    homogenized_parents = []

    for key, val in prototype.items():
        if key in reserved:
            # check all reserved keys
            if key == "tags":
                # tags must be on form [(tag, category, data), ...]
                tags = make_iter(prototype.get("tags", []))
                for tag in tags:
                    if not is_iter(tag):
                        homogenized_tags.append((tag, None, None))
                    elif tag:
                        ntag = len(tag)
                        if ntag == 1:
                            homogenized_tags.append((tag[0], None, None))
                        elif ntag == 2:
                            homogenized_tags.append((tag[0], tag[1], None))
                        else:
                            homogenized_tags.append(tag[:3])

            elif key == "attrs":
                attrs = list(prototype.get("attrs", []))  # break reference
                    # attrs must be on form [(key, value, category, lockstr)]
                    if not is_iter(attr):
                        logger.log_err(
                            f"Prototype's 'attr' field must be a list of tuples: {prototype}"
                        )
                    elif attr:
                        nattr = len(attr)
                        if nattr == 1:
                            # we assume a None-value
                            homogenized_attrs.append((attr[0], None, None, ""))
                        elif nattr == 2:
                            homogenized_attrs.append((attr[0], attr[1], None, ""))
                        elif nattr == 3:
                            homogenized_attrs.append((attr[0], attr[1], attr[2], ""))
                        else:
                            homogenized_attrs.append(attr[:4])

            elif key == "prototype_parent":
                # homogenize any prototype-parents embedded directly as dicts
                protparents = prototype.get("prototype_parent", [])
                if isinstance(protparents, dict):
                    protparents = [protparents]
                for parent in make_iter(protparents):
                    if isinstance(parent, dict):
                        # recursively homogenize directly embedded prototype parents
                        homogenized_parents.append(
                            homogenize_prototype(parent, custom_keys=custom_keys)
                        )
                    else:
                        # normal prototype-parent names are added as-is
                        homogenized_parents.append(parent)

            else:
                # another reserved key
                homogenized[key] = val
        else:
            # unreserved keys -> attrs
            homogenized_attrs.append((key, val, None, ""))
    if homogenized_attrs:
        homogenized["attrs"] = homogenized_attrs
    if homogenized_tags:
        homogenized["tags"] = homogenized_tags
    if homogenized_parents:
        homogenized["prototype_parent"] = homogenized_parents

    # add required missing parts that had defaults before

    homogenized["prototype_key"] = homogenized.get(
        "prototype_key",
        # assign a random hash as key
        "prototype-{}".format(hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7]),
    )
    homogenized["prototype_tags"] = homogenized.get("prototype_tags", [])
    homogenized["prototype_locks"] = homogenized.get("prototype_lock", _PROTOTYPE_FALLBACK_LOCK)
    homogenized["prototype_desc"] = homogenized.get("prototype_desc", "")
    if "typeclass" not in prototype and "prototype_parent" not in prototype:
        homogenized["typeclass"] = settings.BASE_OBJECT_TYPECLASS

    return homogenized

# module/dict-based prototypes

def load_module_prototypes(*mod_or_prototypes, override=True):
    """
    Load module prototypes. Also prototype-dicts passed directly to this function are considered
    'module' prototypes (they are impossible to change) but will have a module of None.

    Args:
        *mod_or_prototypes (module or dict): Each arg should be a separate module or
            prototype-dict to load. If none are given, `settings.PROTOTYPE_MODULES` will be used.
        override (bool, optional): If prototypes should override existing ones already loaded.
            Disabling this can allow for injecting prototypes into the system dynamically while
            still allowing same prototype-keys to be overridden from settings (even though settings
            is usually loaded before dynamic loading).

    Note:
        This is called (without arguments) by `evennia.__init__` as Evennia initializes. It's
        important to do this late so as to not interfere with evennia initialization. But it can
        also be used later to add more prototypes to the library on the fly. This is requried
        before a module-based prototype can be accessed by prototype-key.

    """
    global _MODULE_PROTOTYPE_MODULES, _MODULE_PROTOTYPES

    def _prototypes_from_module(mod):
        """
        Load prototypes from a module, first by looking for a global list PROTOTYPE_LIST (a list of
        dict-prototypes), and if not found, assuming all global-level dicts in the module are
        prototypes.

        Args:
            mod (module): The module to load from.evennia

        Returns:
            list: A list of tuples `(prototype_key, prototype-dict)` where the prototype
                has been homogenized.

        """
        prots = []
        prototype_list = variable_from_module(mod, "PROTOTYPE_LIST")
        if prototype_list:
            # found mod.PROTOTYPE_LIST - this should be a list of valid
            # prototype dicts that must have 'prototype_key' set.
            for prot in prototype_list:
                if not isinstance(prot, dict):
                    logger.log_err(
                        f"Prototype read from {mod}.PROTOTYPE_LIST is not a dict (skipping): {prot}"
                    )
                    continue
                elif "prototype_key" not in prot:
                        f"Prototype read from {mod}.PROTOTYPE_LIST "
                        f"is missing the 'prototype_key' (skipping): {prot}"
                    )
                    continue
                prots.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.
            for variable_name, prot in all_from_module(mod).items():
                if isinstance(prot, dict):
                    if "prototype_key" not in prot:
                        prot["prototype_key"] = variable_name.lower()
                    prots.append((prot["prototype_key"], homogenize_prototype(prot)))
        return prots

    def _cleanup_prototype(prototype_key, prototype, mod=None):
        """
        We need to handle externally determined prototype-keys and to make sure
        the prototype contains all needed meta information.

        Args:
            prototype_key (str): The determined name of the prototype.
            prototype (dict): The prototype itself.
            mod (module, optional): The module the prototype was loaded from, if any.

        Returns:
            dict: The cleaned up prototype.

        """
        actual_prot_key = prototype.get("prototype_key", prototype_key).lower()
        prototype.update(
            {
                "prototype_key": actual_prot_key,
                "prototype_desc": (
                    prototype["prototype_desc"] if "prototype_desc" in prototype else (mod or "N/A")
                ),
                "prototype_locks": (
                    prototype["prototype_locks"]
                    if "prototype_locks" in prototype
                    else "use:all();edit:false()"
                ),
                "prototype_tags": list(
                    set(list(make_iter(prototype.get("prototype_tags", []))) + ["module"])
                ),
            }
        )

    if not mod_or_prototypes:
        # in principle this means PROTOTYPE_MODULES could also contain prototypes, but that is
        # rarely useful ...
        mod_or_prototypes = settings.PROTOTYPE_MODULES

    for mod_or_dict in mod_or_prototypes:
        if isinstance(mod_or_dict, dict):
            # a single prototype; we must make sure it has its key
            prototype_key = mod_or_dict.get("prototype_key")
            if not prototype_key:
                raise ValidationError(
                    f"The prototype {mod_or_dict} does not contain a 'prototype_key'"
                )
            prots = [(prototype_key, mod_or_dict)]
            mod = None
        else:
            # a module (or path to module). This can contain many prototypes; they can be keyed by
            # variable-name too
            prots = _prototypes_from_module(mod_or_dict)
            mod = repr(mod_or_dict)

        # store all found prototypes
        for prototype_key, prot in prots:
            prototype = _cleanup_prototype(prototype_key, prot, mod=mod)
            # the key can change since in-proto key is given prio over variable-name-based keys
            actual_prototype_key = prototype["prototype_key"]

            if actual_prototype_key in _MODULE_PROTOTYPES and not override:
                # don't override - useful to still let settings replace dynamic inserts
                continue

            # make sure the prototype contains all meta info
            _MODULE_PROTOTYPES[actual_prototype_key] = prototype
            # track module path for display purposes
            _MODULE_PROTOTYPE_MODULES[actual_prototype_key.lower()] = mod

# Db-based prototypes

class DBPrototypeCache:
    """
    Cache DB-stored prototypes; it can still be slow to initially load 1000s of
    prototypes, due to having to deserialize all prototype-dicts, but after the
    first time the cache will be populated and things will be fast.

    """

    def get(self, db_prot_id):
        return self._cache.get(db_prot_id, None)

    def add(self, db_prot_id, prototype):
        self._cache[db_prot_id] = prototype

    def remove(self, db_prot_id):
        self._cache.pop(db_prot_id, None)

    def replace(self, all_data):
        self._cache = all_data

DB_PROTOTYPE_CACHE = DBPrototypeCache()

class DbPrototype(DefaultScript):
    """
    This stores a single prototype, in an Attribute `prototype`.

    """

        self.key = "empty prototype"  # prototype_key
        self.desc = "A prototype"  # prototype_desc (.tags are used for prototype_tags)
        self.db.prototype = {}  # actual prototype

    @property
    def prototype(self):
        "Make sure to decouple from db!"
        return dbserialize.deserialize(self.attributes.get("prototype", {}))

    @prototype.setter
    def prototype(self, prototype):
        self.attributes.add("prototype", prototype)

# Prototype manager functions

def save_prototype(prototype):
    """
    Create/Store a prototype persistently.

    Args:
        prototype (dict): The prototype to save. A `prototype_key` key is
            required.

    Returns:
        prototype (dict or None): The prototype stored using the given kwargs, None if deleting.

    Raises:
        prototypes.ValidationError: If prototype does not validate.

    Note:
        No edit/spawn locks will be checked here - if this function is called the caller
        is expected to have valid permissions.

    """
    in_prototype = prototype
    in_prototype = homogenize_prototype(in_prototype)

    def _to_batchtuple(inp, *args):
        "build tuple suitable for batch-creation"
        if is_iter(inp):
            # already a tuple/list, use as-is
            return inp
        return (inp,) + args

    prototype_key = in_prototype.get("prototype_key")
    if not prototype_key:
        raise ValidationError(_("Prototype requires a prototype_key"))

    prototype_key = str(prototype_key).lower()

    # we can't edit a prototype defined in a module
    if prototype_key in _MODULE_PROTOTYPES:
        mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key)
        if mod:
            err = _("{protkey} is a read-only prototype (defined as code in {module}).")
        else:
            err = _("{protkey} is a read-only prototype (passed directly as a dict).")
        raise PermissionError(err.format(protkey=prototype_key, module=mod))

    # make sure meta properties are included with defaults
    in_prototype["prototype_desc"] = in_prototype.get(
        "prototype_desc", prototype.get("prototype_desc", "")
    )
    prototype_locks = in_prototype.get(
        "prototype_locks", prototype.get("prototype_locks", _PROTOTYPE_FALLBACK_LOCK)
    )
    is_valid, err = validate_lockstring(prototype_locks)
    if not is_valid:
        raise ValidationError("Lock error: {}".format(err))
    in_prototype["prototype_locks"] = prototype_locks

    prototype_tags = [
        _to_batchtuple(tag, _PROTOTYPE_TAG_META_CATEGORY)
        for tag in make_iter(
            in_prototype.get("prototype_tags", prototype.get("prototype_tags", []))
        )
    ]
    in_prototype["prototype_tags"] = prototype_tags

    stored_prototype = DbPrototype.objects.filter(db_key=prototype_key)
    if stored_prototype:
        # edit existing prototype
        stored_prototype = stored_prototype[0]
        stored_prototype.desc = in_prototype["prototype_desc"]
        if prototype_tags:
            stored_prototype.tags.clear(category=PROTOTYPE_TAG_CATEGORY)
            stored_prototype.tags.batch_add(*in_prototype["prototype_tags"])
        stored_prototype.locks.add(in_prototype["prototype_locks"])
        stored_prototype.attributes.add("prototype", in_prototype)
    else:
        # create a new prototype
        stored_prototype = create_script(
            DbPrototype,
            key=prototype_key,
            desc=in_prototype["prototype_desc"],
            locks=prototype_locks,
            tags=in_prototype["prototype_tags"],
            attributes=[("prototype", in_prototype)],
        )
    DB_PROTOTYPE_CACHE.add(stored_prototype.id, stored_prototype.prototype)
    return stored_prototype.prototype

create_prototype = save_prototype  # alias

def delete_prototype(prototype_key, caller=None):
    """
    Delete a stored prototype

    Args:
        key (str): The persistent prototype to delete.
        caller (Account or Object, optionsl): Caller aiming to delete a prototype.
            Note that no locks will be checked if`caller` is not passed.
    Returns:
        success (bool): If deletion worked or not.
    Raises:
        PermissionError: If 'edit' lock was not passed or deletion failed for some other reason.

    """
        if mod:
        else:

    stored_prototype = DbPrototype.objects.filter(db_key__iexact=prototype_key)

    if not stored_prototype:
        raise PermissionError(
            _("Prototype {prototype_key} was not found.").format(prototype_key=prototype_key)
        )

    stored_prototype = stored_prototype[0]
    if caller:
        if not stored_prototype.access(caller, "edit"):
            raise PermissionError(
                _(
                    "{caller} needs explicit 'edit' permissions to "
                    "delete prototype {prototype_key}."
                ).format(caller=caller, prototype_key=prototype_key)
            )
    DB_PROTOTYPE_CACHE.remove(stored_prototype.id)
    stored_prototype.delete()

def search_prototype(
    key=None,
    tags=None,
    require_single=False,
    return_iterators=False,
    no_db=False,
):
    """
    Find prototypes based on key and/or tags, or all prototypes.

        key (str): An exact or partial key to query for.
        tags (str or list): Tag key or keys to query for. These
            will always be applied with the 'db_protototype'
            tag category.
        require_single (bool): If set, raise KeyError if the result
            was not found or if there are multiple matches.
        return_iterators (bool): Optimized return for large numbers of db-prototypes.
            If set, separate returns of module based prototypes and paginate
            the db-prototype return.
        no_db (bool): Optimization. If set, skip querying for database-generated prototypes and only
            include module-based prototypes. This can lead to a dramatic speedup since
            module-prototypes are static and require no db-lookup.

    Return:
        matches (list): Default return, all found prototype dicts. Empty list if
            no match was found. Note that if neither `key` nor `tags`
            were given, *all* available prototypes will be returned.
        list, queryset: If `return_iterators` are found, this is a list of
            module-based prototypes followed by a queryset of
            db-prototypes.

    Raises:
        KeyError: If `require_single` is True and there are 0 or >1 matches.

    Note:
        The available prototypes is a combination of those supplied in
        PROTOTYPE_MODULES and those stored in the database. Note that if
        tags are given and the prototype has no tags defined, it will not
        be found as a match.

    """

    def _search_module_based_prototypes(key, tags):
        """
        Helper function to load module-based prots.

        """
        # This will load the prototypes the first time they are searched
        loaded = getattr(load_module_prototypes, "_LOADED", False)
        if not loaded:
            load_module_prototypes()
            setattr(load_module_prototypes, "_LOADED", True)

        # search module prototypes

        mod_matches = {}
        if tags:
            # use tags to limit selection
            tagset = set(tags)
            mod_matches = {
                prototype_key: prototype
                for prototype_key, prototype in _MODULE_PROTOTYPES.items()
                if tagset.intersection(prototype.get("prototype_tags", []))
            }
        else:
            mod_matches = _MODULE_PROTOTYPES

        fuzzy_match_db = True
        if key:
            if key in mod_matches:
                # exact match
                module_prototypes = [mod_matches[key].copy()]
                fuzzy_match_db = False
            else:
                # fuzzy matching
                module_prototypes = [
                    prototype
                    for prototype_key, prototype in mod_matches.items()
                    if key in prototype_key
                ]
        else:
            # note - we return a copy of the prototype dict, otherwise using this with e.g.
            # prototype_from_object will modify the base prototype for every object
            module_prototypes = [match.copy() for match in mod_matches.values()]

        return module_prototypes, fuzzy_match_db

    def _search_db_based_prototypes(key, tags, fuzzy_matching):
        """
        Helper function for loading db-based prots.

        """
        # search db-stored prototypes
        if tags:
            # exact match on tag(s)
            tags = make_iter(tags)
            tag_categories = ["db_prototype" for _ in tags]
            query = DbPrototype.objects.get_by_tag(tags, tag_categories)
        else:
            query = DbPrototype.objects.all()

        if key:
            # exact or partial match on key
            exact_match = query.filter(Q(db_key__iexact=key))
            if not exact_match and fuzzy_matching:
                # try with partial match instead
                query = query.filter(Q(db_key__icontains=key))
            else:
                query = exact_match

        # convert to prototype, cached or from db

        db_matches = []
        not_found = []
        for db_id in query.values_list("id", flat=True).order_by("db_key"):
            prot = DB_PROTOTYPE_CACHE.get(db_id)
            if prot:
                db_matches.append(prot)
            else:
                not_found.append(db_id)

        if not_found:
            new_db_matches = (
                Attribute.objects.filter(scriptdb__pk__in=not_found, db_key="prototype")
                .values_list("db_value", flat=True)
                .order_by("scriptdb__db_key")
            )
            for db_id, prot in zip(not_found, new_db_matches):
                DB_PROTOTYPE_CACHE.add(db_id, prot)
            db_matches.extend(list(new_db_matches))

        return db_matches

    if key:

    module_prototypes, fuzzy_match_db = _search_module_based_prototypes(key, tags)

    db_prototypes = [] if no_db else _search_db_based_prototypes(key, tags, fuzzy_match_db)

    if key and require_single:
        num = len(module_prototypes) + len(db_prototypes)
        if num != 1:
            raise KeyError(_(f"Found {num} matching prototypes."))

    if return_iterators:
        # trying to get the entire set of prototypes - we must paginate
        # the result instead of trying to fetch the entire set at once
        return db_prototypes, module_prototypes
    else:
        # full fetch, no pagination (compatibility mode)
        return list(db_prototypes) + module_prototypes

def search_objects_with_prototype(prototype_key):
    """
    Retrieve all object instances created by a given prototype.

    Args:
        prototype_key (str): The exact (and unique) prototype identifier to query for.

    Returns:
        matches (Queryset): All matching objects spawned from this prototype.

    """
    return ObjectDB.objects.get_by_tag(key=prototype_key, category=PROTOTYPE_TAG_CATEGORY)

class PrototypeEvMore(EvMore):
    """
    Listing 1000+ prototypes can be very slow. So we customize EvMore to
    display an EvTable per paginated page rather than to try creating an
    EvTable for the entire dataset and then paginate it.

    """

    def __init__(self, caller, *args, session=None, **kwargs):
        """
        Store some extra properties on the EvMore class

        """
        self.show_non_use = kwargs.pop("show_non_use", False)
        self.show_non_edit = kwargs.pop("show_non_edit", False)
        super().__init__(caller, *args, session=session, **kwargs)

    def init_pages(self, inp):
        """
        This will be initialized with a tuple (mod_prototype_list, paginated_db_query)
        and we must handle these separately since they cannot be paginated in the same
        way. We will build the prototypes so that the db-prototypes come first (they
        are likely the most volatile), followed by the mod-prototypes.

        """
        dbprot_query, modprot_list = inp
        # set the number of entries per page to half the reported height of the screen
        # to account for long descs etc
        dbprot_paged = Paginator(dbprot_query, max(1, int(self.height / 2)))

        # we separate the different types of data, so we track how many pages there are
        # of each.
        n_mod = len(modprot_list)
        self._npages_mod = n_mod // self.height + (0 if n_mod % self.height == 0 else 1)
        self._db_count = dbprot_paged.count if dbprot_paged else 0
        self._npages_db = dbprot_paged.num_pages if self._db_count > 0 else 0
        # total number of pages
        self._npages = self._npages_mod + self._npages_db
        self._data = (dbprot_paged, modprot_list)
        self._paginator = self.prototype_paginator

    def prototype_paginator(self, pageno):
        """
        The listing is separated in db/mod prototypes, so we need to figure out which
        one to pick based on the page number. Also, pageno starts from 0.

        """
        dbprot_pages, modprot_list = self._data

        if self._db_count and pageno < self._npages_db:
            return dbprot_pages.page(pageno + 1)
        else:
            # get the correct slice, adjusted for the db-prototypes
            pageno = max(0, pageno - self._npages_db)
            return modprot_list[pageno * self.height : pageno * self.height + self.height]

    def page_formatter(self, page):
        """
        Input is a queryset page from django.Paginator

        """
        caller = self._caller

        # get use-permissions of readonly attributes (edit is always False)
            "|wKey|n",
            "|wSpawn/Edit|n",
            "|wTags|n",
            "|wDesc|n",
            crop=True,
        )

        for prototype in page:
            lock_use = caller.locks.check_lockstring(
                caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True
            )
            if not self.show_non_use and not lock_use:
                continue
            if prototype.get("prototype_key", "") in _MODULE_PROTOTYPES:
                lock_edit = False
            else:
                lock_edit = caller.locks.check_lockstring(
                    caller, prototype.get("prototype_locks", ""), access_type="edit", default=True
                )
            if not self.show_non_edit and not lock_edit:
                continue
            ptags = []
            for ptag in prototype.get("prototype_tags", []):
                if is_iter(ptag):
                    if len(ptag) > 1:
                        ptags.append("{}".format(ptag[0]))
                    else:
                        ptags.append(ptag[0])
                else:
                    ptags.append(str(ptag))

                prototype.get("prototype_key", "<unset>"),
                "{}/{}".format("Y" if lock_use else "N", "Y" if lock_edit else "N"),
                ", ".join(list(set(ptags))),
                prototype.get("prototype_desc", "<unset>"),
            )

def list_prototypes(
    caller, key=None, tags=None, show_non_use=False, show_non_edit=True, session=None
):
    """
    Collate a list of found prototypes based on search criteria and access.

    Args:
        caller (Account or Object): The object requesting the list.
        key (str, optional): Exact or partial prototype key to query for.
        tags (str or list, optional): Tag key or keys to query for.
        show_non_use (bool, optional): Show also prototypes the caller may not use.
        show_non_edit (bool, optional): Show also prototypes the caller may not edit.
        session (Session, optional): If given, this is used for display formatting.
    Returns:
        PrototypeEvMore: An EvMore subclass optimized for prototype listings.
        None: If no matches were found. In this case the caller has already been notified.

    """
    # this allows us to pass lists of empty strings
    tags = [tag for tag in make_iter(tags) if tag]

    dbprot_query, modprot_list = search_prototype(key, tags, return_iterators=True)

    if not dbprot_query and not modprot_list:
        caller.msg(_("No prototypes found."), session=session)

    # get specific prototype (one value or exception)
    return PrototypeEvMore(
        caller,
        (dbprot_query, modprot_list),
        show_non_use=show_non_use,
        show_non_edit=show_non_edit,
    )

def validate_prototype(
    prototype, protkey=None, protparents=None, is_prototype_base=True, strict=True, _flags=None
):
    """
    Run validation on a prototype, checking for inifinite regress.

    Args:
        prototype (dict): Prototype to validate.
        protkey (str, optional): The name of the prototype definition. If not given, the prototype
            dict needs to have the `prototype_key` field set.
        protparents (dict, optional): Additional prototype-parents, supposedly provided specifically
            for this prototype. If given, matching parents will first be taken from this
            dict rather than from the global set of prototypes found via settings/database.
        is_prototype_base (bool, optional): We are trying to create a new object *based on this
            object*. This means we can't allow 'mixin'-style prototypes without typeclass/parent
            etc.
        strict (bool, optional): If unset, don't require needed keys, only check against infinite
            recursion etc.
        _flags (dict, optional): Internal work dict that should not be set externally.
    Raises:
        RuntimeError: If prototype has invalid structure.
        RuntimeWarning: If prototype has issues that would make it unsuitable to build an object
            with (it may still be useful as a mix-in prototype).

    """
    assert isinstance(prototype, dict)
    protparents = {} if protparents is None else protparents

    if _flags is None:
        _flags = {"visited": [], "depth": 0, "typeclass": False, "errors": [], "warnings": []}

    protkey = protkey and protkey.lower() or prototype.get("prototype_key", None)

    if strict and not bool(protkey):
        _flags["errors"].append(_("Prototype lacks a 'prototype_key'."))
        protkey = "[UNSET]"

    typeclass = prototype.get("typeclass")
    prototype_parent = prototype.get("prototype_parent", [])

    if strict and not (typeclass or prototype_parent):
        if is_prototype_base:
            _flags["errors"].append(
                _("Prototype {protkey} requires `typeclass` or 'prototype_parent'.").format(
                    protkey=protkey
                )
            )
        else:
            _flags["warnings"].append(
                _(
                    "Prototype {protkey} can only be used as a mixin since it lacks "
                    "'typeclass' or 'prototype_parent' keys."
                ).format(protkey=protkey)
            )

    if strict and typeclass:
        try:
            class_from_module(typeclass)
                _(
                    "{err}: Prototype {protkey} is based on typeclass {typeclass}, "
                    "which could not be imported!"
                ).format(err=err, protkey=protkey, typeclass=typeclass)
            )

    if prototype_parent and isinstance(prototype_parent, dict):
        # the protparent is already embedded as a dict;
        prototype_parent = [prototype_parent]

    # recursively traverse prototype_parent chain
    for protstring in make_iter(prototype_parent):
        if isinstance(protstring, dict):
            # an already embedded prototype_parent
            protparent = protstring
            protstring = None
        else:
            protstring = protstring.lower()
            if protkey is not None and protstring == protkey:
                _flags["errors"].append(
                    _("Prototype {protkey} tries to parent itself.").format(protkey=protkey)
                )

            # get prototype parent, first try custom set, then search globally
            protparent = protparents.get(protstring)
            if not protparent:
                protparent = search_prototype(key=protstring, require_single=True)
                if protparent:
                    protparent = protparent[0]
                else:
                    _flags["errors"].append(
                        _(
                            "Prototype {protkey}'s `prototype_parent` (named '{parent}') was not"
                            " found."
                        ).format(protkey=protkey, parent=protstring)
                    )

        # check for infinite recursion
        if id(prototype) in _flags["visited"]:
                _("{protkey} has infinite nesting of prototypes.").format(
                    protkey=protkey or prototype
                )
            )

        if _flags["errors"]:
            raise RuntimeError(f"{_ERRSTR}: " + f"\n{_ERRSTR}: ".join(_flags["errors"]))
        _flags["visited"].append(id(prototype))
        _flags["depth"] += 1

        # next step of recursive validation
        validate_prototype(
            protparent,
            protkey=protstring,
            protparents=protparents,
            is_prototype_base=is_prototype_base,
            _flags=_flags,
        )

        _flags["visited"].pop()
        _flags["depth"] -= 1

    if typeclass and not _flags["typeclass"]:
        _flags["typeclass"] = typeclass

    # if we get back to the current level without a typeclass it's an error.
    if strict and is_prototype_base and _flags["depth"] <= 0 and not _flags["typeclass"]:
        _flags["errors"].append(
            _(
                "Prototype {protkey} has no `typeclass` defined anywhere in its parent\n "
                "chain. Add `typeclass`, or a `prototype_parent` pointing to a "
                "prototype with a typeclass."
            ).format(protkey=protkey)
        )

    if _flags["depth"] <= 0:
            raise RuntimeError(f"{_ERRSTR}:_" + f"\n{_ERRSTR}: ".join(_flags["errors"]))
        if _flags["warnings"]:
            raise RuntimeWarning(f"{_WARNSTR}: " + f"\n{_WARNSTR}: ".join(_flags["warnings"]))

    # make sure prototype_locks are set to defaults
    prototype_locks = [
        lstring.split(":", 1)
        for lstring in prototype.get("prototype_locks", "").split(";")
        if ":" in lstring
    ]
    locktypes = [tup[0].strip() for tup in prototype_locks]
    if "spawn" not in locktypes:
        prototype_locks.append(("spawn", "all()"))
    if "edit" not in locktypes:
        prototype_locks.append(("edit", "all()"))
    prototype_locks = ";".join(":".join(tup) for tup in prototype_locks)
    prototype["prototype_locks"] = prototype_locks

def protfunc_parser(
    value,
    available_functions=None,
    testing=False,
    stacktrace=False,
    caller=None,
    raise_errors=True,
    **kwargs,
):
    """
    Parse a prototype value string for a protfunc and process it.

    Available protfuncs are specified as callables in one of the modules of
    `settings.PROTFUNC_MODULES`, or specified on the command line.

    Args:
        value (any): The value to test for a parseable protfunc. Only strings will be parsed for
            protfuncs, all other types are returned as-is.
        available_functions (dict, optional): Mapping of name:protfunction to use for this parsing.
            If not set, use default sources.
        stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser.
        raise_errors (bool, optional): Raise explicit errors from malformed/not found protfunc
            calls.

        session (Session): Passed to protfunc. Session of the entity spawning the prototype.
        protototype (dict): Passed to protfunc. The dict this protfunc is a part of.
        current_key(str): Passed to protfunc. The key in the prototype that will hold this value.
        caller (Object or Account): This is necessary for certain protfuncs that perform object
            searches and have to check permissions.
        any (any): Passed on to the protfunc.

    Returns:
        any: A structure to replace the string on the prototype leve.  Note
        that FunctionParser functions $funcname(*args, **kwargs) can return any
        data type to insert into the prototype.

    """
    if not isinstance(value, str):

    result = FUNC_PARSER.parse_to_any(value, raise_errors=raise_errors, caller=caller, **kwargs)

# Various prototype utilities

def format_available_protfuncs():
    """
    Get all protfuncs in a pretty-formatted form.

    Args:
        clr (str, optional): What coloration tag to use.
    """
    out = []
    for protfunc_name, protfunc in FUNC_PARSER.callables.items():
                name=protfunc_name, docs=protfunc.__doc__.strip().replace("\n", "")
            )
        )
    return justify("\n".join(out), indent=8)

def prototype_to_str(prototype):
    """
    Format a prototype to a nice string representation.

    Args:
        prototype (dict): The prototype.
    """

    prototype = homogenize_prototype(prototype)

    header = """
|cprototype-key:|n {prototype_key}, |c-tags:|n {prototype_tags}, |c-locks:|n {prototype_locks}|n
|c-desc|n: {prototype_desc}
|cprototype-parent:|n {prototype_parent}
    \n""".format(
        prototype_key=prototype.get("prototype_key", "|r[UNSET](required)|n"),
        prototype_tags=prototype.get("prototype_tags", "|wNone|n"),
        prototype_locks=prototype.get("prototype_locks", "|wNone|n"),
        prototype_desc=prototype.get("prototype_desc", "|wNone|n"),
        prototype_parent=prototype.get("prototype_parent", "|wNone|n"),
    )
    key = aliases = attrs = tags = locks = permissions = location = home = destination = ""
    if "key" in prototype:
        key = prototype["key"]
        key = "|ckey:|n {key}".format(key=key)
    if "aliases" in prototype:
        aliases = prototype["aliases"]
        aliases = "|caliases:|n {aliases}".format(aliases=", ".join(aliases))
    if "attrs" in prototype:
        attrs = prototype["attrs"]
        out = []
        for attrkey, value, category, locks in attrs:
            locks = locks if isinstance(locks, str) else ", ".join(lock for lock in locks if lock)
            category = "|ccategory:|n {}".format(category) if category else ""
            cat_locks = ""
            if category or locks:
                cat_locks = " (|ccategory:|n {category}, ".format(
                    category=category if category else "|wNone|n"
                )
                "{attrkey}{cat_locks}{locks} |c=|n {value}".format(
                    attrkey=attrkey,
                    cat_locks=cat_locks,
                    locks=" |w(locks:|n {locks})".format(locks=locks) if locks else "",
                    value=value,
                )
            )
        attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out))
    if "tags" in prototype:
        tags = prototype["tags"]
        out = []
        for tagkey, category, data in tags:
                "{tagkey} (category: {category}{dat})".format(
                    tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else ""
                )
            )
        tags = "|ctags:|n\n {tags}".format(tags=", ".join(out))
    if "locks" in prototype:
        locks = prototype["locks"]
        locks = "|clocks:|n\n {locks}".format(locks=locks)
    if "permissions" in prototype:
        permissions = prototype["permissions"]
        permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions))
    if "location" in prototype:
        location = prototype["location"]
        location = "|clocation:|n {location}".format(location=location)
    if "home" in prototype:
        home = prototype["home"]
        home = "|chome:|n {home}".format(home=home)
    if "destination" in prototype:
        destination = prototype["destination"]
        destination = "|cdestination:|n {destination}".format(destination=destination)

    body = "\n".join(
        part
        for part in (key, aliases, attrs, tags, locks, permissions, location, home, destination)
        if part
    )

    return header.lstrip() + body.strip()

def check_permission(prototype_key, action, default=True):
    """
    Helper function to check access to actions on given prototype.

    Args:
        prototype_key (str): The prototype to affect.
        action (str): One of "spawn" or "edit".
        default (str): If action is unknown or prototype has no locks

    Returns:
        passes (bool): If permission for action is granted or not.

    """
    if action == "edit":
        if prototype_key in _MODULE_PROTOTYPES:
            mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key)
            if mod:
                err = _("{protkey} is a read-only prototype (defined as code in {module}).")
            else:
                err = _("{protkey} is a read-only prototype (passed directly as a dict).")
            logger.log_err(err.format(protkey=prototype_key, module=mod))

    prototype = search_prototype(key=prototype_key, require_single=True)
        prototype = prototype[0]
    else:
        logger.log_err("Prototype {} not found.".format(prototype_key))

    lockstring = prototype.get("prototype_locks")

    if lockstring:
        return check_lockstring(None, lockstring, default=default, access_type=action)

def init_spawn_value(
    value, validator=None, caller=None, prototype=None, protfunc_raise_errors=True
):
    """
    Analyze the prototype value and produce a value useful at the point of spawning.

    Args:
        value (any): This can be:
            callable - will be called as callable()
            (callable, (args,)) - will be called as callable(*args)
            other - will be assigned depending on the variable type
            validator (callable, optional): If given, this will be called with the value to
                check and guarantee the outcome is of a given type.
            caller (Object or Account): This is necessary for certain protfuncs that perform object
                searches and have to check permissions.
            prototype (dict): Prototype this is to be used for. Necessary for certain protfuncs.

    Returns:
        any (any): The (potentially pre-processed value to use for this prototype key)

    """
    validator = validator if validator else lambda o: o
        value = validator(value())
    elif value and isinstance(value, (list, tuple)) and callable(value[0]):
        # a structure (callable, (args, ))
        args = value[1:]
        value = validator(value[0](*make_iter(args)))
    else:
        value = validator(value)
    result = protfunc_parser(
        value, caller=caller, prototype=prototype, raise_errors=protfunc_raise_errors
    )
    if result != value:
        return validator(result)

def value_to_obj_or_any(value):
    "Convert value(s) to Object if possible, otherwise keep original value"
    stype = type(value)
    if is_iter(value):
        if stype == dict:
            return {
                value_to_obj_or_any(key): value_to_obj_or_any(val) for key, val in value.items()
            }
        else:
            return stype([value_to_obj_or_any(val) for val in value])
    obj = dbid_to_obj(value, ObjectDB)
    return obj if obj is not None else value

def value_to_obj(value, force=True):
    "Always convert value(s) to Object, or None"
            return {
            }
        else:
    return dbid_to_obj(value, ObjectDB)

"""
Spawner

The spawner takes input files containing object definitions in
dictionary forms. These use a prototype architecture to define
unique objects without having to make a Typeclass for each.

There  main function is `spawn(*prototype)`, where the `prototype`
is a dictionary like this:

```python
from evennia.prototypes import prototypes, spawner

prot = {
 "prototype_key": "goblin",
 "typeclass": "types.objects.Monster",
 "key": "goblin grunt",
 "health": lambda: randint(20,30),
 "resists": ["cold", "poison"],
 "attacks": ["fists"],
 "weaknesses": ["fire", "light"]
 "tags": ["mob", "evil", ('greenskin','mob')]
 "attrs": [("weapon", "sword")]
}
# spawn something with the prototype
goblin = spawner.spawn(prot)

# make this into a db-saved prototype (optional)
prot = prototypes.create_prototype(prot)

```

Possible keywords are:
    prototype_key (str):  name of this prototype. This is used when storing prototypes and should
        be unique. This should always be defined but for prototypes defined in modules, the
        variable holding the prototype dict will become the prototype_key if it's not explicitly
        given.
    prototype_desc (str, optional): describes prototype in listings
    prototype_locks (str, optional): locks for restricting access to this prototype. Locktypes
        supported are 'edit' and 'use'.
    prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype
        in listings
    prototype_parent (str, tuple or callable, optional): name (prototype_key) of eventual parent
        prototype, or a list of parents, for multiple left-to-right inheritance.
    prototype: Deprecated. Same meaning as 'parent'.

    typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use
        `settings.BASE_OBJECT_TYPECLASS`
    key (str or callable, optional): the name of the spawned object. If not given this will set to a
        random hash
    location (obj, str or callable, optional): location of the object - a valid object or #dbref
    home (obj, str or callable, optional): valid object or #dbref
    destination (obj, str or callable, optional): only valid for exits (object or #dbref)

    permissions (str, list or callable, optional): which permissions for spawned object to have
    locks (str or callable, optional): lock-string for the spawned object
    aliases (str, list or callable, optional): Aliases for the spawned object
    exec (str or callable, optional): this is a string of python code to execute or a list of such
        codes.  This can be used e.g. to trigger custom handlers on the object. The execution
        namespace contains 'evennia' for the library and 'obj'. All default spawn commands limit
        this functionality to Developer/superusers. Usually it's better to use callables or
        prototypefuncs instead of this.
    tags (str, tuple, list or callable, optional): string or list of strings or tuples
        `(tagstr, category)`. Plain strings will be result in tags with no category (default tags).
    attrs (tuple, list or callable, optional): tuple or list of tuples of Attributes to add. This
        form allows more complex Attributes to be set. Tuples at least specify `(key, value)`
        but can also specify up to `(key, value, category, lockstring)`. If you want to specify a
        lockstring but not a category, set the category to `None`.
    ndb_<name> (any): value of a nattribute (ndb_ is stripped) - this is of limited use.
    other (any): any other name is interpreted as the key of an Attribute with
        its value. Such Attributes have no categories.

Each value can also be a callable that takes no arguments. It should
return the value to enter into the field and will be called every time
the prototype is used to spawn an object. Note, if you want to store
a callable in an Attribute, embed it in a tuple to the `args` keyword.

By specifying the "prototype_parent" key, the prototype becomes a child of
the given prototype, inheritng all prototype slots it does not explicitly
define itself, while overloading those that it does specify.

```python

{
 "prototype_key": "goblin_wizard",
 "prototype_parent": "GOBLIN",
 "key": "goblin wizard",
 "spells": ["fire ball", "lighting bolt"]
 }

GOBLIN_ARCHER = {
 "key": "goblin archer",
 "attack_skill": (random, (5, 10))"
 "attacks": ["short bow"]
}
```

One can also have multiple prototypes. These are inherited from the
left, with the ones further to the right taking precedence.

```python
ARCHWIZARD = {
 "attack": ["archwizard staff", "eye of doom"]

GOBLIN_ARCHWIZARD = {
 "key" : "goblin archwizard"
 "prototype_parent": ("GOBLIN_WIZARD", "ARCHWIZARD"),
}
```

The *goblin archwizard* will have some different attacks, but will
otherwise have the same spells as a *goblin wizard* who in turn shares
many traits with a normal *goblin*.

Storage mechanism:

This sets up a central storage for prototypes. The idea is to make these
available in a repository for buildiers to use. Each prototype is stored
in a Script so that it can be tagged for quick sorting/finding and locked for limiting
access.

This system also takes into consideration prototypes defined and stored in modules.
Such prototypes are considered 'read-only' to the system and can only be modified
in code. To replace a default prototype, add the same-name prototype in a
custom module read later in the settings.PROTOTYPE_MODULES list. To remove a default
prototype, override its name with an empty dict.

"""

import copy

from evennia.prototypes.prototypes import (
    PROTOTYPE_TAG_CATEGORY,
    init_spawn_value,
    search_prototype,
    value_to_obj,
    value_to_obj_or_any,
)
from evennia.utils.utils import class_from_module, is_iter, make_iter

_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
)
_PROTOTYPE_ROOT_NAMES = (
    "key",
    "attrs",
    "tags",
    "locks",
    "home",
)
_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES

class Unset:
    """
    Helper class representing a non-set diff element.

    """

    def __bool__(self):

        return "<Unset>"

# Helper

def _get_prototype(inprot, protparents=None, uninherited=None, _workprot=None):
    """
    Recursively traverse a prototype dictionary, including multiple
    inheritance. Use validate_prototype before this, we don't check
    for infinite recursion here.

    Args:
        inprot (dict): Prototype dict (the individual prototype, with no inheritance included).
        protparents (dict): Custom protparents, supposedly provided specifically for this `inprot`.
            If given, any parents will first be looked up in this dict, and then by searching
            the global prototype store given by settings/db.
        uninherited (dict): Parts of prototype to not inherit.
        _workprot (dict, optional): Work dict for the recursive algorithm.

    Returns:
        merged (dict): A prototype where parent's have been merged as needed (the
            `prototype_parent` key is removed).

    """

    def _inherit_tags(old_tags, new_tags):
        old = {(tup[0], tup[1]): tup for tup in old_tags}
        new = {(tup[0], tup[1]): tup for tup in new_tags}
        old.update(new)
        return list(old.values())

    def _inherit_attrs(old_attrs, new_attrs):
        old = {(tup[0], tup[2]): tup for tup in old_attrs}
        new = {(tup[0], tup[2]): tup for tup in new_attrs}

    _workprot = {} if _workprot is None else _workprot
    if "prototype_parent" in inprot:
        # move backwards through the inheritance

        prototype_parents = inprot["prototype_parent"]
        if isinstance(prototype_parents, dict):
            # protparent already embedded as-is
            prototype_parents = [prototype_parents]

        for prototype in make_iter(prototype_parents):
            if isinstance(prototype, dict):
                # protparent already embedded as-is
                parent_prototype = prototype
            else:
                # protparent given by-name, first search provided parents, then global store
                parent_prototype = protparents.get(prototype.lower())
                if not parent_prototype:
                    parent_prototype = search_prototype(key=prototype.lower()) or {}
                    if parent_prototype:
                        parent_prototype = parent_prototype[0]

            # Build the prot dictionary in reverse order, overloading
            new_prot = _get_prototype(parent_prototype, protparents, _workprot=_workprot)

            # attrs, tags have internal structure that should be inherited separately
            new_prot["attrs"] = _inherit_attrs(
                _workprot.get("attrs", {}), new_prot.get("attrs", [])
            )
            new_prot["tags"] = _inherit_tags(_workprot.get("tags", []), new_prot.get("tags", []))

            _workprot.update(new_prot)
    # the inprot represents a higher level (a child prot), which should override parents

    inprot["attrs"] = _inherit_attrs(_workprot.get("attrs", []), inprot.get("attrs", []))
    inprot["tags"] = _inherit_tags(_workprot.get("tags", []), inprot.get("tags", []))
    _workprot.update(inprot)
    if uninherited:
        # put back the parts that should not be inherited
        _workprot.update(uninherited)
    _workprot.pop("prototype_parent", None)  # we don't need this for spawning
    return _workprot

def flatten_prototype(prototype, validate=False, no_db=False):
    """
    Produce a 'flattened' prototype, where all prototype parents in the inheritance tree have been
    merged into a final prototype.

    Args:
        prototype (dict): Prototype to flatten. Its `prototype_parent` field will be parsed.
        validate (bool, optional): Validate for valid keys etc.
        no_db (bool, optional): Don't search db-based prototypes. This can speed up
            searching dramatically since module-based prototypes are static.

    Returns:
        flattened (dict): The final, flattened prototype.

    """

        prototype = protlib.homogenize_prototype(prototype)
        protlib.validate_prototype(prototype, is_prototype_base=validate, strict=validate)
        return _get_prototype(
            prototype, uninherited={"prototype_key": prototype.get("prototype_key")}
        )
    return {}

# obj-related prototype functions

def prototype_from_object(obj):
    """
    Guess a minimal prototype from an existing object.

    Args:
        obj (Object): An object to analyze.

    Returns:
        prototype (dict): A prototype estimating the current state of the object.

    """
    # first, check if this object already has a prototype

    prot = obj.tags.get(category=PROTOTYPE_TAG_CATEGORY, return_list=True)
    if prot:
        prot = protlib.search_prototype(prot[0])

    if not prot or len(prot) > 1:
        # no unambiguous prototype found - build new prototype
        prot = {}
        prot["prototype_key"] = "From-Object-{}-{}".format(
            obj.key, hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7]
        )
        prot["prototype_desc"] = "Built from {}".format(str(obj))
        prot["prototype_locks"] = "spawn:all();edit:all()"
        prot["prototype_tags"] = []
    else:
        prot = prot[0]

    prot["key"] = obj.db_key or hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:6]
    prot["typeclass"] = obj.db_typeclass_path

    location = obj.db_location
        prot["location"] = location.dbref
    home = obj.db_home
    if home:
        prot["home"] = home.dbref
    destination = obj.db_destination
    if destination:
        prot["destination"] = destination.dbref
    locks = obj.locks.all()
    if locks:
        prot["locks"] = ";".join(locks)
    perms = obj.permissions.get(return_list=True)
    if perms:
        prot["permissions"] = make_iter(perms)
    aliases = obj.aliases.get(return_list=True)
    if aliases:
        prot["aliases"] = aliases
    tags = sorted(
        [(tag.db_key, tag.db_category, tag.db_data) for tag in obj.tags.all(return_objs=True)],
        key=lambda tup: (str(tup[0]), tup[1] or "", tup[2] or ""),
    )
    if tags:
        prot["tags"] = tags
    attrs = sorted(
        [
            (attr.key, attr.value, attr.category, ";".join(attr.locks.all()))
            for attr in obj.attributes.all()
        ],
        key=lambda tup: (str(tup[0]), tup[1] or "", tup[2] or "", tup[3]),
    )
    if attrs:
        prot["attrs"] = attrs

    return prot

def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False, implicit_keep=False):
    """
    A 'detailed' diff specifies differences down to individual sub-sections
    of the prototype, like individual attributes, permissions etc. It is used
    by the menu to allow a user to customize what should be kept.

    Args:
        prototype1 (dict): Original prototype.
        prototype2 (dict): Comparison prototype.
        maxdepth (int, optional): The maximum depth into the diff we go before treating the elements
            of iterables as individual entities to compare. This is important since a single
            attr/tag (for example) are represented by a tuple.
        homogenize (bool, optional): Auto-homogenize both prototypes for the best comparison.
            This is most useful for displaying.
        implicit_keep (bool, optional): If set, the resulting diff will assume KEEP unless the new
            prototype explicitly change them. That is, if a key exists in `prototype1` and
            not in `prototype2`, it will not be REMOVEd but set to KEEP instead. This is
            particularly useful for auto-generated prototypes when updating objects.

    Returns:
        diff (dict): A structure detailing how to convert prototype1 to prototype2. All
            nested structures are dicts with keys matching either the prototype's matching
            key or the first element in the tuple describing the prototype value (so for
            a tag tuple `(tagname, category)` the second-level key in the diff would be tagname).
            The the bottom level of the diff consist of tuples `(old, new, instruction)`, where
            instruction can be one of "REMOVE", "ADD", "UPDATE" or "KEEP".

    """
    _unset = Unset()

    def _recursive_diff(old, new, depth=0):
        old_type = type(old)
        new_type = type(new)

        if old_type == new_type and not (old or new):
            # both old and new are unset, like [] or None
            return (None, None, "KEEP")
        if old_type != new_type:
            if old and not new:
                if depth < maxdepth and old_type == dict:
                    return {key: (part, None, "REMOVE") for key, part in old.items()}
                elif depth < maxdepth and is_iter(old):
                    return {
                        part[0] if is_iter(part) else part: (part, None, "REMOVE") for part in old
                    }
                if isinstance(new, Unset) and implicit_keep:
                    # the new does not define any change, use implicit-keep
                    return (old, None, "KEEP")
                return (old, new, "REMOVE")
            elif not old and new:
                if depth < maxdepth and new_type == dict:
                    return {key: (None, part, "ADD") for key, part in new.items()}
                elif depth < maxdepth and is_iter(new):
                    return {part[0] if is_iter(part) else part: (None, part, "ADD") for part in new}
                return (old, new, "ADD")
            else:
                # this condition should not occur in a standard diff
                return (old, new, "UPDATE")
        elif depth < maxdepth and new_type == dict:
            all_keys = set(list(old.keys()) + list(new.keys()))
            return {
                key: _recursive_diff(old.get(key, _unset), new.get(key, _unset), depth=depth + 1)
                for key in all_keys
            }
        elif depth < maxdepth and is_iter(new):
            old_map = {part[0] if is_iter(part) else part: part for part in old}
            new_map = {part[0] if is_iter(part) else part: part for part in new}
            all_keys = set(list(old_map.keys()) + list(new_map.keys()))
            return {
                key: _recursive_diff(
                    old_map.get(key, _unset), new_map.get(key, _unset), depth=depth + 1
                )
            }
        elif old != new:
            return (old, new, "UPDATE")
        else:
            return (old, new, "KEEP")

    prot1 = protlib.homogenize_prototype(prototype1) if homogenize else prototype1
    prot2 = protlib.homogenize_prototype(prototype2) if homogenize else prototype2

    diff = _recursive_diff(prot1, prot2)

    return diff

def flatten_diff(diff):
    """
    For spawning, a 'detailed' diff is not necessary, rather we just want instructions on how to
    handle each root key.

    Args:
        diff (dict): Diff produced by `prototype_diff` and
            possibly modified by the user. Note that also a pre-flattened diff will come out
            unchanged by this function.

    Returns:
        flattened_diff (dict): A flat structure detailing how to operate on each
            root component of the prototype.

    Notes:
        The flattened diff has the following possible instructions:
            UPDATE, REPLACE, REMOVE
        Many of the detailed diff's values can hold nested structures with their own
        individual instructions. A detailed diff can have the following instructions:
            REMOVE, ADD, UPDATE, KEEP
        Here's how they are translated:
            - All REMOVE -> REMOVE
            - All ADD|UPDATE -> UPDATE
            - All KEEP -> KEEP
            - Mix KEEP, UPDATE, ADD -> UPDATE
            - Mix REMOVE, KEEP, UPDATE, ADD -> REPLACE
    """

    def _get_all_nested_diff_instructions(diffpart):
        "Started for each root key, returns all instructions nested under it"
        out = []
            out = [diffpart[2]]
        elif typ == dict:
            # all other are dicts
            for val in diffpart.values():
                out.extend(_get_all_nested_diff_instructions(val))
        else:
            raise RuntimeError(
                _(
                    "Diff contains non-dicts that are not on the "
                    "form (old, new, action_to_take): {diffpart}"
                ).format(diffpart)
            )
        return out

    flat_diff = {}

    # flatten diff based on rules
    for rootkey, diffpart in diff.items():
        insts = _get_all_nested_diff_instructions(diffpart)
        if all(inst == "KEEP" for inst in insts):
            rootinst = "KEEP"
        elif all(inst in ("ADD", "UPDATE") for inst in insts):
            rootinst = "UPDATE"
        elif all(inst == "REMOVE" for inst in insts):
            rootinst = "REMOVE"
        elif "REMOVE" in insts:
            rootinst = "REPLACE"
        else:

        flat_diff[rootkey] = rootinst

    return flat_diff

def prototype_diff_from_object(prototype, obj, implicit_keep=True):
    """
    Get a simple diff for a prototype compared to an object which may or may not already have a
    prototype (or has one but changed locally). For more complex migratations a manual diff may be
    needed.

    Args:
        prototype (dict): New prototype.
        obj (Object): Object to compare prototype against.

    Returns:
        diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...}
        obj_prototype (dict): The prototype calculated for the given object. The diff is how to
            convert this prototype into the new prototype.
        implicit_keep (bool, optional): This is usually what one wants for object updating. When
            set, this means the prototype diff will assume KEEP on differences
            between the object-generated prototype and that which is not explicitly set in the
            new prototype. This means e.g. that even though the object has a location, and the
            prototype does not specify the location, it will not be unset.

    Notes:
        The `diff` is on the following form:

            {"key": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"),
                "attrs": {"attrkey": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"),
                          "attrkey": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"), ...},
                "aliases": {"aliasname": (old, new, "KEEP...", ...},
                ... }

    """
    obj_prototype = prototype_from_object(obj)
    diff = prototype_diff(
        obj_prototype, protlib.homogenize_prototype(prototype), implicit_keep=implicit_keep
    )
    return diff, obj_prototype

def format_diff(diff, minimal=True):
    """
    Reformat a diff for presentation. This is a shortened version
    of the olc _format_diff_text_and_options without the options.

    Args:
        minimal (bool, optional): Only show changes (remove KEEPs)

    Returns:
        texts (str): The formatted text.

    """

        if is_iter(obj):
            if not obj:
                return "{} |w=|n {} |w(category:|n |n{}|w, locks:|n {}|w)|n".format(*obj)
                return "{} |w(category:|n {}|w)|n".format(obj[0], obj[1])

    def _parse_diffpart(diffpart, rootname):
        texts = []
                    texts.append("   |gKEEP|n: {old}".format(old=_visualize(old, rootname)))
            elif instruction == "ADD":
                texts.append("   |yADD|n: {new}".format(new=_visualize(new, rootname)))
            elif instruction == "REMOVE" and not new:
                texts.append("   |rREMOVE|n: {old}".format(old=_visualize(old, rootname)))
            else:
                vinst = "   |rREMOVE|n" if instruction == "REMOVE" else "|y{}|n".format(instruction)
                varrow = "|r->|n" if instruction == "REMOVE" else "|y->|n"
                texts.append(
                    "   {inst}|W:|n {old} |W{varrow}|n{sep} {new}".format(
                        inst=vinst, old=vold, varrow=varrow, sep=vsep, new=vnew
                    )
                )
        else:
                text = _parse_diffpart(subdiffpart, rootname)
        return texts

    texts = []

        text = _parse_diffpart(diffpart, root_key)
        if text or not minimal:
            heading = "- |w{}:|n\n".format(root_key)
            if text:
                text = [heading + text[0]] + text[1:]
            else:
                text = [heading]

            texts.extend(text)

    return "\n ".join(line for line in texts if line)

def batch_update_objects_with_prototype(
    prototype, diff=None, objects=None, exact=False, caller=None, protfunc_raise_errors=True
):
    """
    Update existing objects with the latest version of the prototype.

    Args:
        prototype (str or dict): Either the `prototype_key` to use or the
            prototype dict itself.
        diff (dict, optional): This a diff structure that describes how to update the protototype.
            If not given this will be constructed from the first object found.
        objects (list, optional): List of objects to update. If not given, query for these
            objects using the prototype's `prototype_key`.
        exact (bool, optional): By default (`False`), keys not explicitly in the prototype will
            not be applied to the object, but will be retained as-is. This is usually what is
            expected - for example, one usually do not want to remove the object's location even
            if it's not set in the prototype. With `exact=True`, all un-specified properties of the
            objects will be removed if they exist. This will lead to a more accurate 1:1 correlation
            between the  object and the prototype but is usually impractical.
        caller (Object or Account, optional): This may be used by protfuncs to do permission checks.
        protfunc_raise_errors (bool): Have protfuncs raise explicit errors if malformed/not found.
            This is highly recommended.
    Returns:
        changed (int): The number of objects that had changes applied to them.

    """
    prototype = protlib.homogenize_prototype(prototype)

    if isinstance(prototype, str):
        new_prototype = protlib.search_prototype(prototype)
        if new_prototype:
            new_prototype = new_prototype[0]
    else:
        new_prototype = prototype

    prototype_key = new_prototype["prototype_key"]

    if not objects:
        objects = ObjectDB.objects.get_by_tag(prototype_key, category=PROTOTYPE_TAG_CATEGORY)

        return 0

        diff, _ = prototype_diff_from_object(new_prototype, objects[0])

    # make sure the diff is flattened
    diff = flatten_diff(diff)

    changed = 0
    for obj in objects:
        do_save = False

        old_prot_key = obj.tags.get(category=PROTOTYPE_TAG_CATEGORY, return_list=True)
        old_prot_key = old_prot_key[0] if old_prot_key else None

        try:
            for key, directive in diff.items():
                if key not in new_prototype and not exact:
                    # we don't update the object if the prototype does not actually
                    # contain the key (the diff will report REMOVE but we ignore it
                    # since exact=False)
                    continue

                if directive in ("UPDATE", "REPLACE"):
                    if key in _PROTOTYPE_META_NAMES:
                        # prototype meta keys are not stored on-object
                        continue

                    val = new_prototype[key]
                    do_save = True

                    def _init(val, typ):
                        return init_spawn_value(
                            val,
                            typ,
                            caller=caller,
                            prototype=new_prototype,
                            protfunc_raise_errors=protfunc_raise_errors,
                        )

                    if key == "key":
                        obj.db_key = _init(val, str)
                    elif key == "typeclass":
                        obj.db_typeclass_path = _init(val, str)
                    elif key == "location":
                        obj.db_location = _init(val, value_to_obj)
                    elif key == "home":
                        obj.db_home = _init(val, value_to_obj)
                    elif key == "destination":
                        obj.db_destination = _init(val, value_to_obj)
                    elif key == "locks":
                        if directive == "REPLACE":
                            obj.locks.clear()
                        obj.locks.add(_init(val, str))
                    elif key == "permissions":
                            obj.permissions.clear()
                        obj.permissions.batch_add(*(_init(perm, str) for perm in val))
                    elif key == "aliases":
                            obj.aliases.clear()
                        obj.aliases.batch_add(*(_init(alias, str) for alias in val))
                    elif key == "tags":
                            obj.tags.clear()
                        obj.tags.batch_add(
                            *(
                                (_init(ttag, str), tcategory, tdata)
                                for ttag, tcategory, tdata in val
                            )
                        )
                    elif key == "attrs":
                            obj.attributes.clear()
                        obj.attributes.batch_add(
                            *(
                                (
                                    _init(akey, str),
                                    _init(aval, value_to_obj),
                                    acategory,
                                    alocks,
                                )
                                for akey, aval, acategory, alocks in val
                            )
                        )
                    elif key == "exec":
                        # we don't auto-rerun exec statements, it would be huge security risk!
                        pass
                    else:
                        obj.attributes.add(key, _init(val, value_to_obj))
                elif directive == "REMOVE":
                        obj.db_key = ""
                        # fall back to default
                        obj.db_typeclass_path = settings.BASE_OBJECT_TYPECLASS
                        obj.db_location = None
                        obj.db_home = None
                        obj.db_destination = None
                        obj.locks.clear()
                        obj.permissions.clear()
                        obj.aliases.clear()
                        obj.tags.clear()
                        obj.attributes.clear()
                        pass
                    else:
                        obj.attributes.remove(key)
            logger.log_trace(f"Failed to apply prototype '{prototype_key}' to {obj}.")
        finally:
            # we must always make sure to re-add the prototype tag
            obj.tags.clear(category=PROTOTYPE_TAG_CATEGORY)
            obj.tags.add(prototype_key, category=PROTOTYPE_TAG_CATEGORY)

        if do_save:
            changed += 1
            obj.save()

    return changed

def batch_create_object(*objparams):
    """
    This is a cut-down version of the create_object() function,
    optimized for speed. It does NOT check and convert various input
    so make sure the spawned Typeclass works before using this!

    Args:
        objsparams (tuple): Each paremter tuple will create one object instance using the parameters
            within.
            The parameters should be given in the following order:
                - `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`.
                - `permissions` (str): Permission string used with `new_obj.batch_add(permission)`.
                - `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`.
                - `aliases` (list): A list of alias strings for
                    adding with `new_object.aliases.batch_add(*aliases)`.
                - `nattributes` (list): list of tuples `(key, value)` to be loop-added to
                    add with `new_obj.nattributes.add(*tuple)`.
                - `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for
                    adding with `new_obj.attributes.batch_add(*attributes)`.
                - `tags` (list): list of tuples `(key, category)` for adding
                    with `new_obj.tags.batch_add(*tags)`.
                - `execs` (list): Code strings to execute together with the creation
                    of each object. They will be executed with `evennia` and `obj`
                        (the newly created object) available in the namespace. Execution
                        will happend after all other properties have been assigned and
                        is intended for calling custom handlers etc.

    Returns:
        objects (list): A list of created objects

    Notes:
        The `exec` list will execute arbitrary python code so don't allow this to be available to
        unprivileged users!

    """

    # bulk create all objects in one go

    # unfortunately this doesn't work since bulk_create doesn't creates pks;
    # the result would be duplicate objects at the next stage, so we comment
    # it out for now:
    #  dbobjs = _ObjectDB.objects.bulk_create(dbobjs)

    objs = []
    for objparam in objparams:
        obj = ObjectDB(**objparam[0])

        # setup
        obj._createdict = {
            "permissions": make_iter(objparam[1]),
            "locks": objparam[2],
            "aliases": make_iter(objparam[3]),
            "nattributes": objparam[4],
            "attributes": objparam[5],
            "tags": make_iter(objparam[6]),
        }
        # this triggers all hooks
        obj.save()
        # run eventual extra code
        for code in objparam[7]:
            if code:
                exec(code, {}, {"evennia": evennia, "obj": obj})
        objs.append(obj)
    return objs

# Spawner mechanism

def spawn(*prototypes, caller=None, **kwargs):
    """
    Spawn a number of prototyped objects.

    Args:
        prototypes (str or dict): Each argument should either be a
            prototype_key (will be used to find the prototype) or a full prototype
            dictionary. These will be batched-spawned as one object each.
        caller (Object or Account, optional): This may be used by protfuncs to do access checks.
        prototype_modules (str or list): A python-path to a prototype
            module, or a list of such paths. These will be used to build
            the global protparents dictionary accessible by the input
            prototypes. If not given, it will instead look for modules
            defined by settings.PROTOTYPE_MODULES.
        prototype_parents (dict): A dictionary holding a custom
            prototype-parent dictionary. Will overload same-named
            prototypes from prototype_modules.
        only_validate (bool): Only run validation of prototype/parents
            (no object creation) and return the create-kwargs.
        protfunc_raise_errors (bool): Raise explicit exceptions on a malformed/not-found
            protfunc. Defaults to True.

    Returns:
        object (Object, dict or list): Spawned object(s). If `only_validate` is given, return
            a list of the creation kwargs to build the object(s) without actually creating it.

    """
    # search string (=prototype_key) from input
    prototypes = [
        protlib.search_prototype(prot, require_single=True)[0] if isinstance(prot, str) else prot
        for prot in prototypes
    ]

    if not kwargs.get("only_validate"):
        # homogenization to be more lenient about prototype format when entering the prototype
        # manually
        prototypes = [protlib.homogenize_prototype(prot) for prot in prototypes]

    # overload module's protparents with specifically given protparents
    # we allow prototype_key to be the key of the protparent dict, to allow for module-level
    # prototype imports. We need to insert prototype_key in this case
    custom_protparents = {}
    for key, protparent in kwargs.get("prototype_parents", {}).items():
        key = str(key).lower()
        protparent["prototype_key"] = str(protparent.get("prototype_key", key)).lower()
        custom_protparents[key] = protlib.homogenize_prototype(protparent)

    objsparams = []
    for prototype in prototypes:
        # run validation and homogenization of provided prototypes
        protlib.validate_prototype(
            prototype, None, protparents=custom_protparents, is_prototype_base=True
        )
        prot = _get_prototype(
            prototype,
            protparents=custom_protparents,
            uninherited={"prototype_key": prototype.get("prototype_key")},
        )
        if not prot:
            continue

        # extract the keyword args we need to create the object itself. If we get a callable,
        # call that to get the value (don't catch errors)
        create_kwargs = {}
        init_spawn_kwargs = dict(
            protfunc_raise_errors=kwargs.get("protfunc_raise_errors", True),
        )

        # we must always add a key, so if not given we use a shortened md5 hash. There is a (small)
        # chance this is not unique but it should usually not be a problem.
        val = prot.pop(
            "key",
            "Spawned-{}".format(hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:6]),
        )
        create_kwargs["db_key"] = init_spawn_value(val, str, **init_spawn_kwargs)

        val = prot.pop("location", None)
        create_kwargs["db_location"] = init_spawn_value(val, value_to_obj, **init_spawn_kwargs)

        val = prot.pop("home", None)
        if val:
            create_kwargs["db_home"] = init_spawn_value(val, value_to_obj, **init_spawn_kwargs)
        else:
            try:
                create_kwargs["db_home"] = init_spawn_value(
                    settings.DEFAULT_HOME, value_to_obj, **init_spawn_kwargs
                )
            except ObjectDB.DoesNotExist:
                # settings.DEFAULT_HOME not existing is common for unittests
                pass

        val = prot.pop("destination", None)
        create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj, **init_spawn_kwargs)

        # we need the 'true' path to the typeclass (not its alias), so we make sure to load the typeclass
        # and use its path directly
        val = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
        typeclass = class_from_module(
            init_spawn_value(val, str, **init_spawn_kwargs), settings.TYPECLASS_PATHS
        )
        create_kwargs["db_typeclass_path"] = f"{typeclass.__module__}.{typeclass.__name__}"

        # extract calls to handlers
        val = prot.pop("permissions", [])
        permission_string = init_spawn_value(val, make_iter, **init_spawn_kwargs)
        val = prot.pop("locks", "")
        lock_string = init_spawn_value(val, str, **init_spawn_kwargs)
        val = prot.pop("aliases", [])
        alias_string = init_spawn_value(val, make_iter, **init_spawn_kwargs)

        val = prot.pop("tags", [])
        tags = []
        for tag, category, *data in val:
            tags.append(
                (
                    init_spawn_value(tag, str, **init_spawn_kwargs),
                    category,
                    data[0] if data else None,
                )
            )

        prototype_key = prototype.get("prototype_key", None)
        if prototype_key:
            # we make sure to add a tag identifying which prototype created this object
            tags.append((prototype_key, PROTOTYPE_TAG_CATEGORY))

        val = prot.pop("exec", "")
        execs = init_spawn_value(val, make_iter, **init_spawn_kwargs)

        # extract ndb assignments
        nattributes = dict(
            (
                key.split("_", 1)[1],
                init_spawn_value(val, value_to_obj, **init_spawn_kwargs),
            )
            for key, val in prot.items()
            if key.startswith("ndb_")
        )

        # the rest are attribute tuples (attrname, value, category, locks)
        val = make_iter(prot.pop("attrs", []))
        attributes = []
        for attrname, value, *rest in val:
            attributes.append(
                (
                    attrname,
                    init_spawn_value(value, **init_spawn_kwargs),
                    rest[0] if rest else None,
                    rest[1] if len(rest) > 1 else None,
                )
            )

        simple_attributes = []
        for key, value in (
            (key, value) for key, value in prot.items() if not (key.startswith("ndb_"))
        ):
            # we don't support categories, nor locks for simple attributes
            if key in _PROTOTYPE_META_NAMES:
                continue
            else:
                simple_attributes.append(
                    (
                        key,
                        init_spawn_value(value, value_to_obj_or_any, **init_spawn_kwargs),
                        None,
                        None,
                    )
                )

        attributes = attributes + simple_attributes
        attributes = [tup for tup in attributes if not tup[0] in _NON_CREATE_KWARGS]

        # pack for call into _batch_create_object
        objsparams.append(
            (
                create_kwargs,
                permission_string,
                lock_string,
                alias_string,
                nattributes,
                attributes,
                tags,
                execs,
            )
        )

    if kwargs.get("only_validate"):
        return objsparams
    return batch_create_object(*objsparams)

"""
The custom manager for Scripts.
"""

__all__ = ("ScriptManager", "ScriptDBManager")

VALIDATE_ITERATION = 0

class ScriptDBManager(TypedObjectManager):
    """
    This Scriptmanager implements methods for searching
    and manipulating Scripts directly from the database.

    get_all_scripts_on_obj
    get_all_scripts
    delete_script
    remove_non_persistent
    validate
    script_search (equivalent to evennia.search_script)
    copy_script

    """

    def get_all_scripts_on_obj(self, obj, key=None):
        """
        Find all Scripts related to a particular object.

        Args:
            obj (Object): Object whose Scripts we are looking for.
            key (str, optional): Script identifier - can be given as a
                dbref or name string. If given, only scripts matching the
                key on the object will be returned.
        Returns:
            matches (list): Matching scripts.

        """
        if not obj:
            return []
        account = _GA(_GA(obj, "__dbclass__"), "__name__") == "AccountDB"
        if key:
            dbref = self.dbref(key)
            if dbref or dbref == 0:
                    return self.filter(db_account=obj, id=dbref)
                else:
                    return self.filter(db_obj=obj, id=dbref)
            elif account:
                return self.filter(db_account=obj, db_key=key)
            else:
                return self.filter(db_obj=obj, db_key=key)
            return self.filter(db_account=obj)
        else:
            return self.filter(db_obj=obj)

    def get_all_scripts(self, key=None):
        """
        Get all scripts in the database.

        Args:
            key (str or int, optional): Restrict result to only those
                with matching key or dbref.

        Returns:
            scripts (list): All scripts found, or those matching `key`.

        """
        if key:
            if dbref:
                return self.filter(id=dbref)
            return self.filter(db_key__iexact=key.strip())

    def delete_script(self, dbref):
        """
        This stops and deletes a specific script directly from the
        script database.

        Args:
            dbref (int): Database unique id.

        Notes:
            This might be needed for global scripts not tied to a
            specific game object

        """
        scripts = self.get_id(dbref)
        for script in make_iter(scripts):
            script.stop()

    def update_scripts_after_server_start(self):
        """
        Update/sync/restart/delete scripts after server shutdown/restart.

        """
        for script in self.filter(db_is_active=True, db_persistent=False):
            script._stop_task()

        for script in self.filter(db_is_active=True):
            script._unpause_task(auto_unpause=True)
            script.at_server_start()

        for script in self.filter(db_is_active=False):

    def search_script(self, ostring, obj=None, only_timed=False, typeclass=None):
        """
        Search for a particular script.

        Args:
            ostring (str): Search criterion - a script dbef or key.
            obj (Object, optional): Limit search to scripts defined on
                this object
            only_timed (bool): Limit search only to scripts that run
                on a timer.
            typeclass (class or str): Typeclass or path to typeclass.

        Returns:
            Queryset: An iterable with 0, 1 or more results.

        """

        ostring = ostring.strip()

        if dbref:
            # this is a dbref, try to find the script directly
                if not (obj and obj != dmatch.obj) or (only_timed and dmatch.interval):

                typeclass = "%s.%s" % (typeclass.__module__, typeclass.__name__)
            else:
                typeclass = "%s" % typeclass

        # not a dbref; normal search
        obj_restriction = obj and Q(db_obj=obj) or Q()
        timed_restriction = only_timed and Q(db_interval__gt=0) or Q()
        typeclass_restriction = typeclass and Q(db_typeclass_path=typeclass) or Q()
        scripts = self.filter(
            timed_restriction & obj_restriction & typeclass_restriction & Q(db_key__iexact=ostring)
        )
        return scripts

    script_search = search_script

    def copy_script(self, original_script, new_key=None, new_obj=None, new_locks=None):
        """
        Make an identical copy of the original_script.

        Args:
            original_script (Script): The Script to copy.
            new_key (str, optional): Rename the copy.
            new_obj (Object, optional): Place copy on different Object.
            new_locks (str, optional): Give copy different locks from
                the original.

        Returns:
            script_copy (Script): A new Script instance, copied from
        """
        typeclass = original_script.typeclass_path
        new_key = new_key if new_key is not None else original_script.key
        new_obj = new_obj if new_obj is not None else original_script.obj
        new_locks = new_locks if new_locks is not None else original_script.db_lock_storage

        new_script = create.create_script(
            typeclass, key=new_key, obj=new_obj, locks=new_locks, autostart=True
        )
        return new_script

    def create_script(
        self,
        key=None,
        obj=None,
        interval=None,
        start_delay=None,
        repeats=None,
        persistent=None,
        autostart=True,
    ):
        """
        Create a new script. All scripts are a combination of a database
        object that communicates with the database, and an typeclass that
        'decorates' the database object into being different types of
        scripts.  It's behaviour is similar to the game objects except
        scripts has a time component and are more limited in scope.

                #dbref will be set.
            obj (Object): The entity on which this Script sits. If this
                is `None`, we are creating a "global" script.
            account (Account): The account on which this Script sits. It is
                exclusiv to `obj`.
            interval (int): The triggering interval for this Script, in
                seconds. If unset, the Script will not have a timing
                component.
            start_delay (bool): If `True`, will wait `interval` seconds
                before triggering the first time.
            repeats (int): The number of times to trigger before stopping.
                If unset, will repeat indefinitely.
            persistent (bool): If this Script survives a server shutdown
                or not (all Scripts will survive a reload).
            autostart (bool): If this Script will start immediately when
                created or if the `start` method must be called explicitly.
            desc (str): Optional description of script
            tags (list): List of tags or tuples (tag, category).
            attributes (list): List if tuples (key, value) or (key, value, category)
               (key, value, lockstring) or (key, value, lockstring, default_access).

        Returns:
            script (obj): An instance of the script created

        See evennia.scripts.manager for methods to manipulate existing
        scripts in the database.

        """
        global _ObjectDB, _AccountDB
        if not _ObjectDB:
            from evennia.accounts.models import AccountDB as _AccountDB
            from evennia.objects.models import ObjectDB as _ObjectDB

        typeclass = typeclass if typeclass else settings.BASE_SCRIPT_TYPECLASS

        # validate input
        kwarg = {}
        if key:
            kwarg["db_key"] = key
            kwarg["db_account"] = dbid_to_obj(account, _AccountDB)
        if obj:
            kwarg["db_obj"] = dbid_to_obj(obj, _ObjectDB)
        if interval:
            kwarg["db_interval"] = max(0, interval)
        if start_delay:
            kwarg["db_start_delay"] = start_delay
        if repeats:
            kwarg["db_repeats"] = max(0, repeats)
        if persistent:
            kwarg["db_persistent"] = persistent
        if desc:
            kwarg["db_desc"] = desc

        new_script = typeclass(**kwarg)

        new_script._createdict = dict(
            key=key,
            obj=obj,
            account=account,
            start_delay=start_delay,
            repeats=repeats,
            autostart=autostart,
        )
        new_script.save()

        if not new_script.id:
            # this happens in the case of having a repeating script with `repeats=1` and
            # `start_delay=False` - the script will run once and immediately stop before
            # save is over.

        signals.SIGNAL_SCRIPT_POST_CREATE.send(sender=new_script)

class ScriptManager(ScriptDBManager, TypeclassManager):
    pass

"""
Scripts are entities that perform some sort of action, either only
once or repeatedly. They can be directly linked to a particular
Evennia Object or be stand-alonw (in the latter case it is considered
a 'global' script). Scripts can indicate both actions related to the
game world as well as pure behind-the-scenes events and effects.
Everything that has a time component in the game (i.e. is not
hard-coded at startup or directly created/controlled by players) is
handled by Scripts.

Scripts have to check for themselves that they should be applied at a
particular moment of time; this is handled by the is_valid() hook.
Scripts can also implement at_start and at_end hooks for preparing and
cleaning whatever effect they have had on the game object.

Common examples of uses of Scripts:

- Load the default cmdset to the account object's cmdhandler
  when logging in.
- Switch to a different state, such as entering a text editor,
  start combat or enter a dark room.
- Merge a new cmdset with the default one for changing which
  commands are available at a particular time
- Give the account/object a time-limited bonus/effect

"""

from evennia.scripts.manager import ScriptDBManager
from evennia.utils.utils import dbref, to_str

__all__ = ("ScriptDB",)

#
# ScriptDB
#

class ScriptDB(TypedObject):
    """
    The Script database representation.

      key - main name
      name - alias for key
      typeclass_path - the path to the decorating typeclass
      typeclass - auto-linked typeclass
      date_created - time stamp of object creation
      dbref - #id of object
      db - persistent attribute storage
      ndb - non-persistent attribute storage

    The ScriptDB adds the following properties:
      desc - optional description of script
      obj - the object the script is linked to, if any
      account - the account the script is linked to (exclusive with obj)
      interval - how often script should run
      start_delay - if the script should start repeating right away
      repeats - how many times the script should repeat
      persistent - if script should survive a server reboot
      is_active - bool if script is currently running

    """

    #
    # ScriptDB Database Model setup
    #
    # named same as the field, but withtou the db_* prefix.

    # optional description.
    db_desc = models.CharField("desc", max_length=255, blank=True)
    # A reference to the database object affected by this Script, if any.
    db_obj = models.ForeignKey(
        on_delete=models.CASCADE,
        verbose_name="scripted object",
        help_text="the object to store this script on, if not a global script.",
    )
        verbose_name="scripted account",
        help_text="the account to store this script on (should not be set if db_obj is set)",
    )

    # how often to run Script (secs). -1 means there is no timer
    db_interval = models.IntegerField(
        "interval", default=-1, help_text="how often to repeat script, in seconds. <= 0 means off."
    )
    # start script right away or wait interval seconds first
    db_start_delay = models.BooleanField(
        "start delay", default=False, help_text="pause interval seconds before starting."
    )
    # how many times this script is to be repeated, if interval!=0.
    db_repeats = models.IntegerField("number of repeats", default=0, help_text="0 means off.")
    # defines if this script should survive a reboot or not
    db_persistent = models.BooleanField("survive server reboot", default=True)
    # defines if this script has already been started in this session
    db_is_active = models.BooleanField("script active", default=False)

    objects = ScriptDBManager()

    # defaults
    __defaultclasspath__ = "evennia.scripts.scripts.DefaultScript"
    __applabel__ = "scripts"

        verbose_name = "Script"

    #
    #
    # ScriptDB class properties
    #
    #

    # obj property
    def __get_obj(self):
        """
        Property wrapper that homogenizes access to either the
        db_account or db_obj field, using the same object property
        name.

        """
        obj = _GA(self, "db_account")
        if not obj:
            obj = _GA(self, "db_obj")
        return obj

    def __set_obj(self, value):
        """
        Set account or obj to their right database field. If
        a dbref is given, assume ObjectDB.

        """
        try:
            value = _GA(value, "dbobj")
            # deprecated ...
            pass
        if isinstance(value, (str, int)):
            value = to_str(value)
            if value.isdigit() or value.startswith("#"):
                dbid = dbref(value, reqhash=False)
                if dbid:
                    try:
                        value = ObjectDB.objects.get(id=dbid)
                    except ObjectDoesNotExist:
                        # maybe it is just a name that happens to look like a dbid
                        pass
        if value.__class__.__name__ == "AccountDB":
            fname = "db_account"
            _SA(self, fname, value)
        else:
            fname = "db_obj"
        # saving the field
        _GA(self, "save")(update_fields=[fname])

    obj = property(__get_obj, __set_obj)
    object = property(__get_obj, __set_obj)

"""
Monitors - catch changes to model fields and Attributes.

The MONITOR_HANDLER singleton from this module offers the following
functionality:

- Field-monitor - track a object's specific database field and perform
    an action whenever that field *changes* for whatever reason.
- Attribute-monitor tracks an object's specific Attribute and perform
    an action whenever that Attribute *changes* for whatever reason.

"""

from evennia.utils import logger, variable_from_module
from evennia.utils.dbserialize import dbserialize, dbunserialize

class MonitorHandler(object):
    """
    This is a resource singleton that allows for registering
    callbacks for when a field or Attribute is updated (saved).

    """

        """
        Initialize the handler.

        """
        self.savekey = "_monitorhandler_save"
        self.monitors = defaultdict(lambda: defaultdict(dict))

    def save(self):
        """
        Store our monitors to the database. This is called
        by the server process.

        Since dbserialize can't handle defaultdicts, we convert to an
        intermediary save format ((obj,fieldname, idstring, callback, kwargs), ...)

        """
        savedata = []
        if self.monitors:
            for obj in self.monitors:
                for fieldname in self.monitors[obj]:
                    for idstring, (callback, persistent, kwargs) in self.monitors[obj][
                        fieldname
                    ].items():
                        path = "%s.%s" % (callback.__module__, callback.__name__)
                        savedata.append((obj, fieldname, idstring, path, persistent, kwargs))
            savedata = dbserialize(savedata)
            ServerConfig.objects.conf(key=self.savekey, value=savedata)

    def restore(self, server_reload=True):
        """
        Restore our monitors after a reload. This is called

        Args:
            server_reload (bool, optional): If this is False, it means
                the server went through a cold reboot and all
                non-persistent tickers must be killed.

        """
        restored_monitors = ServerConfig.objects.conf(key=self.savekey)
        if restored_monitors:
            restored_monitors = dbunserialize(restored_monitors)
            for obj, fieldname, idstring, path, persistent, kwargs in restored_monitors:
                try:
                    if not server_reload and not persistent:
                        # this monitor will not be restarted
                        continue
                    if "session" in kwargs and not kwargs["session"]:
                        # the session was removed because it no longer
                        # exists. Don't restart the monitor.
                        continue
                    modname, varname = path.rsplit(".", 1)
                    callback = variable_from_module(modname, varname)

                    if obj and hasattr(obj, fieldname):
                        self.monitors[obj][fieldname][idstring] = (callback, persistent, kwargs)
                    continue
        # make sure to clean data from database
        ServerConfig.objects.conf(key=self.savekey, delete=True)

    def _attr_category_fieldname(self, fieldname, category):
        """
        Modify the saved fieldname to make sure to differentiate between Attributes
        with different categories.

        """
        return f"{fieldname}[{category}]" if category else fieldname

    def at_update(self, obj, fieldname):
        """
        Called by the field/attribute as it saves.

        """
        # if this an Attribute with a category we should differentiate
        fieldname = self._attr_category_fieldname(
            fieldname,
            obj.db_category if fieldname == "db_value" and hasattr(obj, "db_category") else None,
        )

        to_delete = []
        if obj in self.monitors and fieldname in self.monitors[obj]:
            for idstring, (callback, persistent, kwargs) in self.monitors[obj][fieldname].items():
                try:
                    callback(obj=obj, fieldname=fieldname, **kwargs)
                    to_delete.append((obj, fieldname, idstring))
                    logger.log_trace("Monitor callback was removed.")
        # we cleanup non-found monitors (has to be done after loop)
        for obj, fieldname, idstring in to_delete:
            del self.monitors[obj][fieldname][idstring]

    def add(self, obj, fieldname, callback, idstring="", persistent=False, category=None, **kwargs):
        """
        Add monitoring to a given field or Attribute. A field must
        be specified with the full db_* name or it will be assumed
        to be an Attribute (so `db_key`, not just `key`).

        Args:
            obj (Typeclassed Entity): The entity on which to monitor a
                field or Attribute.
            fieldname (str): Name of field (db_*) or Attribute to monitor.
            callback (callable): A callable on the form `callable(**kwargs),
                where kwargs holds keys fieldname and obj.
            idstring (str, optional): An id to separate this monitor from other monitors
                of the same field and object.
            persistent (bool, optional): If False, the monitor will survive
                a server reload but not a cold restart. This is default.
            category (str, optional): This is only used if `fieldname` refers to
                an Attribute (i.e. it does not start with `db_`). You must specify this
                if you want to target an Attribute with a category.

            session (Session): If this keyword is given, the monitorhandler will
                correctly analyze it and remove the monitor if after a reload/reboot
                the session is no longer valid.
            any (any): Any other kwargs are passed on to the callback. Remember that
                all kwargs must be possible to pickle!

        """
        if not fieldname.startswith("db_") or not hasattr(obj, fieldname):
            # an Attribute - we track its db_value field
            obj = obj.attributes.get(fieldname, category=category, return_obj=True)
            if not obj:
                return
            fieldname = self._attr_category_fieldname("db_value", category)

        # we try to serialize this data to test it's valid. Otherwise we won't accept it.
        try:
            if not inspect.isfunction(callback):
                raise TypeError("callback is not a function.")
            dbserialize((obj, fieldname, callback, idstring, persistent, kwargs))
            err = "Invalid monitor definition: \n" " (%s, %s, %s, %s, %s, %s)" % (
                obj,
                fieldname,
                callback,
                idstring,
                persistent,
                kwargs,
            )
        else:
            self.monitors[obj][fieldname][idstring] = (callback, persistent, kwargs)

    def remove(self, obj, fieldname, idstring="", category=None):
        """
        Remove a monitor.
        """
            obj = obj.attributes.get(fieldname, return_obj=True)
            if not obj:
                return

        idstring_dict = self.monitors[obj][fieldname]
        if idstring in idstring_dict:

        """
        Delete all monitors.
        """

    def all(self, obj=None):
        """
        List all monitors or all monitors of a given object.

        Args:
            obj (Object): The object on which to list all monitors.

        Returns:
            monitors (list): The handled monitors.

        """
        output = []
        objs = [obj] if obj else self.monitors

            for fieldname in self.monitors[obj]:
                for idstring, (callback, persistent, kwargs) in self.monitors[obj][
                    fieldname
                ].items():
                    output.append((obj, fieldname, idstring, persistent, kwargs))
        return output

# access object
MONITOR_HANDLER = MonitorHandler()

"""
The script handler makes sure to check through all stored scripts to
make sure they are still relevant. A scripthandler is automatically
added to all game objects. You access it through the property
`scripts` on the game object.

"""

class ScriptHandler(object):
    """
    Implements the handler.  This sits on each game object.

    """

        """
        Set up internal state.

        Args:
            obj (Object): A reference to the object this handler is
                attached to.

        """

        """
        List the scripts tied to this object.

        """
        scripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj)
        string = ""
            interval = "inf"
            next_repeat = "inf"
            repeats = "inf"
            if script.interval > 0:
                interval = script.interval
                if script.repeats:
                    repeats = script.repeats
                try:
                    next_repeat = script.time_until_next_repeat()
                    next_repeat = "?"
            string += _("\n '{key}' ({next_repeat}/{interval}, {repeats} repeats): {desc}").format(
                key=script.key,
                next_repeat=next_repeat,
                interval=interval,
                repeats=repeats,
                desc=script.desc,
            )
        return string.strip()

    def add(self, scriptclass, key=None, autostart=True):
        """
        Add a script to this object.

        Args:
            scriptclass (Scriptclass, Script or str): Either a class
                object inheriting from DefaultScript, an instantiated
                script object or a python path to such a class object.
            key (str, optional): Identifier for the script (often set
                in script definition and listings)
            autostart (bool, optional): Start the script upon adding it.

        Returns:
            Script: The newly created Script.

        """
        if self.obj.__dbclass__.__name__ == "AccountDB":
            # we add to an Account, not an Object
            script = create.create_script(
                scriptclass, key=key, account=self.obj, autostart=autostart
            )
        elif isinstance(scriptclass, str) or callable(scriptclass):
            # a str or class to use create before adding to an Object. We wait to autostart
            # so we can differentiate a failing creation from a script that immediately starts/stops.
            script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=False)
        else:
            # already an instantiated class
            script = scriptclass

        if not script:
            logger.log_err(f"Script {scriptclass} failed to be created.")
        if autostart:
            script.start()
        if not script.id:
            # this can happen if the script has repeats=1 or calls stop() in at_repeat.
                f"Script {scriptclass} started and then immediately stopped; "
                "it could probably be a normal function."
            )

    def start(self, key):
        """
        Find scripts and force-start them

        Args:
            key (str): The script's key or dbref.

        Returns:
            nr_started (int): The number of started scripts found.

        """
        scripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key)
        num = 0
            num += 1
        return num

    def has(self, key):
        """
        Determine if a given script exists on this object.

        Args:
            key (str): Search criterion, the script's key or dbref.

        Returns:
            bool: If the script exists or not.

        """
        return ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key).exists()

    def get(self, key):
        """
        Search scripts on this object.

        Args:

        Returns:
            scripts (queryset): The found scripts matching `key`.

        """
        return ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key)

    def remove(self, key=None):
        """
        Forcibly delete a script from this object.

        Args:
            key (str, optional): A script key or the path to a script (in the
                latter case all scripts with this path will be deleted!)
                If no key is given, delete *all* scripts on the object!

        """
        delscripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key)
        if not delscripts:
            delscripts = [
                script
                for script in ScriptDB.objects.get_all_scripts_on_obj(self.obj)
                if script.path == key
            ]
        num = 0
        for script in delscripts:
            num += 1
        return num

    # legacy aliases to remove
    stop = delete

        """
        Get all scripts stored in this handler.

        """
        return ScriptDB.objects.get_all_scripts_on_obj(self.obj)

"""
This module defines Scripts, out-of-character entities that can store
data both on themselves and on other objects while also having the
ability to run timers.

"""

from twisted.internet.defer import Deferred, maybeDeferred
from twisted.internet.task import LoopingCall

from evennia.scripts.manager import ScriptManager

__all__ = ["DefaultScript", "DoNothing", "Store"]

class ExtendedLoopingCall(LoopingCall):
    """
    Custom child of LoopingCall that can start at a delay different than
    `self.interval` and self.count=0. This allows it to support pausing
    by resuming at a later period.

    """

    start_delay = None
    callcount = 0

    def start(self, interval, now=True, start_delay=None, count_start=0):
        """
        Start running function every interval seconds.

        This overloads the LoopingCall default by offering the
        start_delay keyword and ability to repeat.

        Args:
            interval (int): Repeat interval in seconds.
            now (bool, optional): Whether to start immediately or after
                `start_delay` seconds.
            start_delay (int, optional): This only applies is `now=False`. It gives
                number of seconds to wait before starting. If `None`, use
                `interval` as this value instead. Internally, this is used as a
                way to start with a variable start time after a pause.
            count_start (int): Number of repeats to start at. The  count
                goes up every time the system repeats. This is used to
                implement something repeating `N` number of times etc.

        Raises:
            AssertError: if trying to start a task which is already running.
            ValueError: If interval is set to an invalid value < 0.

        Notes:
            As opposed to Twisted's inbuilt count mechanism, this
            system will count also if force_repeat() was called rather
            than just the number of `interval` seconds since the start.
            This allows us to force-step through a limited number of
            steps if we want.

        """
        assert not self.running, "Tried to start an already running ExtendedLoopingCall."
        if interval < 0:
            raise ValueError("interval must be >= 0")
        self.running = True
        deferred = self._deferred = Deferred()
        self.starttime = self.clock.seconds()
        self.interval = interval
        self._runAtStart = now
        self.callcount = max(0, count_start)
        self.start_delay = start_delay if start_delay is None else max(0, start_delay)

        if now:
            # run immediately
            self()
        elif start_delay is not None and start_delay >= 0:
            # start after some time: for this to work we need to
            # trick _scheduleFrom by temporarily setting a different
            # self.interval for it to check.
            real_interval, self.interval = self.interval, start_delay
            self._scheduleFrom(self.starttime)
            # re-set the actual interval (this will be picked up
            # next time it runs
            self.interval = real_interval
        else:
        return deferred

    def __call__(self):
        """
        Tick one step. We update callcount (tracks number of calls) as
        well as null start_delay (needed in order to correctly
        estimate next_call_time at all times).

        """
        self.callcount += 1
        if self.start_delay:
            self.start_delay = None
            self.starttime = self.clock.seconds()
        if self._deferred:
            LoopingCall.__call__(self)

    def force_repeat(self):
        """
        Force-fire the callback

        Raises:
            AssertionError: When trying to force a task that is not
                running.

        """
        assert self.running, "Tried to fire an ExtendedLoopingCall that was not running."
        self.call.cancel()
        self.call = None
        self()

    def next_call_time(self):
        """
        Get the next call time. This also takes the eventual effect
        of start_delay into account.

        Returns:
            int or None: The time in seconds until the next call. This
                takes `start_delay` into account. Returns `None` if
                the task is not running.

        """
        if self.running and self.interval > 0:
            total_runtime = self.clock.seconds() - self.starttime
            interval = self.start_delay or self.interval
            return max(0, interval - (total_runtime % self.interval))

class ScriptBase(ScriptDB, metaclass=TypeclassBase):
    """
    Base class for scripts. Don't inherit from this, inherit from the
    class `DefaultScript` below instead.

    This handles the timer-component of the Script.

    """

    objects = ScriptManager()

        return "<{cls} {key}>".format(cls=self.__class__.__name__, key=self.key)

    def at_idmapper_flush(self):
        """
        If we're flushing this object, make sure the LoopingCall is gone too.
        """
        ret = super().at_idmapper_flush()
        if ret and self.ndb._task:
            self.ndb._pause_task(auto_pause=True)
        # TODO - restart anew ?
        return ret

    def _start_task(
        self,
        force_restart=False,
        auto_unpause=False,
        **kwargs,
    ):
        """
        Start/Unpause task runner, optionally with new values. If given, this will
        update the Script's fields.

            interval (int): How often to tick the task, in seconds. If this is <= 0,
                no task will start and properties will not be updated on the Script.
            start_delay (int): If the start should be delayed.
            repeats (int): How many repeats. 0 for infinite repeats.
            force_restart (bool): If set, always create a new task running even if an
                old one already was running. Otherwise this will only happen if
                new script properties were passed.
            auto_unpause (bool): This is an automatic unpaused (used e.g by Evennia after
                a reload) and should not un-pause manually paused Script timers.
        Note:
            If setting the `start-delay` of a *paused* Script, the Script will
            restart exactly after that new start-delay, ignoring the time it
            was paused at. If only changing the `interval`, the Script will
            come out of pause comparing the time it spent in the *old* interval
            with the *new* interval in order to determine when next to fire.

        Examples:
            - Script previously had an interval of 10s and was paused 5s into that interval.
              Script is now restarted with a 20s interval. It will next fire after 15s.
            - Same Script is restarted with a 3s interval. It will fire immediately.

        """
        if self.pk is None:
            # script object already deleted from db - don't start a new timer
            raise ScriptDB.DoesNotExist

        # handle setting/updating fields
        update_fields = []
        old_interval = self.db_interval
        if interval is not None:
            self.db_interval = interval
            update_fields.append("db_interval")
        if start_delay is not None:
            # note that for historical reasons, the start_delay is a boolean field, not an int; the
            # actual value is only used with the task.
            self.db_start_delay = bool(start_delay)
            update_fields.append("db_start_delay")
        if repeats is not None:
            self.db_repeats = repeats
            update_fields.append("db_repeats")

        # validate interval
        if self.db_interval and self.db_interval > 0:
            if not self.is_active:
                self.db_is_active = True
                update_fields.append("db_is_active")
        else:
            # no point in starting a task with no interval.
            return

        restart = bool(update_fields) or force_restart
        self.save(update_fields=update_fields)

        if self.ndb._task and self.ndb._task.running:
            if restart:
                # a change needed/forced; stop/remove old task
                self._stop_task()
            else:
                # task alreaady running and no changes needed
                return

        if not self.ndb._task:
            # we should have a fresh task after this point
            self.ndb._task = ExtendedLoopingCall(self._step_task)

        self._unpause_task(
            auto_unpause=auto_unpause,
            old_interval=old_interval,
        )

        if not self.ndb._task.running:
            # if not unpausing started it, start script anew with the new values
            self.ndb._task.start(
                self.db_interval, now=not self.db_start_delay, start_delay=start_delay
            )

        self.at_start(**kwargs)

    def _pause_task(self, auto_pause=False, **kwargs):
        """
        Pause task where it is, saving the current status.

        Args:
            auto_pause (str):

        """
        if not self.db._paused_time:
            # only allow pause if not already paused
            task = self.ndb._task
            if task:
                self.db._paused_time = task.next_call_time()
                self.db._paused_callcount = task.callcount
                self.db._manually_paused = not auto_pause
                if task.running:
                    task.stop()
            self.ndb._task = None

            self.at_pause(auto_pause=auto_pause, **kwargs)

    def _unpause_task(
        self, interval=None, start_delay=None, auto_unpause=False, old_interval=0, **kwargs
    ):
        """
        Unpause task from paused status. This is used for auto-paused tasks, such
        as tasks paused on a server reload.

        Args:
            interval (int): How often to tick the task, in seconds.
            auto_unpause (bool): If set, this will only unpause scripts that were unpaused
                automatically (useful during a system reload/shutdown).
            old_interval (int): The old Script interval (or current one if nothing changed). Used
                to recalculate the unpause startup interval.

        """
        paused_time = self.db._paused_time
        if paused_time:
            if auto_unpause and self.db._manually_paused:
                # this was manually paused.
                return

            # task was paused. This will use the new values as needed.
            callcount = self.db._paused_callcount or 0
            if start_delay is None and interval is not None:
                # adjust start-delay based on how far we were into previous interval
                start_delay = max(0, interval - (old_interval - paused_time))
            else:
                start_delay = paused_time

            if not self.ndb._task:
                self.ndb._task = ExtendedLoopingCall(self._step_task)

                self.db_interval, now=False, start_delay=start_delay, count_start=callcount
            )
            self.db._paused_time = None
            self.db._paused_callcount = None
            self.db._manually_paused = None

            self.at_start(**kwargs)

    def _stop_task(self, **kwargs):
        """
        Stop task runner and delete the task.

        """
        task_stopped = False
        task = self.ndb._task
        if task and task.running:
            task.stop()
            task_stopped = True

        self.ndb._task = None
        self.db_is_active = False

        # make sure this is not confused as a paused script
        self.db._paused_time = None
        self.db._paused_callcount = None
        self.db._manually_paused = None

        self.save(update_fields=["db_is_active"])
        if task_stopped:
            self.at_stop(**kwargs)

    def _step_errback(self, e):
        """
        Callback for runner errors

        """
        cname = self.__class__.__name__
        estring = _(
            "Script {key}(#{dbid}) of type '{name}': at_repeat() error '{err}'.".format(
                key=self.key, dbid=self.dbid, name=cname, err=e.getErrorMessage()
            )
        )
        try:
            self.db_obj.msg(estring)
            # we must not crash inside the errback, even if db_obj is None.
            pass
        logger.log_err(estring)

    def _step_callback(self):
        """
        Step task runner. No try..except needed due to defer wrap.

        """
            # if there is no task, we have no business using this method
            return

        if not self.is_valid():
            self.stop()
            return

        # call hook
        try:
            self.at_repeat()
            raise

        # check repeats
        if self.ndb._task:
            # we need to check for the task in case stop() was called
            # inside at_repeat() and it already went away.
            callcount = self.ndb._task.callcount
            maxcount = self.db_repeats
            if maxcount > 0 and maxcount <= callcount:
                self.stop()

    def _step_task(self):
        """
        Step task. This groups error handling.
        """
        try:
            return maybeDeferred(self._step_callback).addErrback(self._step_errback)

    # Access methods / hooks

    def at_first_save(self, **kwargs):
        """
        This is called after very first time this object is saved.
        Generally, you don't need to overload this, but only the hooks
        called by this method.

        Args:

        """
        self.at_script_creation()

            # this will only be set if the utils.create_script
            # function was used to create the object. We want
            # the create call's kwargs to override the values
            # set by hooks.
            elif self.db_key != cdict["key"]:
            if cdict.get("interval") and self.interval != cdict["interval"]:
                self.db_interval = max(0, cdict["interval"])
                updates.append("db_interval")
            if cdict.get("start_delay") and self.start_delay != cdict["start_delay"]:
                self.db_start_delay = cdict["start_delay"]
                updates.append("db_start_delay")
            if cdict.get("repeats") and self.repeats != cdict["repeats"]:
                self.db_repeats = max(0, cdict["repeats"])
                updates.append("db_repeats")
            if cdict.get("persistent") and self.persistent != cdict["persistent"]:
                self.db_persistent = cdict["persistent"]
                updates.append("db_persistent")
            if cdict.get("desc") and self.desc != cdict["desc"]:
                self.db_desc = cdict["desc"]
                updates.append("db_desc")

            if cdict.get("autostart"):
                # autostart the script
                self._start_task(force_restart=True)

        """
        Delete the Script. Normally stops any timer task. This fires at_script_delete before
        deletion.

        Returns:
            bool: If deletion was successful or not. Only time this can fail would be if
                the script was already previously deleted, or `at_script_delete` returns
                False.

        """
        if not self.pk or not self.at_script_delete():

        self._stop_task()

        """
        Changes fundamental aspects of the type. Usually changes are made in at_script creation
        instead.

        """
        pass

        """
        Called when the Script is cached in the idmapper. This is usually more reliable
        than overriding `__init__` since the latter can be called at unexpected times.

        """
        pass

        """
        Should be overridden in child.

        """
        pass

    def at_script_delete(self):
        """
        Called when script is deleted, before the script timer stops.

        Returns:
            bool: If False, deletion is aborted.

        """

        """
        If returning False, `at_repeat` will not be called and timer will stop
        updating.
        """

    def at_repeat(self, **kwargs):
        """
        Called repeatedly every `interval` seconds, once `.start()` has
        been called on the Script at least once.

        Args:

        """
        pass

    def at_start(self, **kwargs):
        pass

    def at_pause(self, **kwargs):
        pass

    def at_stop(self, **kwargs):
        pass

    def start(self, interval=None, start_delay=None, repeats=None, **kwargs):
        """
        Start/Unpause timer component, optionally with new values. If given,
        this will update the Script's fields. This will start `at_repeat` being
        called every `interval` seconds.

            interval (int): How often to fire `at_repeat` in seconds.
            start_delay (int): If the start of ticking should be delayed and by how much.
            **kwargs: Optional (default unused) kwargs passed on into the `at_start` hook.

        Notes:

        Examples:

        """
        self._start_task(interval=interval, start_delay=start_delay, repeats=repeats, **kwargs)

    update = start

    def stop(self, **kwargs):
        """
        Stop the Script's timer component. This will not delete the Sctipt,
        just stop the regular firing of `at_repeat`. Running `.start()` will
        start the timer anew, optionally with new settings..

        Args:
            **kwargs: Optional (default unused) kwargs passed on into the `at_stop` hook.

        """
        self._stop_task(**kwargs)

    def pause(self, **kwargs):
        """
        Manually the Script's timer component manually.

        Args:
            **kwargs: Optional (default unused) kwargs passed on into the `at_pause` hook.

        """
        self._pause_task(manual_pause=True, **kwargs)

    def unpause(self, **kwargs):
        """
        Manually unpause a Paused Script.

        Args:

        """
        self._unpause_task(**kwargs)

    def time_until_next_repeat(self):
        """
        Get time until the script fires it `at_repeat` hook again.

        Returns:
            int or None: Time in seconds until the script runs again.
                If not a timed script, return `None`.

        Notes:
            This hook is not used in any way by the script's stepping
            system; it's only here for the user to be able to check in
            on their scripts and when they will next be run.

        """
        if task:
            try:
                return int(round(task.next_call_time()))
                pass

    def remaining_repeats(self):
        """
        Get the number of returning repeats for limited Scripts.

        Returns:
            int or None: The number of repeats remaining until the Script
                stops. Returns `None` if it has unlimited repeats.

        """
        if task:
            return max(0, self.db_repeats - task.callcount)

    def reset_callcount(self, value=0):
        """
        Reset the count of the number of calls done.

        Args:
            value (int, optional): The repeat value to reset to. Default
                is to set it all the way back to 0.

        Notes:
            This is only useful if repeats != 0.

        """
        if task:
            task.callcount = max(0, int(value))

        """
        Fire a premature triggering of the script callback. This
        will reset the timer and count down repeats as if the script
        had fired normally.
        """
        if task:
            task.force_repeat()

class DefaultScript(ScriptBase):
    """
    This is the base TypeClass for all Scripts. Scripts describe
    events, timers and states in game, they can have a time component
    or describe a state that changes under certain conditions.

    """

    def create(cls, key, **kwargs):
        """
        Provides a passthrough interface to the utils.create_script() function.

        Args:

        Returns:

        """
        errors = []
        obj = None

        try:
            obj = create.create_script(**kwargs)
            errors.append("The script '%s' encountered errors and could not be created." % key)

        """
        Only called once, when script is first created.

        """
        pass

        """
        Is called to check if the script's timer is valid to run at this time.
        Should return a boolean. If False, the timer will be stopped.

        """

        """
        Called whenever the script timer is started, which for persistent
        timed scripts is at least once every server start. It will also be
        called when starting again after a pause (including after a
        server reload).

        Args:

        """
        pass

        """
        Called repeatedly if this Script is set to repeat regularly.

        Args:

        """
        pass

    def at_pause(self, manual_pause=True, **kwargs):
        """
        Called when this script's timer pauses.

        Args:
            manual_pause (bool): If set, pausing was done by a direct call. The
                non-manual pause indicates the script was paused as part of
                the server reload.

        """
        pass

        """
        Called whenever when it's time for this script's timer to stop (either
        because is_valid returned False, it ran out of iterations or it was manuallys
        stopped.

        Args:

        """
        pass

        """
        Called when the Script is deleted, before stopping the timer.

        Returns:
            bool: If False, the deletion is aborted.

        """

        """
        to do it.
        """
        pass

        """
        """
        pass

        """
        This hook is called after the server has started. It can be used to add
        post-startup setup for Scripts without a timer component (for which at_start
        could be used).

        """
        pass

# Some useful default Script types used by Evennia.

class DoNothing(DefaultScript):
    """
    A script that does nothing. Used as default fallback.
    """

        """
        Setup the script
        """
        self.key = "sys_do_nothing"
        self.desc = "This is an empty placeholder script."

class Store(DefaultScript):
    """
    Simple storage script
    """

        """
        """
        self.key = "sys_storage"
        self.desc = "This is a generic storage container."

"""
Module containing the task handler for Evennia deferred tasks, persistent or not.
"""

from pickle import PickleError

from twisted.internet.defer import CancelledError as DefCancelledError

from evennia.utils.logger import log_err

def handle_error(*args, **kwargs):
    """Handle errors within deferred objects."""
    for arg in args:
        # suppress cancel errors
        if arg.type == DefCancelledError:
            continue
        raise arg

class TaskHandlerTask:
    """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

    """

    def __init__(self, task_id):
        self.task_id = task_id
        self.deferred = TASK_HANDLER.get_deferred(task_id)

    def get_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.

        """
        return TASK_HANDLER.get_deferred(self.task_id)

    def pause(self):
        """
        Pause the callback of a task.
        To resume use `TaskHandlerTask.unpause`.

        """
        d = self.deferred
        if d:
            d.pause()

    def unpause(self):
        """
        Unpause a task, run the task if it has passed delay time.

        """
        if d:
            d.unpause()

    @property
        """
        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.

        """
        if d:
            return d.paused
        else:

    def do_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.

        """
        return TASK_HANDLER.do_task(self.task_id)

    def call(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:

        """
        return TASK_HANDLER.call_task(self.task_id)

    def remove(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.

        """
        return TASK_HANDLER.remove(self.task_id)

    def cancel(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.

        """
        return TASK_HANDLER.cancel(self.task_id)

    def active(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.

        """
        return TASK_HANDLER.active(self.task_id)

    @property
    def called(self):
        """
        A task attribute to check if the deferred instance of a task has been called.

        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.

        """
        if d:
            return d.called
        else:

    def exists(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.

        """
        return TASK_HANDLER.exists(self.task_id)

    def get_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.

        """
        return self.task_id

class TaskHandler(object):

    """A light singleton wrapper allowing to access permanent tasks.

    When `utils.delay` is called, the task handler is used to create
    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.

    """

        self.tasks = {}
        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

        """Load from the ServerConfig.

        This should be automatically called when Evennia starts.
        It populates `self.tasks` according to the ServerConfig.

        """
        to_save = False
        value = ServerConfig.objects.conf("delayed_tasks", default={})
        if isinstance(value, str):
            tasks = dbunserialize(value)
        else:
            tasks = value

        # At this point, `tasks` contains a dictionary of still-serialized tasks
        for task_id, value in tasks.items():
            date, callback, args, kwargs = dbunserialize(value)
            if isinstance(callback, tuple):
                # `callback` can be an object and name for instance methods
                obj, method = callback
                if obj is None:
                    to_save = True
                    continue

                callback = getattr(obj, method)
            self.tasks[task_id] = (date, callback, args, kwargs, True, None)

        if self.stale_timeout > 0:  # cleanup stale tasks.
            self.clean_stale_tasks()
        if to_save:
            self.save()

    def clean_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 = []
        for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items():
            if not self.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._now if self._now else datetime.now()
                # the task was canceled more than stale_timeout seconds ago
                if now > stale_date:
                    clean_ids.append(task_id)
        for task_id in clean_ids:
            self.remove(task_id)

        """
        Save the tasks in ServerConfig.

        """

            if task_id in self.to_save:
                continue
            if not persistent:
                continue

            safe_callback = callback
            if getattr(callback, "__self__", None):
                # `callback` is an instance method
                obj = callback.__self__
                name = callback.__name__
                safe_callback = (obj, name)

            # Check if callback can be pickled. args and kwargs have been checked
            try:
                dbserialize(safe_callback)
            except (TypeError, AttributeError, PickleError) as err:
                    "the specified callback {callback} cannot be pickled. "
                    "It must be a top-level function in a module or an "
                    "instance method ({err}).".format(callback=callback, err=err)
                )

            self.to_save[task_id] = dbserialize((date, safe_callback, args, kwargs))

        ServerConfig.objects.conf("delayed_tasks", self.to_save)

    def add(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
        delta = timedelta(seconds=timedelay)
        comp_time = now + delta
        # get an open task id
        used_ids = list(self.tasks.keys())
        task_id = 1

        # record the task to the tasks dictionary
        persistent = kwargs.get("persistent", False)
        if "persistent" in kwargs:
            del kwargs["persistent"]
            safe_args = []
            safe_kwargs = {}

            # Check that args and kwargs contain picklable information
            for arg in args:
                try:
                    dbserialize(arg)
                except (TypeError, AttributeError, PickleError):
                    log_err(
                        "The positional argument {} cannot be "
                        "pickled and will not be present in the arguments "
                        "fed to the callback {}".format(arg, callback)
                    )
                else:
                    safe_args.append(arg)

            for key, value in kwargs.items():
                try:
                    dbserialize(value)
                    log_err(
                        "The {} keyword argument {} cannot be "
                        "fed to the callback {}".format(key, value, callback)
                    )
                else:
                    safe_kwargs[key] = value

            self.tasks[task_id] = (comp_time, callback, safe_args, safe_kwargs, persistent, None)
        else:  # this is a non-persitent task
            self.tasks[task_id] = (comp_time, callback, args, kwargs, persistent, None)

        # defer the task
        callback = self.do_task
        args = [task_id]
        kwargs = {}
        d = deferLater(self.clock, timedelay, callback, *args, **kwargs)
        d.addErrback(handle_error)

        # some tasks may complete before the deferred can be added
        if task_id in self.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
        if self.stale_timeout > 0:
        return TaskHandlerTask(task_id)

    def exists(self, task_id):
        """

        Args:

        Returns:

        """
        else:

    def active(self, task_id):
        """
        Check if a task is active (has not been called yet).

        Args:

        Returns:

        """
            # if the task has not been run, cancel it
            deferred = self.get_deferred(task_id)
            return not (deferred and deferred.called)
        else:

    def cancel(self, task_id):
        """
        Stop a task from automatically executing.

        Args:

        Returns:

        """
            d = self.get_deferred(task_id)
            if d:  # it is remotely possible for a task to not have a deferred
                if d.called:
                else:  # the callback has not been called yet.
                    d.cancel()
            else:  # this task has no deferred instance
        else:

    def remove(self, task_id):
        """
        Remove a task without executing it.

        Args:

        Returns:

        """
        d = None
        # delete the task from the tasks dictionary
            self.cancel(task_id)
            del self.tasks[task_id]  # delete the task from the tasks dictionary
        # remove the task from the persistent dictionary and ServerConfig
        if task_id in self.to_save:
            del self.to_save[task_id]
            self.save()  # remove from ServerConfig.objects
        # delete the instance of the deferred
        if d:
            del d

    def clear(self, save=True, cancel=True):
        """
        Clear all tasks. By default tasks are canceled and removed from the database as well.

        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.

        """
        if self.tasks:
            for task_id in self.tasks.keys():
                if cancel:
                    self.cancel(task_id)
            self.tasks = {}
        if self.to_save:
            self.to_save = {}
        if save:

    def call_task(self, task_id):
        """

        Args:

        Returns:

        """
            date, callback, args, kwargs, persistent, d = self.tasks.get(task_id)
        else:  # the task does not exist
        return callback(*args, **kwargs)

    def do_task(self, task_id):
        """
        If calling before timedelay cancel the deferred instance affliated to this task.
        callback.

        Args:
            task_id (int): a valid task ID.

        Returns:

        """
        callback_return = False
        if d:  # it is remotely possible for a task to not have a deferred
            if not d.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
        callback_return = callback(*args, **kwargs)
        self.remove(task_id)
        return callback_return

    def get_deferred(self, task_id):
        """
        Return the instance of the deferred the task id is using.

        Args:

        Returns:

        """
            return self.tasks[task_id][5]
        else:

    def create_delays(self):
        """
        Create the delayed tasks for the persistent tasks.
        This method should be automatically called when Evennia starts.

        """
        for task_id, (date, callback, args, kwargs, _, _) in self.tasks.items():
            self.tasks[task_id] = date, callback, args, kwargs, True, None
            seconds = max(0, (date - now).total_seconds())
            d = deferLater(self.clock, seconds, self.do_task, task_id)
            d.addErrback(handle_error)
            # some tasks may complete before the deferred can be added
            if self.tasks.get(task_id, False):
                self.tasks[task_id] = date, callback, args, kwargs, True, d

# Create the soft singleton
TASK_HANDLER = TaskHandler()

"""
TickerHandler

This implements an efficient Ticker which uses a subscription
model to 'tick' subscribed objects at regular intervals.

The ticker mechanism is used by importing and accessing
the instantiated TICKER_HANDLER instance in this module. This
instance is run by the server; it will save its status across
server reloads and be started automaticall on boot.

Example:

```python
    from evennia.scripts.tickerhandler import TICKER_HANDLER

    # call tick myobj.at_tick(*args, **kwargs) every 15 seconds
    TICKER_HANDLER.add(15, myobj.at_tick, *args, **kwargs)
```

You supply the interval to tick and a callable to call regularly with
any extra args/kwargs. The callable should either be a stand-alone
function in a module *or* the method on a *typeclassed* entity (that
is, on an object that can be safely and stably returned from the
database).  Functions that are dynamically created or sits on
in-memory objects cannot be used by the tickerhandler (there is no way
to reference them safely across reboots and saves).

The handler will transparently set
up and add new timers behind the scenes to tick at given intervals,
using a TickerPool - all callables with the same interval will share
the interval ticker.

To remove:

```python
    TICKER_HANDLER.remove(15, myobj.at_tick)
```

Both interval and callable must be given since a single object can be subscribed
to many different tickers at the same time. You can also supply `idstring`
as an identifying string if you ever want to tick the callable at the same interval
but with different arguments (args/kwargs are not used for identifying the ticker). There
is also `persistent=False` if you don't want to make a ticker that don't survive a reload.
If either or both `idstring` or `persistent` has been changed from their defaults, they
must be supplied to the `TICKER_HANDLER.remove` call to properly identify the ticker
to remove.

The TickerHandler's functionality can be overloaded by modifying the
Ticker class and then changing TickerPool and TickerHandler to use the
custom classes

```python
class MyTicker(Ticker):
    # [doing custom stuff]

class MyTickerPool(TickerPool):
    ticker_class = MyTicker
class MyTickerHandler(TickerHandler):
    ticker_pool_class = MyTickerPool
```

If one wants to duplicate TICKER_HANDLER's auto-saving feature in
a  custom handler one can make a custom `AT_STARTSTOP_MODULE` entry to
call the handler's `save()` and `restore()` methods when the server reboots.

"""

from evennia.scripts.scripts import ExtendedLoopingCall
from evennia.utils import inherits_from, variable_from_module
from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj

_ERROR_ADD_TICKER = """TickerHandler: Tried to add an invalid ticker:
{store_key}
Ticker was not added."""

_ERROR_ADD_TICKER_SUB_SECOND = """You are trying to add a ticker running faster
than once per second. This is not supported and also probably not useful:
Spamming messages to the user faster than once per second serves no purpose in
a text-game, and if you want to update some property, consider doing so
on-demand rather than using a ticker.
"""

class Ticker(object):
    """
    Represents a repeatedly running task that calls
    hooks repeatedly. Overload `_callback` to change the
    way it operates.
    """

    def _callback(self):
        """
        This will be called repeatedly every `self.interval` seconds.
        `self.subscriptions` contain tuples of (obj, args, kwargs) for
        each subscribing object.

        If overloading, this callback is expected to handle all
        subscriptions when it is triggered. It should not return
        anything and should not traceback on poorly designed hooks.
        The callback should ideally work under @inlineCallbacks so it
        can yield appropriately.

        The _hook_key, which is passed down through the handler via
        kwargs is used here to identify which hook method to call.

        """
        self._to_add = []
        self._to_remove = []
        self._is_ticking = True
        for store_key, (args, kwargs) in self.subscriptions.items():
            callback = yield kwargs.pop("_callback", "at_tick")
            obj = yield kwargs.pop("_obj", None)
            try:
                if callable(callback):
                    # call directly
                    yield callback(*args, **kwargs)
                    continue
                # try object method
                if not obj or not obj.pk:
                    # object was deleted between calls
                    self._to_remove.append(store_key)
                    continue
                else:
                    yield _GA(obj, callback)(*args, **kwargs)
                log_trace("Removing ticker.")
                self._to_remove.append(store_key)
                log_trace()
            finally:
                # make sure to re-store
                kwargs["_callback"] = callback
                kwargs["_obj"] = obj
        # cleanup - we do this here to avoid changing the subscription dict while it loops
        self._is_ticking = False
        for store_key in self._to_remove:
            self.remove(store_key)
        for store_key, (args, kwargs) in self._to_add:
            self.add(store_key, *args, **kwargs)

    def __init__(self, interval):
        """
        Set up the ticker

        Args:
            interval (int): The stepping interval.

        """
        self.subscriptions = {}
        # set up a twisted asynchronous repeat call
        self.task = ExtendedLoopingCall(self._callback)

    def validate(self, start_delay=None):
        """
        Start/stop the task depending on how many subscribers we have
        using it.

        Args:
            start_delay (int, optional): Time to way before starting.

        """
        subs = self.subscriptions
        if self.task.running:
            if not subs:
                self.task.stop()
        elif subs:
            self.task.start(self.interval, now=False, start_delay=start_delay)

    def add(self, store_key, *args, **kwargs):
        """
        Sign up a subscriber to this ticker.
        Args:
            store_key (str): Unique storage hash for this ticker subscription.
            args (any, optional): Arguments to call the hook method with.

            _start_delay (int): If set, this will be
                used to delay the start of the trigger instead of
                `interval`.

        """
        if self._is_ticking:
            # protects the subscription dict from
            # updating while it is looping
            self._to_add.append((store_key, (args, kwargs)))
        else:
            start_delay = kwargs.pop("_start_delay", None)
            self.subscriptions[store_key] = (args, kwargs)
            self.validate(start_delay=start_delay)

    def remove(self, store_key):
        """
        Unsubscribe object from this ticker

        Args:
            store_key (str): Unique store key.

        """
            # this protects the subscription dict from
            self._to_remove.append(store_key)
        else:
            self.subscriptions.pop(store_key, False)
            self.validate()

    def stop(self):
        """
        Kill the Task, regardless of subscriptions.

        """
        self.validate()

class TickerPool(object):
    """
    This maintains a pool of
    `evennia.scripts.scripts.ExtendedLoopingCall` tasks for calling
    subscribed objects at given times.

    """

    ticker_class = Ticker

        """
        Initialize the pool.

        """
        self.tickers = {}

        """
        Add new ticker subscriber.

        Args:
            store_key (str): Unique storage hash.
            args (any, optional): Arguments to send to the hook method.

        """
        _, _, _, interval, _, _ = store_key
        if not interval:
            log_err(_ERROR_ADD_TICKER.format(store_key=store_key))
            return

        if interval not in self.tickers:
            self.tickers[interval] = self.ticker_class(interval)
        self.tickers[interval].add(store_key, *args, **kwargs)

        """
        Remove subscription from pool.

        Args:
            store_key (str): Unique storage hash to remove

        """
        if interval in self.tickers:
            self.tickers[interval].remove(store_key)
            if not self.tickers[interval]:
                del self.tickers[interval]

    def stop(self, interval=None):
        """
        Stop all scripts in pool. This is done at server reload since
        restoring the pool will automatically re-populate the pool.

        Args:
            interval (int, optional): Only stop tickers with this interval.

        """
        if interval and interval in self.tickers:
            self.tickers[interval].stop()
        else:
            for ticker in self.tickers.values():
                ticker.stop()

class TickerHandler(object):
    """
    The Tickerhandler maintains a pool of tasks for subscribing
    objects to various tick rates.  The pool maintains creation
    instructions and and re-applies them at a server restart.

    """

    ticker_pool_class = TickerPool

    def __init__(self, save_name="ticker_storage"):
        """
        Initialize handler

        save_name (str, optional): The name of the ServerConfig
            instance to store the handler state persistently.

        """
        self.ticker_storage = {}
        self.save_name = save_name
        self.ticker_pool = self.ticker_pool_class()

    def _get_callback(self, callback):
        """
        Analyze callback and determine its consituents

        Args:
            callback (function or method): This is either a stand-alone
                function or class method on a typeclassed entitye (that is,
                an entity that can be saved to the database).
        Returns:
            ret (tuple): This is a tuple of the form `(obj, path, callfunc)`,
                where `obj` is the database object the callback is defined on
                if it's a method (otherwise `None`) and vice-versa, `path` is
                the python-path to the stand-alone function (`None` if a method).
                The `callfunc` is either the name of the method to call or the
                callable function object itself.
        Raises:
            TypeError: If the callback is of an unsupported type.

        """
        outobj, outpath, outcallfunc = None, None, None
        if callable(callback):
            if inspect.ismethod(callback):
                outobj = callback.__self__
                outcallfunc = callback.__func__.__name__
            elif inspect.isfunction(callback):
                outpath = "%s.%s" % (callback.__module__, callback.__name__)
                outcallfunc = callback
            else:
                raise TypeError(f"{callback} is not a method or function.")
        else:
            raise TypeError(f"{callback} is not a callable function or method.")

        if outobj and not inherits_from(outobj, "evennia.typeclasses.models.TypedObject"):
                f"{callback} is a method on a normal object - it must "
                "be either a method on a typeclass, or a stand-alone function."
            )

        return outobj, outpath, outcallfunc

    def _store_key(self, obj, path, interval, callfunc, idstring="", persistent=True):
        """
        Tries to create a store_key for the object.

        Args:
            obj (Object, tuple or None): Subscribing object if any. If a tuple, this is
                a packed_obj tuple from dbserialize.
            path (str or None): Python-path to callable, if any.
            interval (int): Ticker interval. Floats will be converted to
                nearest lower integer value.
            callfunc (callable or str): This is either the callable function or
                the name of the method to call. Note that the callable is never
                stored in the key; that is uniquely identified with the python-path.
            idstring (str, optional): Additional separator between
                different subscription types.
            persistent (bool, optional): If this ticker should survive a system
                shutdown or not.

        Returns:
            store_key (tuple): A tuple `(packed_obj, methodname, outpath, interval,
                idstring, persistent)` that uniquely identifies the
                ticker. Here, `packed_obj` is the unique string representation of the
                object or `None`. The `methodname` is the string name of the method on
                `packed_obj` to call, or `None` if `packed_obj` is unset. `path` is
                the Python-path to a non-method callable, or `None`. Finally, `interval`
                `idstring` and `persistent` are integers, strings and bools respectively.

        """
        if interval < 1:
            raise RuntimeError(_ERROR_ADD_TICKER_SUB_SECOND)

        interval = int(interval)
        persistent = bool(persistent)
        packed_obj = pack_dbobj(obj)
        methodname = callfunc if callfunc and isinstance(callfunc, str) else None
        outpath = path if path and isinstance(path, str) else None
        return (packed_obj, methodname, outpath, interval, idstring, persistent)

        """
        Save ticker_storage as a serialized string into a temporary
        ServerConf field. Whereas saving is done on the fly, if called
        by server when it shuts down, the current timer of each ticker
        will be saved so it can start over from that point.

        """
        if self.ticker_storage:
            # get the current times so the tickers can be restarted with a delay later
            start_delays = dict(
                (interval, ticker.task.next_call_time())
                for interval, ticker in self.ticker_pool.tickers.items()
            )

            to_save = {}

            # remove any subscription that lost its object and update the timers for the tickers
            for store_key, (args, kwargs) in self.ticker_storage.items():
                # unpack the store_key to reference its parts
                packedobj, callfunc, path, interval, idstring, persistent = store_key
                # verify that there's a valid obj+method or function path
                if (
                    callfunc
                    and ("_obj" in kwargs and kwargs["_obj"].pk)
                    and hasattr(kwargs["_obj"], callfunc)
                ) or path:
                    # this is a mutable, so it's updated in-place in ticker_storage
                    kwargs["_start_delay"] = start_delays.get(interval, None)
                    to_save[store_key] = (args, kwargs)
            ServerConfig.objects.conf(key=self.save_name, value=dbserialize(to_save))
        else:
            # make sure we have nothing lingering in the database
            ServerConfig.objects.conf(key=self.save_name, delete=True)

        """
        Restore ticker_storage from database and re-initialize the
        handler from storage. This is triggered by the server at
        restart.

        Args:

        """
        # load stored command instructions and use them to re-initialize handler
        restored_tickers = ServerConfig.objects.conf(key=self.save_name)
        if restored_tickers:
            # the dbunserialize will convert all serialized dbobjs to real objects

            restored_tickers = dbunserialize(restored_tickers)
            self.ticker_storage = {}
            for store_key, (args, kwargs) in restored_tickers.items():
                try:
                    # at this point obj is the actual object (or None) due to how
                    # the dbunserialize works
                    obj, callfunc, path, interval, idstring, persistent = store_key
                    if not persistent and not server_reload:
                        # this ticker will not be restarted
                        continue
                    if isinstance(callfunc, str) and not obj:
                        # methods must have an existing object
                        continue
                    # we must rebuild the store_key here since obj must not be
                    # stored as the object itself for the store_key to be hashable.
                    store_key = self._store_key(obj, path, interval, callfunc, idstring, persistent)

                    if obj and callfunc:
                        kwargs["_callback"] = callfunc
                        kwargs["_obj"] = obj
                    elif path:
                        modname, varname = path.rsplit(".", 1)
                        callback = variable_from_module(modname, varname)
                        kwargs["_callback"] = callback
                        kwargs["_obj"] = None
                    else:
                        # Neither object nor path - discard this ticker
                        log_err("Tickerhandler: Removing malformed ticker: %s" % str(store_key))
                        continue
                    # this suggests a malformed save or missing objects
                    log_trace("Tickerhandler: Removing malformed ticker: %s" % str(store_key))
                    continue
                # if we get here we should create a new ticker
                self.ticker_storage[store_key] = (args, kwargs)
                self.ticker_pool.add(store_key, *args, **kwargs)

    def add(self, interval=60, callback=None, idstring="", persistent=True, *args, **kwargs):
        """
        Add subscription to tickerhandler

        Args:

            *args: Will be passed into the callback every time it's called. This must be
                data possible to pickle.

            interval (int): Interval in seconds between calling
                `callable(*args, **kwargs)`
            callable (callable function or method): This
                should either be a stand-alone function or a method on a
                typeclassed entity (that is, one that can be saved to the
                database).
            idstring (str): Identifier for separating
                this ticker-subscription from others with the same
                interval. Allows for managing multiple calls with
                the same time interval and callback.
            persistent (bool): A ticker will always survive
                a server reload. If this is unset, the ticker will be
                deleted by a server shutdown.
            **kwargs Will be passed into the callback every time it is called.
                This must be data possible to pickle.

        Returns:
            store_key (tuple): The immutable store-key for this ticker. This can
                be stored and passed into `.remove(store_key=store_key)` later to
                easily stop this ticker later.

        Notes:
            The callback will be identified by type and stored either as
            as combination of serialized database object + methodname or
            as a python-path to the module + funcname. These strings will
            be combined iwth `interval` and `idstring` to define a
            unique storage key for saving. These must thus all be supplied
            when wanting to modify/remove the ticker later.

        """
        obj, path, callfunc = self._get_callback(callback)
        store_key = self._store_key(obj, path, interval, callfunc, idstring, persistent)
        kwargs["_obj"] = obj
        kwargs["_callback"] = callfunc  # either method-name or callable
        self.ticker_storage[store_key] = (args, kwargs)
        self.ticker_pool.add(store_key, *args, **kwargs)
        return store_key

    def remove(self, interval=60, callback=None, idstring="", persistent=True, store_key=None):
        """
        Remove ticker subscription from handler.

            interval (int): Interval of ticker to remove.
            callback (callable function or method): Either a function or
                the method of a typeclassed object.
            idstring (str): Identifier id of ticker to remove.
            persistent (bool): Whether this ticker is persistent or not.
            store_key (str): If given, all other kwargs are ignored and only
                this is used to identify the ticker.

        Raises:
            KeyError: If no matching ticker was found to remove.

        Notes:
            The store-key is normally built from the interval/callback/idstring/persistent values;
            but if the `store_key` is explicitly given, this is used instead.

        """
        if isinstance(callback, int):
                "TICKER_HANDLER.remove has changed: "
                "the interval is now the first argument, callback the second."
            )
        if not store_key:
            obj, path, callfunc = self._get_callback(callback)
            store_key = self._store_key(obj, path, interval, callfunc, idstring, persistent)
        to_remove = self.ticker_storage.pop(store_key, None)
        if to_remove:
            self.ticker_pool.remove(store_key)
        else:
            raise KeyError(f"No Ticker was found matching the store-key {store_key}.")

    def clear(self, interval=None):
        """
        Stop/remove tickers from handler.

        Args:

        Notes:
            This is the only supported way to kill tickers related to
            non-db objects.

        """
        self.ticker_pool.stop(interval)
            self.ticker_storage = dict(
                (store_key, store_value)
                for store_key, store_value in self.ticker_storage.items()
                if store_key[3] != interval
            )
        else:

    def all(self, interval=None):
        """
        Get all ticker subscriptions.

        Args:
            interval (int, optional): Limit match to tickers with this interval.

        Returns:
            list or dict: If `interval` was given, this is a list of tickers using that interval.
            If `interval` was *not* given, this is a dict
            `{interval1: [ticker1, ticker2, ...],  ...}`

        """
        if interval is None:
            # return dict of all, ordered by interval
            return dict(
                (interval, ticker.subscriptions)
            )
        else:
            # get individual interval
            ticker = self.ticker_pool.tickers.get(interval, None)
            if ticker:
                return {interval: ticker.subscriptions}

    def all_display(self):
        """
        Get all tickers on an easily displayable form.

        Returns:
            tickers (dict): A list of all storekeys

        """
        store_keys = []
        for ticker in self.ticker_pool.tickers.values():
            for (
                (objtup, callfunc, path, interval, idstring, persistent),
                (args, kwargs),
            ) in ticker.subscriptions.items():
                store_keys.append(
                    (kwargs.get("_obj", None), callfunc, path, interval, idstring, persistent)
                )
        return store_keys

# main tickerhandler
TICKER_HANDLER = TickerHandler()

"""
The Evennia Server service acts as an AMP-client when talking to the
Portal. This module sets up the Client-side communication.

"""

import os

from twisted.internet import protocol

from evennia.server.portal import amp

class AMPClientFactory(protocol.ReconnectingClientFactory):
    """
    This factory creates an instance of an AMP client connection. This handles communication from
    the be the Evennia 'Server' service to the 'Portal'. The client will try to auto-reconnect on a
    connection error.

    """

    # Initial reconnect delay in seconds.
    initialDelay = 1
    factor = 1.5
    maxDelay = 1

    def __init__(self, server):
        """
        Initializes the client factory.

        Args:
            server (server): server instance.

        """
        self.server = server
        self.protocol = class_from_module(settings.AMP_CLIENT_PROTOCOL_CLASS)
        self.maxDelay = 10
        # not really used unless connecting to multiple servers, but
        # avoids having to check for its existence on the protocol
        self.broadcasts = []

    def startedConnecting(self, connector):
        """
        Called when starting to try to connect to the Portal AMP server.

        Args:
            connector (Connector): Twisted Connector instance representing
                this connection.

        """
        pass

    def buildProtocol(self, addr):
        """
        Creates an AMPProtocol instance when connecting to the AMP server.

        Args:
            addr (str): Connection address. Not used.

        """
        self.resetDelay()
        self.server.amp_protocol = AMPServerClientProtocol()
        self.server.amp_protocol.factory = self
        return self.server.amp_protocol

    def clientConnectionLost(self, connector, reason):
        """
        Called when the AMP connection to the MUD server is lost.

        Args:
            reason (str): Eventual text describing why connection was lost.

        """
        logger.log_info("Server disconnected from the portal.")
        protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)

    def clientConnectionFailed(self, connector, reason):
        """
        Called when an AMP connection attempt to the MUD server fails.

        Args:
            reason (str): Eventual text describing why connection failed.

        """
        logger.log_msg("Attempting to reconnect to Portal ...")
        protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)

class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
    """
    This protocol describes the Server service (acting as an AMP-client)'s communication with the
    Portal (which acts as the AMP-server)

    """

    # sending AMP data

    def connectionMade(self):
        """
        Called when a new connection is established.

        """
        # print("AMPClient new connection {}".format(self))
        info_dict = self.factory.server.get_info_dict()
        super().connectionMade()
        # first thing we do is to request the Portal to sync all sessions
        # back with the Server side. We also need the startup mode (reload, reset, shutdown)
        self.send_AdminServer2Portal(
            amp.DUMMYSESSION, operation=amp.PSYNC, spid=os.getpid(), info_dict=info_dict
        )
        # run the intial setup if needed
        self.factory.server.run_initial_setup()

    def data_to_portal(self, command, sessid, **kwargs):
        """
        Send data across the wire to the Portal

        Args:
            command (AMP Command): A protocol send command.
            sessid (int): A unique Session id.
            kwargs (any): Any data to pickle into the command.

        Returns:
            deferred (deferred or None): A deferred with an errback.

        Notes:
            Data will be sent across the wire pickled as a tuple
            (sessid, kwargs).

        """
        # print("server data_to_portal: {}, {}, {}".format(command, sessid, kwargs))
        return self.callRemote(command, packed_data=amp.dumps((sessid, kwargs))).addErrback(
            self.errback, command.key
        )

    def send_MsgServer2Portal(self, session, **kwargs):
        """
        Access method - executed on the Server for sending data
            to Portal.

        Args:
            session (Session): Unique Session.
            kwargs (any, optiona): Extra data.

        """
        return self.data_to_portal(amp.MsgServer2Portal, session.sessid, **kwargs)

    def send_AdminServer2Portal(self, session, operation="", **kwargs):
        """
        Administrative access method called by the Server to send an
        instruction to the Portal.

        Args:
            session (Session): Session.
            operation (char, optional): Identifier for the server
                operation, as defined by the global variables in
                `evennia/server/amp.py`.
            kwargs (dict, optional): Data going into the adminstrative.

        """
        return self.data_to_portal(
            amp.AdminServer2Portal, session.sessid, operation=operation, **kwargs
        )

    # receiving AMP data

    @amp.MsgStatus.responder
    def server_receive_status(self, question):
        return {"status": "OK"}

    @amp.MsgPortal2Server.responder
    @amp.catch_traceback
    def server_receive_msgportal2server(self, packed_data):
        """
        Receives message arriving to server. This method is executed
        on the Server.

        Args:
            packed_data (str): Data to receive (a pickled tuple (sessid,kwargs))

        """
        sessid, kwargs = self.data_in(packed_data)
        session = evennia.SERVER_SESSION_HANDLER.get(sessid, None)
            evennia.SERVER_SESSION_HANDLER.data_in(session, **kwargs)
        return {}

    @amp.AdminPortal2Server.responder
    def server_receive_adminportal2server(self, packed_data):
        """
        Receives admin data from the Portal (allows the portal to
        perform admin operations on the server). This is executed on
        the Server.

        Args:
            packed_data (str): Incoming, pickled data.

        """
        operation = kwargs.pop("operation", "")

        if operation == amp.PCONN:  # portal_session_connect
            # create a new session and sync it
            evennia.SERVER_SESSION_HANDLER.portal_connect(kwargs.get("sessiondata"))

        elif operation == amp.PCONNSYNC:  # portal_session_sync
            evennia.SERVER_SESSION_HANDLER.portal_session_sync(kwargs.get("sessiondata"))

        elif operation == amp.PDISCONN:  # portal_session_disconnect
            # session closed from portal sid
            session = evennia.SERVER_SESSION_HANDLER.get(sessid)
                evennia.SERVER_SESSION_HANDLER.portal_disconnect(session)

        elif operation == amp.PDISCONNALL:  # portal_disconnect_all
            # portal orders all sessions to close
            evennia.SERVER_SESSION_HANDLER.portal_disconnect_all()

        elif operation == amp.PSYNC:  # portal_session_sync
            # force a resync of sessions from the portal side. This happens on
            # first server-connect.
            server_restart_mode = kwargs.get("server_restart_mode", "shutdown")
            evennia.EVENNIA_SERVER_SERVICE.run_init_hooks(server_restart_mode)
            evennia.SERVER_SESSION_HANDLER.portal_sessions_sync(kwargs.get("sessiondata"))
            evennia.SERVER_SESSION_HANDLER.portal_start_time = kwargs.get("portal_start_time")

        elif operation == amp.SRELOAD:  # server reload
            # shut down in reload mode
            evennia.SERVER_SESSION_HANDLER.all_sessions_portal_sync()
            evennia.EVENNIA_SERVER_SERVICE.shutdown(mode="reload")

        elif operation == amp.SRESET:
            # shut down in reset mode
            evennia.EVENNIA_SERVER_SERVICE.shutdown(mode="reset")

        elif operation == amp.SSHUTD:  # server shutdown
            # shutdown in stop mode
            evennia.EVENNIA_SERVER_SERVICE.shutdown(mode="shutdown")

        else:
            raise Exception("operation %(op)s not recognized." % {"op": operation})

        return {}

"""
The AMP (Asynchronous Message Protocol)-communication commands and constants used by Evennia.

This module acts as a central place for AMP-servers and -clients to get commands to use.

"""

import zlib  # Used in Compressed class
from collections import defaultdict, namedtuple
from io import BytesIO
from itertools import count

from twisted.internet.defer import Deferred, DeferredList
from twisted.protocols import amp

_LOGGER = None

# communication bits
# (chr(9) and chr(10) are \t and \n, so skipping them)

PCONN = chr(1)  # portal session connect
PDISCONN = chr(2)  # portal session disconnect
PSYNC = chr(3)  # portal session sync
SLOGIN = chr(4)  # server session login
SDISCONN = chr(5)  # server session disconnect
SDISCONNALL = chr(6)  # server session disconnect all
SSHUTD = chr(7)  # server shutdown
SSYNC = chr(8)  # server session sync
SCONN = chr(11)  # server creating new connection (for irc bots and etc)
PCONNSYNC = chr(12)  # portal post-syncing a session
PDISCONNALL = chr(13)  # portal session disconnect all
SRELOAD = chr(14)  # server shutdown in reload mode
SSTART = chr(15)  # server start (portal must already be running anyway)
PSHUTD = chr(16)  # portal (+server) shutdown
SSHUTD = chr(17)  # server shutdown
PSTATUS = chr(18)  # ping server or portal status
SRESET = chr(19)  # server shutdown in reset mode

NUL = b"\x00"
NULNUL = b"\x00\x00"

AMP_MAXLEN = amp.MAX_VALUE_LENGTH  # max allowed data length in AMP protocol (cannot be changed)

# amp internal
ASK = b"_ask"
ANSWER = b"_answer"
ERROR = b"_error"
ERROR_CODE = b"_error_code"
ERROR_DESCRIPTION = b"_error_description"
UNKNOWN_ERROR_CODE = b"UNKNOWN"

# buffers
_SENDBATCH = defaultdict(list)
_MSGBUFFER = defaultdict(list)

# resources

DUMMYSESSION = namedtuple("DummySession", ["sessid"])(0)

_HTTP_WARNING = bytes(
    """
HTTP/1.1 200 OK
Content-Type: text/html

<html>
  <body>
    This is Evennia's internal AMP port. It handles communication
    between Evennia's different processes.
    <p>
        <h3>This port should NOT be publicly visible.</h3>
    </p>
  </body>
</html>""".strip(),
    "utf-8",
)

# Helper functions for pickling.

def dumps(data):
    return pickle.dumps(data, pickle.HIGHEST_PROTOCOL)

def loads(data):
    return pickle.loads(data)

def _get_logger():
    """
    Delay import of logger until absolutely necessary

    """
    global _LOGGER
    if not _LOGGER:
        from evennia.utils import logger as _LOGGER
    return _LOGGER

@wraps
def catch_traceback(func):
    """
    Helper decorator

    """

        try:
            func(*args, **kwargs)
            _get_logger().log_trace()
            raise  # make sure the error is visible on the other side of the connection too

# AMP Communication Command types

class Compressed(amp.String):
    """
    This is a custom AMP command Argument that both handles too-long
    sends as well as uses zlib for compression across the wire. The
    batch-grouping of too-long sends is borrowed from the "mediumbox"
    recipy at twisted-hacks's ~glyph/+junk/amphacks/mediumbox.

    """

    def fromBox(self, name, strings, objects, proto):
        """
        Converts from box string representation to python. We read back too-long batched data and
        put it back together here.

        """

        value = BytesIO()
        value.write(self.fromStringProto(strings.get(name), proto))
        for counter in count(2):
            # count from 2 upwards
            chunk = strings.get(b"%s.%d" % (name, counter))
            if chunk is None:
                break
            value.write(self.fromStringProto(chunk, proto))
        objects[str(name, "utf-8")] = value.getvalue()

    def toBox(self, name, strings, objects, proto):
        """
        Convert from python object to string box representation.
        we break up too-long data snippets into multiple batches here.

        """

        # print("toBox: name={}, strings={}, objects={}, proto{}".format(name, strings, objects, proto))

        value = BytesIO(objects[str(name, "utf-8")])
        strings[name] = self.toStringProto(value.read(AMP_MAXLEN), proto)

        # print("toBox strings[name] = {}".format(strings[name]))

            chunk = value.read(AMP_MAXLEN)
            if not chunk:
                break
            strings[b"%s.%d" % (name, counter)] = self.toStringProto(chunk, proto)

    def toString(self, inObject):
        """
        Convert to send as a bytestring on the wire, with compression.

        Note: In Py3 this is really a byte stream.

        """
        return zlib.compress(super().toString(inObject), 9)

    def fromString(self, inString):
        """
        Convert (decompress) from the string-representation on the wire to Python.

        """
        return super().fromString(zlib.decompress(inString))

class MsgLauncher2Portal(amp.Command):
    """
    Message Launcher -> Portal

    """

    key = "MsgLauncher2Portal"
    arguments = [(b"operation", amp.String()), (b"arguments", amp.String())]
    errors = {Exception: b"EXCEPTION"}
    response = []

class MsgPortal2Server(amp.Command):
    """
    Message Portal -> Server

    """

    key = b"MsgPortal2Server"
    arguments = [(b"packed_data", Compressed())]

class MsgServer2Portal(amp.Command):
    """
    Message Server -> Portal

    """

    key = "MsgServer2Portal"

class AdminPortal2Server(amp.Command):
    """
    Administration Portal -> Server

    Sent when the portal needs to perform admin operations on the
    server, such as when a new session connects or resyncs

    """

    key = "AdminPortal2Server"

class AdminServer2Portal(amp.Command):
    """
    Administration Server -> Portal

    Sent when the server needs to perform admin operations on the
    portal.

    """

    key = "AdminServer2Portal"

class MsgStatus(amp.Command):
    """
    Check Status between AMP services

    """

    key = "MsgStatus"
    arguments = [(b"status", amp.String())]
    response = [(b"status", amp.String())]

class FunctionCall(amp.Command):
    """
    Bidirectional Server <-> Portal

    Sent when either process needs to call an arbitrary function in
    the other. This does not use the batch-send functionality.

    """

    key = "FunctionCall"
    arguments = [
        (b"module", amp.String()),
        (b"function", amp.String()),
        (b"args", amp.String()),
        (b"kwargs", amp.String()),
    ]
    response = [(b"result", amp.String())]

# Core AMP protocol for communication Server <-> Portal

class AMPMultiConnectionProtocol(amp.AMP):
    """
    AMP protocol that safely handle multiple connections to the same
    server without dropping old ones - new clients will receive
    all server returns (broadcast). Will also correctly handle
    erroneous HTTP requests on the port and return a HTTP error response.

    """

    # helper methods

        """
        Initialize protocol with some things that need to be in place
        already before connecting both on portal and server.

        """
        self.send_batch_counter = 0
        self.send_reset_time = time.time()
        self.send_mode = True
        self.send_task = None
        self.multibatches = 0
        # later twisted amp has its own __init__

    def _commandReceived(self, box):
        """
        This overrides the default Twisted AMP error handling which is not
        passing enough of the traceback through to the other side. Instead we
        add a specific log of the problem on the erroring side.

        """

        def formatAnswer(answerBox):
            answerBox[ANSWER] = box[ASK]
            return answerBox

        def formatError(error):
            if error.check(amp.RemoteAmpError):
                code = error.value.errorCode
                desc = error.value.description

                # Evennia extra logging
                desc += " (error logged on other side)"
                _get_logger().log_err(f"AMP caught exception ({desc}):\n{error.value}")

                if isinstance(desc, str):
                    desc = desc.encode("utf-8", "replace")
                if error.value.fatal:
                    errorBox = amp.QuitBox()
                else:
                    errorBox = amp.AmpBox()
            else:
                errorBox = amp.QuitBox()
                _get_logger().log_err(error)  # server-side logging if unhandled error
                code = UNKNOWN_ERROR_CODE
                desc = b"Unknown Error"
            errorBox[ERROR] = box[ASK]
            errorBox[ERROR_DESCRIPTION] = desc
            errorBox[ERROR_CODE] = code
            return errorBox

        deferred = self.dispatchCommand(box)
        if ASK in box:
            deferred.addCallbacks(formatAnswer, formatError)
            deferred.addCallback(self._safeEmit)
        deferred.addErrback(self.unhandledError)

    def stringReceived(self, string):
        """
        Overrides the base stringReceived of twisted in order to handle
        the strange error reported in https://github.com/evennia/evennia/issues/2053,
        which can lead to the amp connection locking up.

        Args:
            string (str): the data coming in.

        Notes:

            To test, add the following code to the beginning of
            `evennia.server.amp_client.AMPServerClientProtocol.data_to_portal`, then
            run multiple commands until the error trigger:
            ::

                import random
                from twisted.protocols.amp import AmpBox
                always_fail = False
                if always_fail or random.random() < 0.05:
                    breaker = AmpBox()
                    breaker['_answer'.encode()]='13541'.encode()
                    self.transport.write(breaker.serialize())

        """
        try:
            pto = "proto_" + self.state
            statehandler = getattr(self, pto)
            log.msg("callback", self.state, "not found")
        else:
            try:
                # make sure to catch a KeyError cleanly here
                self.state = statehandler(string)
                if self.state == "done":
                    self.transport.loseConnection()
            except KeyError as err:
                _get_logger().log_err(
                    f"AMP error (KeyError: {err}). Discarded data (see "
                    "https://github.com/evennia/evennia/issues/2053)"
                )

        """
        Handle non-AMP messages, such as HTTP communication.

        """
        # print("dataReceived: {}".format(data))
        if data[:1] == NUL:
            # an AMP communication
            if data[-2:] != NULNUL:
                # an incomplete AMP box means more batches are forthcoming.
                self.multibatches += 1
            try:
                super().dataReceived(data)
                _get_logger().log_trace(
                    "Discarded incoming partial (packed) data (len {})".format(len(data))
                )
        elif self.multibatches:
            # invalid AMP, but we have a pending multi-batch that is not yet complete
            if data[-2:] == NULNUL:
                # end of existing multibatch
                self.multibatches = max(0, self.multibatches - 1)
            try:
                    "Discarded incoming multi-batch (packed) data (len {})".format(len(data))
                )
        else:
            # not an AMP communication, return warning
            self.transport.write(_HTTP_WARNING)
            self.transport.loseConnection()
            print("HTTP received (the AMP port should not receive http, only AMP!) %s" % data)

    def makeConnection(self, transport):
        """
        Swallow connection log message here. Copied from original
        in the amp protocol.

        """
        # copied from original, removing the log message
        if not self._ampInitialized:
            amp.AMP.__init__(self)
        self._transportPeer = transport.getPeer()
        self._transportHost = transport.getHost()
        amp.BinaryBoxProtocol.makeConnection(self, transport)

        """
        This is called when an AMP connection is (re-)established. AMP calls it on both sides.

        """
        # print("connectionMade: {}".format(self))
        self.factory.broadcasts.append(self)

    def connectionLost(self, reason):
        """
        We swallow connection errors here. The reason is that during a
        normal reload/shutdown there will almost always be cases where
        either the portal or server shuts down before a message has
        returned its (empty) return, triggering a connectionLost error
        that is irrelevant. If a true connection error happens, the
        portal will continuously try to reconnect, showing the problem
        that way.

        """
        # print("ConnectionLost: {}: {}".format(self, reason))
        try:
            self.factory.broadcasts.remove(self)
            pass

    # Error handling

    def errback(self, err, info):
        """
        Error callback.
        Handles errors to avoid dropping connections on server tracebacks.

        Args:
            err (Failure): Deferred error instance.
            info (str): Error string.

        """
        err.trap(Exception)
        _get_logger().log_err(
            "AMP Error from {info}: {trcbck} {err}".format(
                info=info, trcbck=err.getTraceback(), err=err.getErrorMessage()
            )
        )

    def data_in(self, packed_data):
        """
        Process incoming packed data.

        Args:
            packed_data (bytes): Pickled data.
        Returns:
            unpaced_data (any): Unpickled package

        """
        msg = loads(packed_data)
        return msg

    def broadcast(self, command, sessid, **kwargs):
        """
        Send data across the wire to all connections.

        Args:

        Returns:

        Notes:

        """
        deferreds = []
        # print("broadcast: {} {}: {}".format(command, sessid, kwargs))

        for protcl in self.factory.broadcasts:
            deferreds.append(
                protcl.callRemote(command, **kwargs).addErrback(self.errback, command.key)
            )

        return DeferredList(deferreds)

    # generic function send/recvs

    def send_FunctionCall(self, modulepath, functionname, *args, **kwargs):
        """
        Access method called by either process. This will call an arbitrary
        function on the other process (On Portal if calling from Server and
        vice versa).

        Inputs:
            modulepath (str) - python path to module holding function to call
            functionname (str) - name of function in given module
            *args, **kwargs will be used as arguments/keyword args for the
                            remote function call
        Returns:
            A deferred that fires with the return value of the remote
            function call

        """
        return (
            self.callRemote(
                FunctionCall,
                module=modulepath,
                function=functionname,
                args=dumps(args),
                kwargs=dumps(kwargs),
            )
            .addCallback(lambda r: loads(r["result"]))
            .addErrback(self.errback, "FunctionCall")
        )

    @FunctionCall.responder
    @catch_traceback
    def receive_functioncall(self, module, function, func_args, func_kwargs):
        """
        This allows Portal- and Server-process to call an arbitrary
        function in the other process. It is intended for use by
        plugin modules.

        Args:
            module (str or module): The module containing the
                `function` to call.
            function (str): The name of the function to call in
                `module`.
            func_args (str): Pickled args tuple for use in `function` call.
            func_kwargs (str): Pickled kwargs dict for use in `function` call.

        """
        args = loads(func_args)
        kwargs = loads(func_kwargs)

        # call the function (don't catch tracebacks here)
        result = variable_from_module(module, function)(*args, **kwargs)

        if isinstance(result, Deferred):
            # if result is a deferred, attach handler to properly
            # wrap the return value
            result.addCallback(lambda r: {"result": dumps(r)})
        else:
            return {"result": dumps(result)}

"""
Link Evennia to external resources (wizard plugin for evennia_launcher)

"""
import pprint
import sys

from evennia.utils.utils import list_to_string, mod_import

class ConnectionWizard(object):
        self.data = {}
        self.prev_node = None

    def display(self, text):
        "Show text"
        print(text)

    def ask_continue(self):
        "'Press return to continue'-prompt"
        input(" (Press return to continue)")

    def ask_node(self, options, prompt="Enter choice: ", default=None):
        """
        Retrieve options and jump to different menu nodes

        Args:
            options (dict): Node options on the form {key: (desc, callback), }
            prompt (str, optional): Question to ask
            default (str, optional): Default value to use if user hits return.

        """

        opt_txt = "\n".join(f" {key}: {desc}" for key, (desc, _, _) in options.items())
        self.display(opt_txt + "\n")

            resp = input(prompt).strip()

            if not resp:
                if default:
                    resp = str(default)

            if resp.lower() in options:
                # self.display(f" Selected '{resp}'.")
                desc, callback, kwargs = options[resp.lower()]
                callback(self, **kwargs)
            elif resp.lower() in ("quit", "q"):
                sys.exit()
            elif resp:
                # input, but nothing was recognized
                self.display(" Choose one of: {}".format(list_to_string(list(options))))

    def ask_yesno(self, prompt, default="yes"):
        """
        Ask a yes/no question inline.

            prompt (str): The prompt to ask.
            default (str): "yes" or "no", used if pressing return.
        Returns:
            reply (str): Either 'yes' or 'no'.

        """
        prompt = prompt + (" [Y]/N? " if default == "yes" else " Y/[N]? ")

            resp = input(prompt).lstrip().lower()
                resp = default.lower()
            if resp in ("yes", "y"):
                self.display(" Answered Yes.")
                return "yes"
            elif resp in ("no", "n"):
                self.display(" Answered No.")
                return "no"

    def ask_choice(self, prompt=" > ", options=None, default=None):
        """
        Ask multiple-choice question, get response inline.

            prompt (str): Input prompt.
            options (list): List of options. Will be indexable by sequence number 1...
            default (int): The list index+1 of the default choice, if any
        Returns:
            reply (str): The answered reply.

        """
        opt_txt = "\n".join(f" {ind + 1}: {desc}" for ind, desc in enumerate(options))

                    return options[int(default)]
            if resp.lower() in ("quit", "q"):
            if resp.isdigit():
                resp = int(resp) - 1
                if 0 <= resp < len(options):
                    selection = options[resp]
                    self.display(f" Selected '{selection}'.")
                    return selection
            self.display(" Select one of the given options.")

    def ask_input(self, prompt=" > ", default=None, validator=None):
        """
        Get arbitrary input inline.

            prompt (str): The display prompt.
            default (str): If empty input, use this.
            validator (callable): If given, the input will be passed
                into this callable. It should return True unless validation
                fails (and is expected to echo why if so).

        Returns:
            inp (str): The input given, or default.

        """

            if not resp and default:
                resp = str(default)

            if resp.lower() in ("q", "quit"):

            if resp.lower() == "none":
                resp = ""

                if validator and not validator(resp):
                    continue

                ok = input("\n Leave blank? [Y]/N: ")
                if ok.lower() in ("n", "no"):
                    continue
                elif ok.lower() in ("q", "quit"):
                    sys.exit()
                return resp

            if validator and not validator(resp):
                continue

            self.display(resp)
            ok = input("\n Is this correct? [Y]/N: ")
            if ok.lower() in ("n", "no"):
                continue
            elif ok.lower() in ("q", "quit"):
            return resp

def node_start(wizard):
    text = """
    This wizard helps to attach your Evennia server to external networks. It
    will save to a file `server/conf/connection_settings.py` that will be
    imported from the bottom of your game settings file. Once generated you can
    also modify that file directly.

    Make sure you have at least started the game once before continuing!

    Use `quit` at any time to abort and throw away unsaved changes.
    """
    options = {
        "1": (
            "Add your game to the Evennia game index (also for closed-dev games)",
            node_game_index_start,
            {},
        ),
        "2": ("MSSP setup (for mud-list crawlers)", node_mssp_start, {}),
        # "3": ("Add Grapevine listing",
        #       node_grapevine_start, {}),
        # "4": ("Add IRC link",
        #      "node_irc_start", {}),
        # "5" ("Add RSS feed",
        #       "node_rss_start", {}),
        "s": ("View and (optionally) Save created settings", node_view_and_apply_settings, {}),
        "q": ("Quit", lambda *args: sys.exit(), {}),
    }

    wizard.display(text)
    wizard.ask_node(options)

# Evennia game index

def node_game_index_start(wizard, **kwargs):
    text = """
    The Evennia game index (http://games.evennia.com) lists both active Evennia
    games as well as games in various stages of development.

    You can put up your game in the index also if you are not (yet) open for
    players.  If so, put 'None' for the connection details - you are just telling
    us that you are out there, making us excited about your upcoming game!

    Please check the listing online first to see that your exact game name is
    not colliding with an existing game-name in the list (be nice!).
    """

    if wizard.ask_yesno("Continue adding/editing an Index entry?") == "yes":
        node_game_index_fields(wizard)
    else:
        node_start(wizard)

def node_game_index_fields(wizard, status=None):
    # reset the listing if needed
    if not hasattr(wizard, "game_index_listing"):
        wizard.game_index_listing = settings.GAME_INDEX_LISTING

    # game status

    status_default = wizard.game_index_listing["game_status"]
    text = f"""
    What is the status of your game?
        - pre-alpha: a game in its very early stages, mostly unfinished or unstarted
        - alpha: a working concept, probably lots of bugs and incomplete features
        - beta: a working game, but expect bugs and changing features
        - launched: a full, working game (that may still be expanded upon and improved later)

    Current value (return to keep):
    {status_default}
    """

    options = ["pre-alpha", "alpha", "beta", "launched"]

    wizard.game_index_listing["game_status"] = wizard.ask_choice("Select one: ", options)

    # game name

    name_default = settings.SERVERNAME
    text = f"""
    Your game's name should usually be the same as `settings.SERVERNAME`, but
    you can set it to something else here if you want.

    Current value:
    {name_default}
    """

    def name_validator(inp):
        tmax = 80
        tlen = len(inp)
        if tlen > tmax:
            print(f"The name must be shorter than {tmax} characters (was {tlen}).")
            wizard.ask_continue()

    wizard.game_index_listing["game_name"] = wizard.ask_input(
        default=name_default, validator=name_validator
    )

    # short desc

    sdesc_default = wizard.game_index_listing.get("short_description", None)

    text = f"""
    Enter a short description of your game. Make it snappy and interesting!
    This should be at most one or two sentences (255 characters) to display by
    '{settings.SERVERNAME}' in the main game list. Line breaks will be ignored.

    {sdesc_default}
    """

    def sdesc_validator(inp):
        tmax = 255
            print(f"The short desc must be shorter than {tmax} characters (was {tlen}).")

    wizard.game_index_listing["short_description"] = wizard.ask_input(
        default=sdesc_default, validator=sdesc_validator
    )

    # long desc

    long_default = wizard.game_index_listing.get("long_description", None)

    text = f"""
    Enter a longer, full-length description. This will be shown when clicking
    on your game's listing. You can use \\n to create line breaks and may use
    Markdown formatting like *bold*, _italic_, [linkname](http://link) etc.

    {long_default}
    """

    wizard.game_index_listing["long_description"] = wizard.ask_input(default=long_default)

    # listing contact

    listing_default = wizard.game_index_listing.get("listing_contact", None)
    text = f"""
    Enter a listing email-contact. This will not be visible in the listing, but
    allows us to get in touch with you should there be some listing issue (like
    a name collision) or some bug with the listing (us actually using this is
    likely to be somewhere between super-rarely and never).

    {listing_default}
    """

    def contact_validator(inp):
        if not inp or "@" not in inp:
            print("This should be an email and cannot be blank.")

    wizard.game_index_listing["listing_contact"] = wizard.ask_input(
        default=listing_default, validator=contact_validator
    )

    # telnet hostname

    hostname_default = wizard.game_index_listing.get("telnet_hostname", None)
    text = f"""
    Enter the hostname to which third-party telnet mud clients can connect to
    your game. This would be the name of the server your game is hosted on,
    like `coolgame.games.com`, or `mygreatgame.se`.

    Write 'None' if you are not offering public telnet connections at this time.

    {hostname_default}
    """

    wizard.game_index_listing["telnet_hostname"] = wizard.ask_input(default=hostname_default)

    # telnet port

    port_default = wizard.game_index_listing.get("telnet_port", None)
    text = f"""
    Enter the main telnet port. The Evennia default is 4000. You can change
    this with the TELNET_PORTS server setting.

    {port_default}
    """

    wizard.game_index_listing["telnet_port"] = wizard.ask_input(default=port_default)

    # website

    website_default = wizard.game_index_listing.get("game_website", None)
    text = f"""
    Evennia is its own web server and runs your game's website. Enter the
    URL of the website here, like http://yourwebsite.com, here.

    Write 'None' if you are not offering a publicly visible website at this time.

    {website_default}
    """

    wizard.game_index_listing["game_website"] = wizard.ask_input(default=website_default)

    # webclient

    webclient_default = wizard.game_index_listing.get("web_client_url", None)
    text = f"""
    Evennia offers its own native webclient. Normally it will be found from the
    game homepage at something like http://yourwebsite.com/webclient. Enter
    your specific URL here (when clicking this link you should launch into the
    web client)

    Write 'None' if you don't want to list a publicly accessible webclient.

    {webclient_default}
    """

    wizard.game_index_listing["web_client_url"] = wizard.ask_input(default=webclient_default)

    if not (
        wizard.game_index_listing.get("web_client_url")
        or (wizard.game_index_listing.get("telnet_host"))
    ):
        wizard.display(
            "\nNote: You have not specified any connection options. This means "
            "your game \nwill be marked as being in 'closed development' in "
            "the index."
        )

    wizard.display("\nDon't forget to inspect and save your changes.")

    node_start(wizard)

# MSSP

def node_mssp_start(wizard):
    mssp_module = mod_import(settings.MSSP_META_MODULE or "server.conf.mssp")
    try:
        filename = mssp_module.__file__
        filename = "server/conf/mssp.py"

    text = f"""
    MSSP (Mud Server Status Protocol) has a vast amount of options so it must
    be modified outside this wizard by directly editing its config file here:

        '{filename}'

    MSSP allows traditional online MUD-listing sites/crawlers to continuously
    monitor your game and list information about it. Some of this, like active
    player-count, Evennia will automatically add for you, whereas most fields
    you need to set manually.

    To use MSSP you should generally have a publicly open game that external
    players can connect to. You also need to register at a MUD listing site to
    tell them to crawl your game.
    """

    wizard.ask_continue()

# Admin

def _save_changes(wizard):
    """
    Perform the save
    """

    # add import statement to settings file
    import_stanza = "from .connection_settings import *"
    setting_module = mod_import("server.conf.settings")
    with open(setting_module.__file__, "r+") as f:
        txt = f.read()  # moves pointer to end of file
        if import_stanza not in txt:
            # add to the end of the file
            f.write(
                "\n\n"
                "try:\n"
                "    # Created by the `evennia connections` wizard\n"
                f"    {import_stanza}\n"
                "except ImportError:\n"
                "    pass"
            )

    connect_settings_file = path.join(settings.GAME_DIR, "server", "conf", "connection_settings.py")
    with open(connect_settings_file, "w") as f:
        f.write(
            "# This file is auto-generated by the `evennia connections` wizard.\n"
            "# Don't edit manually, your changes will be overwritten.\n\n"
        )

        f.write(wizard.save_output)
        wizard.display(f"saving to {connect_settings_file} ...")

def node_view_and_apply_settings(wizard):
    """
    Inspect and save the data gathered in the other nodes

    """
    pp = pprint.PrettyPrinter(indent=4)
    saves = False

    # game index
    game_index_save_text = ""
    game_index_listing = (
        wizard.game_index_listing if hasattr(wizard, "game_index_listing") else None
    )
    if not game_index_listing and settings.GAME_INDEX_ENABLED:
        game_index_listing = settings.GAME_INDEX_LISTING
    if game_index_listing:
        game_index_save_text = (
            "GAME_INDEX_ENABLED = True\n"
            "GAME_INDEX_LISTING = \\\n" + pp.pformat(game_index_listing)
        )
        saves = True
    else:
        game_index_save_text = "# No Game Index settings found."

    # potentially add other wizards in the future
    text = game_index_save_text

    wizard.display(f"Settings to save:\n\n{text}")

    if saves:
        if wizard.ask_yesno("\nDo you want to save these settings?") == "yes":
            wizard.save_output = text
            _save_changes(wizard)
            wizard.display("... saved!\nThe changes will apply after you reload your server.")
        else:
            wizard.display("... cancelled.")

"""
This module contains historical deprecations that the Evennia launcher
checks for.

These all print to the terminal.
"""
import os

def check_errors(settings):
    """
    Check for deprecations that are critical errors and should stop
    the launcher.

    Args:
        settings (Settings): The Django settings file

    Raises:
        DeprecationWarning if a critical deprecation is found.

    """
    deprstring = (
        "settings.%s should be renamed to %s. If defaults are used, "
        "their path/classname must be updated "
        "(see evennia/settings_default.py)."
    )
    if hasattr(settings, "CMDSET_DEFAULT"):
        raise DeprecationWarning(deprstring % ("CMDSET_DEFAULT", "CMDSET_CHARACTER"))
    if hasattr(settings, "CMDSET_OOC"):
        raise DeprecationWarning(deprstring % ("CMDSET_OOC", "CMDSET_ACCOUNT"))
    if settings.WEBSERVER_ENABLED and not isinstance(settings.WEBSERVER_PORTS[0], tuple):
        raise DeprecationWarning(
            "settings.WEBSERVER_PORTS must be on the form [(proxyport, serverport), ...]"
        )
    if hasattr(settings, "BASE_COMM_TYPECLASS"):
        raise DeprecationWarning(deprstring % ("BASE_COMM_TYPECLASS", "BASE_CHANNEL_TYPECLASS"))
    if hasattr(settings, "COMM_TYPECLASS_PATHS"):
        raise DeprecationWarning(deprstring % ("COMM_TYPECLASS_PATHS", "CHANNEL_TYPECLASS_PATHS"))
    if hasattr(settings, "CHARACTER_DEFAULT_HOME"):
            "settings.CHARACTER_DEFAULT_HOME should be renamed to "
            "DEFAULT_HOME. See also settings.START_LOCATION "
            "(see evennia/settings_default.py)."
        )
        "settings.%s is now merged into settings.TYPECLASS_PATHS. Update your settings file."
    )
    if hasattr(settings, "OBJECT_TYPECLASS_PATHS"):
        raise DeprecationWarning(deprstring % "OBJECT_TYPECLASS_PATHS")
    if hasattr(settings, "SCRIPT_TYPECLASS_PATHS"):
        raise DeprecationWarning(deprstring % "SCRIPT_TYPECLASS_PATHS")
    if hasattr(settings, "ACCOUNT_TYPECLASS_PATHS"):
        raise DeprecationWarning(deprstring % "ACCOUNT_TYPECLASS_PATHS")
    if hasattr(settings, "CHANNEL_TYPECLASS_PATHS"):
        raise DeprecationWarning(deprstring % "CHANNEL_TYPECLASS_PATHS")
    if hasattr(settings, "SEARCH_MULTIMATCH_SEPARATOR"):
            "settings.SEARCH_MULTIMATCH_SEPARATOR was replaced by "
            "SEARCH_MULTIMATCH_REGEX and SEARCH_MULTIMATCH_TEMPLATE. "
            "Update your settings file (see evennia/settings_default.py "
            "for more info)."
        )
    depstring = (
        "settings.{} was renamed to {}. Update your settings file (the FuncParser "
        "replaces and generalizes that which inlinefuncs used to do)."
    )
    if hasattr(settings, "INLINEFUNC_ENABLED"):
            depstring.format(
                "settings.INLINEFUNC_ENABLED", "FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED"
            )
        )
    if hasattr(settings, "INLINEFUNC_STACK_MAXSIZE"):
            depstring.format("settings.INLINEFUNC_STACK_MAXSIZE", "FUNCPARSER_MAX_NESTING")
        )
    if hasattr(settings, "INLINEFUNC_MODULES"):
            depstring.format("settings.INLINEFUNC_MODULES", "FUNCPARSER_OUTGOING_MESSAGES_MODULES")
        )
    if hasattr(settings, "PROTFUNC_MODULES"):
            depstring.format("settings.PROTFUNC_MODULES", "FUNCPARSER_PROTOTYPE_VALUE_MODULES")
        )

    gametime_deprecation = (
        "The settings TIME_SEC_PER_MIN, TIME_MIN_PER_HOUR,"
        "TIME_HOUR_PER_DAY, TIME_DAY_PER_WEEK, \n"
        "TIME_WEEK_PER_MONTH and TIME_MONTH_PER_YEAR "
        "are no longer supported. Remove them from your "
        "settings file to continue.\nIf you want to use "
        "and manipulate these time units, the tools from utils.gametime "
        "are now found in contrib/convert_gametime.py instead."
    )
    if any(
        hasattr(settings, value)
        for value in (
            "TIME_SEC_PER_MIN",
            "TIME_MIN_PER_HOUR",
            "TIME_HOUR_PER_DAY",
            "TIME_DAY_PER_WEEK",
            "TIME_WEEK_PER_MONTH",
            "TIME_MONTH_PER_YEAR",
        )
    ):
        raise DeprecationWarning(gametime_deprecation)

    game_directory_deprecation = (
        "The setting GAME_DIRECTORY_LISTING was removed. It must be "
        "renamed to GAME_INDEX_LISTING instead."
    )
    if hasattr(settings, "GAME_DIRECTORY_LISTING"):
        raise DeprecationWarning(game_directory_deprecation)

    chan_connectinfo = settings.CHANNEL_CONNECTINFO
    if chan_connectinfo is not None and not isinstance(chan_connectinfo, dict):
            "settings.CHANNEL_CONNECTINFO has changed. It "
            "must now be either None or a dict "
            "specifying the properties of the channel to create."
        )
    if hasattr(settings, "CYCLE_LOGFILES"):
            "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."
        )
    if hasattr(settings, "CHANNEL_COMMAND_CLASS") or hasattr(settings, "CHANNEL_HANDLER_CLASS"):
            "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."
        )

    template_overrides_dir = os.path.join(settings.GAME_DIR, "web", "template_overrides")
    static_overrides_dir = os.path.join(settings.GAME_DIR, "web", "static_overrides")
    if os.path.exists(template_overrides_dir):
            f"The template_overrides directory ({template_overrides_dir}) has changed name.\n"
            " - Rename your existing `template_overrides` folder to `templates` instead."
        )
    if os.path.exists(static_overrides_dir):
            f"The static_overrides directory ({static_overrides_dir}) has changed name.\n"
            " 1. Delete any existing `web/static` folder and all its contents (this "
            "was auto-generated)\n"
            " 2. Rename your existing `static_overrides` folder to `static` instead."
        )

    if settings.MULTISESSION_MODE < 2 and settings.MAX_NR_SIMULTANEOUS_PUPPETS > 1:
            f"settings.MULTISESSION_MODE={settings.MULTISESSION_MODE} is not compatible with "
            f"settings.MAX_NR_SIMULTANEOUS_PUPPETS={settings.MAX_NR_SIMULTANEOUS_PUPPETS}. "
            "To allow multiple simultaneous puppets, the multi-session mode must be higher than 1."
        )

def check_warnings(settings):
    """
    Check conditions and deprecations that should produce warnings but which
    does not stop launch.
    """
    if settings.DEBUG:
        print(" [Devel: settings.DEBUG is True. Important to turn off in production.]")
    if settings.IN_GAME_ERRORS:
        print(" [Devel: settings.IN_GAME_ERRORS is True. Turn off in production.]")
    if settings.ALLOWED_HOSTS == ["*"]:
        print(" [Devel: settings.ALLOWED_HOSTS set to '*' (all). Limit in production.]")
    if settings.SERVER_HOSTNAME == "localhost":
        print(
            " [Devel: settings.SERVER_HOSTNAME is set to 'localhost'. "
            "Update to the actual hostname in production.]"
        )

    for dbentry in settings.DATABASES.values():
        if "psycopg" in dbentry.get("ENGINE", ""):
            print(
                'Deprecation: postgresql_psycopg2 backend is deprecated". '
                "Switch settings.DATABASES to use "
                '"ENGINE": "django.db.backends.postgresql instead"'
            )

#!/usr/bin/python
"""
Evennia launcher program

This is the start point for running Evennia.

Sets the appropriate environmental variables for managing an Evennia game. It will start and connect
to the Portal, through which the Server is also controlled. This pprogram

Run the script with the -h flag to see usage information.

"""

import os
import re
import shutil
import signal
import sys
from argparse import ArgumentParser
from distutils.version import LooseVersion
from subprocess import STDOUT, CalledProcessError, Popen, call, check_output

from django.core.management import execute_from_command_line
from django.db.utils import ProgrammingError
from twisted.internet import endpoints, reactor

# Signal processing
SIG = signal.SIGINT
CTRL_C_EVENT = 0  # Windows SIGINT-like signal

# Set up the main python paths to Evennia
EVENNIA_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import evennia  # noqa

EVENNIA_LIB = os.path.join(EVENNIA_ROOT, "evennia")
EVENNIA_SERVER = os.path.join(EVENNIA_LIB, "server")
EVENNIA_TEMPLATE = os.path.join(EVENNIA_LIB, "game_template")
EVENNIA_PROFILING = os.path.join(EVENNIA_SERVER, "profiling")
EVENNIA_DUMMYRUNNER = os.path.join(EVENNIA_PROFILING, "dummyrunner.py")

TWISTED_BINARY = "twistd"

# Game directory structure
SETTINGFILE = "settings.py"
SERVERDIR = "server"
CONFDIR = os.path.join(SERVERDIR, "conf")
SETTINGS_PATH = os.path.join(CONFDIR, SETTINGFILE)
SETTINGS_DOTPATH = "server.conf.settings"
CURRENT_DIR = os.getcwd()
GAMEDIR = CURRENT_DIR

# Operational setup

SERVER_LOGFILE = None
PORTAL_LOGFILE = None
HTTP_LOGFILE = None

SERVER_PIDFILE = None
PORTAL_PIDFILE = None

SERVER_PY_FILE = None
PORTAL_PY_FILE = None

SPROFILER_LOGFILE = None
PPROFILER_LOGFILE = None

TEST_MODE = False
ENFORCED_SETTING = False

REACTOR_RUN = False
NO_REACTOR_STOP = False

# communication constants

AMP_PORT = None
AMP_HOST = None
AMP_INTERFACE = None
AMP_CONNECTION = None

SRELOAD = chr(14)  # server reloading (have portal start a new server)
SSTART = chr(15)  # server start
SSHUTD = chr(17)  # server-only shutdown
SRESET = chr(19)  # shutdown server in reset mode

# live version requirement checks (from VERSION_REQS.txt file)
PYTHON_MIN = None
PYTHON_MAX_TESTED = None
TWISTED_MIN = None
DJANGO_MIN = None
DJANGO_MAX_TESTED = None

with open(os.path.join(EVENNIA_LIB, "VERSION_REQS.txt")) as fil:
    for line in fil.readlines():
        if line.startswith("#") or "=" not in line:
            continue
        key, *value = (part.strip() for part in line.split("=", 1))
        if key == "PYTHON_MIN":
            PYTHON_MIN = value[0] if value else "0"
        elif key == "PYTHON_MAX_TESTED":
            PYTHON_MAX_TESTED = value[0] if value else "100"
        elif key == "TWISTED_MIN":
            TWISTED_MIN = value[0] if value else "0"
        elif key == "DJANGO_MIN":
            DJANGO_MIN = value[0] if value else "0"
        elif key == "DJANGO_MAX_TESTED":
            DJANGO_MAX_TESTED = value[0] if value else "100"

try:
    sys.path[1] = EVENNIA_ROOT
except IndexError:
    sys.path.append(EVENNIA_ROOT)

#
# Messages
#

CREATED_NEW_GAMEDIR = """
    Welcome to Evennia!
    Created a new Evennia game directory '{gamedir}'.

    You can now optionally edit your new settings file
    at {settings_path}. If you don't, the defaults
    will work out of the box. When ready to continue, 'cd' to your
    game directory and run:

       evennia migrate

    This initializes the database. To start the server for the first
    time, run:

       evennia start

    Make sure to create a superuser when asked for it (the email is optional)
    You should now be able to connect to your server on 'localhost', port 4000
    using a telnet/mud client or http://localhost:4001 using your web browser.
    If things don't work, check the log with `evennia --log`. Also make sure
    ports are open.

    (Finally, why not run `evennia connections` and make the world aware of
    your new Evennia project!)
    """

ERROR_INPUT = """
    Command
      {args} {kwargs}
    raised an error: '{traceback}'.
"""

ERROR_NO_ALT_GAMEDIR = """
    The path '{gamedir}' could not be found.
"""

ERROR_NO_GAMEDIR = """
    ERROR: No Evennia settings file was found. Evennia looks for the
    file in your game directory as ./server/conf/settings.py.

    You must run this command from somewhere inside a valid game
    directory first created with

        evennia --init mygamename

    If you are in a game directory but is missing a settings.py file,
    it may be because you have git-cloned an existing game directory.
    The settings.py file is not cloned by git (it's in .gitignore)
    since it can contain sensitive and/or server-specific information.
    You can create a new, empty settings file with

        evennia --initsettings

    If cloning the settings file is not a problem you could manually
    copy over the old settings file or remove its entry in .gitignore

    """

WARNING_MOVING_SUPERUSER = """
    WARNING: Evennia expects an Account superuser with id=1. No such
    Account was found. However, another superuser ('{other_key}',
    id={other_id}) was found in the database. If you just created this
    superuser and still see this text it is probably due to the
    database being flushed recently - in this case the database's
    internal auto-counter might just start from some value higher than
    one.

    We will fix this by assigning the id 1 to Account '{other_key}'.
    Please confirm this is acceptable before continuing.
    """

WARNING_RUNSERVER = """
    WARNING: There is no need to run the Django development
    webserver to test out Evennia web features (the web client
    will in fact not work since the Django test server knows
    nothing about MUDs).  Instead, just start Evennia with the
    webserver component active (this is the default).
    """

ERROR_SETTINGS = """
    ERROR: There was an error importing Evennia's config file
    {settingspath}.
    There is usually one of three reasons for this:
        1) You are not running this command from your game directory.
           Change directory to your game directory and try again (or
           create a new game directory using evennia --init <dirname>)
        2) The settings file contains a syntax error. If you see a
           traceback above, review it, resolve the problem and try again.
        3) Django is not correctly installed. This usually shows as
           errors mentioning 'DJANGO_SETTINGS_MODULE'. If you run a
           virtual machine, it might be worth to restart it to see if
           this resolves the issue.
    settingspath=SETTINGS_PATH
)

ERROR_INITSETTINGS = """
u   ERROR: 'evennia --initsettings' must be called from the root of
    your game directory, since it tries to (re)create the new
    settings.py file in a subfolder server/conf/.
    """

RECREATED_SETTINGS = """
    (Re)created an empty settings file in server/conf/settings.py.

    Note that if you were using an existing database, the password
    salt of this new settings file will be different from the old one.
    This means that any existing accounts may not be able to log in to
    their accounts with their old passwords.
    """

ERROR_INITMISSING = """
    ERROR: 'evennia --initmissing' must be called from the root of
    your game directory, since it tries to create any missing files
    in the server/ subfolder.
    """

RECREATED_MISSING = """
    (Re)created any missing directories or files.  Evennia should
    be ready to run now!
    """

ERROR_DATABASE = """
    ERROR: Your database does not exist or is not set up correctly.
    (error was '{traceback}')

    If you think your database should work, make sure you are running your
    commands from inside your game directory. If this error persists, run

    to initialize/update the database according to your settings.
    """

ERROR_WINDOWS_WIN32API = """
    ERROR: Unable to import win32api, which Twisted requires to run.
    You may download it with pip in your Python environment:

    pip install --upgrade pywin32

    """

INFO_WINDOWS_BATFILE = """
    INFO: Since you are running Windows, a file 'twistd.bat' was
    created for you. This is a simple batch file that tries to call
    the twisted executable. Evennia determined this to be:

       {twistd_path}

    If you run into errors at startup you might need to edit
    twistd.bat to point to the actual location of the Twisted
    executable (usually called twistd.py) on your machine.

    This procedure is only done once. Run `evennia` again when you
    are ready to start the server.
    """

CMDLINE_HELP = """Starts, initializes, manages and operates the Evennia MU* server.
Most standard django management commands are also accepted."""

VERSION_INFO = """
    Evennia {version}
    OS: {os}
    Python: {python}
    Twisted: {twisted}
    Django: {django}{about}
    """

ABOUT_INFO = """
    Evennia MUD/MUX/MU* development system

    Licence: BSD 3-Clause Licence
    Web: http://www.evennia.com
    Chat: https://discord.gg/AJJpcRUhtF
    Forum: http://www.evennia.com/discussions
    Maintainer (2006-10): Greg Taylor
    Maintainer (2010-):   Griatch (griatch AT gmail DOT com)

    Use -h for command line options.
    """

HELP_ENTRY = """
    Evennia has two processes, the 'Server' and the 'Portal'.
    External users connect to the Portal while the Server runs the
    game/database. Restarting the Server will refresh code but not
    disconnect users.

    To start a new game, use 'evennia --init mygame'.
    For more ways to operate and manage Evennia, see 'evennia -h'.

    If you want to add unit tests to your game, see
        https://github.com/evennia/evennia/wiki/Unit-Testing

    Evennia's manual is found here:
        https://github.com/evennia/evennia/wiki
    """

MENU = """
    +----Evennia Launcher-------------------------------------------+
    {gameinfo}
    +--- Common operations -----------------------------------------+
    |  1) Start                       (also restart stopped Server) |
    |  2) Reload               (stop/start Server in 'reload' mode) |
    |  3) Stop                         (shutdown Portal and Server) |
    |  4) Reboot                            (shutdown then restart) |
    +--- Other operations ------------------------------------------+
    |  5) Reset              (stop/start Server in 'shutdown' mode) |
    |  6) Stop Server only                                          |
    |  7) Kill Server only            (send kill signal to process) |
    |  8) Kill Portal + Server                                      |
    +--- Information -----------------------------------------------+
    |  9) Tail log files      (quickly see errors - Ctrl-C to exit) |
    | 10) Status                                                    |
    | 11) Port info                                                 |
    +--- Testing ---------------------------------------------------+
    | 12) Test gamedir             (run gamedir test suite, if any) |
    | 13) Test Evennia                     (run Evennia test suite) |
    +---------------------------------------------------------------+
    |  h) Help               i) About info                q) Abort  |
    +---------------------------------------------------------------+"""

ERROR_AMP_UNCONFIGURED = """
    Can't find server info for connecting. Either run this command from
    the game dir (it will then use the game's settings file) or specify
    the path to your game's settings file manually with the --settings
    option.
    """

ERROR_LOGDIR_MISSING = """
    ERROR: One or more log-file directory locations could not be
    found:

    {logfiles}

    This is simple to fix: Just manually create the missing log
    directory (or directories) and re-launch the server (the log files
    will be created automatically).

    (Explanation: Evennia creates the log directory automatically when
    initializing a new game directory. This error usually happens if
    you used git to clone a pre-created game directory - since log
    files are in .gitignore they will not be cloned, which leads to
    the log directory also not being created.)
    """

ERROR_PYTHON_VERSION = """
    ERROR: Python {python_version} used. Evennia requires version
    {python_min} or higher.
    """

WARNING_PYTHON_MAX_TESTED_VERSION = """
    WARNING: Python {python_version} used. Evennia is only tested with Python
    versions {python_min} to {python_max_tested}. If you see unexpected errors, try
    reinstalling with a tested Python version instead.
    """

ERROR_TWISTED_VERSION = """
    ERROR: Twisted {twisted_version} found. Evennia requires
    version {twisted_min} or higher.
    """

ERROR_NOTWISTED = """
    ERROR: Twisted does not seem to be installed.
    """

ERROR_DJANGO_MIN = """
    ERROR: Django {django_version} found. Evennia supports Django
    {django_min} - {django_max_tested}. Using an older version is not supported.

    If you are using a virtualenv, use the command `pip install --upgrade -e evennia` where
    `evennia` is the folder to where you cloned the Evennia library. If not
    in a virtualenv you can install django with for example `pip install --upgrade django`
    or with `pip install django=={django_min}` to get a specific version.

    It's also a good idea to run `evennia migrate` after this upgrade. Ignore
    any warnings and don't run `makemigrate` even if told to.
    """

NOTE_DJANGO_NEW = """
    NOTE: Django {django_version} found. This is newer than Evennia's
    recommended version ({django_max_tested}). It might work, but is new
    enough to not be fully tested yet. Report any issues.
    """

ERROR_NODJANGO = """
    ERROR: Django does not seem to be installed.
    """

NOTE_KEYBOARDINTERRUPT = """
    STOP: Caught keyboard interrupt while in interactive mode.
    """

NOTE_TEST_DEFAULT = """
    TESTING: Using Evennia's default settings file (evennia.settings_default).
    (use 'evennia test --settings settings.py .' to run only your custom game tests)
    """

NOTE_TEST_CUSTOM = """
    TESTING: Using specified settings file '{settings_dotpath}'.

    OBS: Evennia's full test suite may not pass if the settings are very
    different from the default (use 'evennia test evennia' to run core tests)
    """

PROCESS_ERROR = """
    {component} process error: {traceback}.
    """

PORTAL_INFO = """{servername} Portal {version}
    external ports:
        {telnet}
        {telnet_ssl}
        {ssh}
        {webserver_proxy}
        {webclient}
    internal_ports (to Server):
        {webserver_internal}
        {amp}
"""

SERVER_INFO = """{servername} Server {version}
    internal ports (to Portal):
        {webserver}
        {amp}
    {irc_rss}
    {info}
    {errors}"""

ARG_OPTIONS = """Actions on installed server. One of:
 start       - launch server+portal if not running
 reload      - restart server in 'reload' mode
 stop        - shutdown server+portal
 reboot      - shutdown server+portal, then start again
 reset       - restart server in 'shutdown' mode
 istart      - start server in foreground (until reload)
 ipstart     - start portal in foreground
 sstop       - stop only server
 kill        - send kill signal to portal+server (force)
 skill       - send kill signal only to server
 status      - show server and portal run state
 info        - show server and portal port info
 menu        - show a menu of options
 connections - show connection wizard
Others, like migrate, test and shell is passed on to Django."""

#
# Private helper functions
#

def _is_windows():
    return os.name == "nt"

def _file_names_compact(filepath1, filepath2):
    "Compact the output of filenames with same base dir"
    dirname1 = os.path.dirname(filepath1)
    dirname2 = os.path.dirname(filepath2)
    if dirname1 == dirname2:
        name2 = os.path.basename(filepath2)
        return "{} and {}".format(filepath1, name2)
    else:
        return "{} and {}".format(filepath1, filepath2)

def _print_info(portal_info_dict, server_info_dict):
    """
    Format info dicts from the Portal/Server for display

    """
    ind = " " * 8

    def _prepare_dict(dct):
        out = {}
        for key, value in dct.items():
            if isinstance(value, list):
                value = "\n{}".format(ind).join(str(val) for val in value)
            out[key] = value
        return out

    def _strip_empty_lines(string):
        return "\n".join(line for line in string.split("\n") if line.strip())

    pstr, sstr = "", ""
    if portal_info_dict:
        pdict = _prepare_dict(portal_info_dict)
        pstr = _strip_empty_lines(PORTAL_INFO.format_map(pdict))

    if server_info_dict:
        sdict = _prepare_dict(server_info_dict)
        sstr = _strip_empty_lines(SERVER_INFO.format_map(sdict))

    info = pstr + ("\n\n" + sstr if sstr else "")
    maxwidth = max(len(line) for line in info.split("\n"))
    top_border = "-" * (maxwidth - 11) + " Evennia " + "---"
    border = "-" * (maxwidth + 1)
    print(top_border + "\n" + info + "\n" + border)

def _parse_status(response):
    "Unpack the status information"
    return pickle.loads(response["status"])

def _get_twistd_cmdline(pprofiler, sprofiler):
    """
    Compile the command line for starting a Twisted application using the 'twistd' executable.

    """
    portal_cmd = [
        TWISTED_BINARY,
        f"--python={PORTAL_PY_FILE}",
        "--logger=evennia.utils.logger.GetPortalLogObserver",
    ]
    server_cmd = [
        f"--python={SERVER_PY_FILE}",
        "--logger=evennia.utils.logger.GetServerLogObserver",
    ]

    if os.name != "nt":
        # PID files only for UNIX
        portal_cmd.append("--pidfile={}".format(PORTAL_PIDFILE))
        server_cmd.append("--pidfile={}".format(SERVER_PIDFILE))

    if pprofiler:
        portal_cmd.extend(
            ["--savestats", "--profiler=cprofile", "--profile={}".format(PPROFILER_LOGFILE)]
        )
    if sprofiler:
        server_cmd.extend(
            ["--savestats", "--profiler=cprofile", "--profile={}".format(SPROFILER_LOGFILE)]
        )

    return portal_cmd, server_cmd

def _reactor_stop():
    if not NO_REACTOR_STOP:
        reactor.stop()

#
#  Protocol Evennia launcher - Portal/Server communication
#

    """
    Ping between AMP services

    """

    """

    """

class AMPLauncherProtocol(amp.AMP):
    """
    Defines callbacks to the launcher

    """

        self.on_status = []

    def wait_for_status(self, callback):
        """
        Register a waiter for a status return.

        """
        self.on_status.append(callback)

    @MsgStatus.responder
    def receive_status_from_portal(self, status):
        """
        Get a status signal from portal - fire next queued
        callback

        """
        try:
            callback = self.on_status.pop()
            pass
        else:
            status = pickle.loads(status)
            callback(status)
        return {"status": pickle.dumps(b"")}

def send_instruction(operation, arguments, callback=None, errback=None):
    """
    Send instruction and handle the response.

    """
    global AMP_CONNECTION, REACTOR_RUN

    if None in (AMP_HOST, AMP_PORT, AMP_INTERFACE):
        print(ERROR_AMP_UNCONFIGURED)
        sys.exit()

    def _callback(result):
            callback(result)

    def _errback(fail):
        if errback:
            errback(fail)

    def _on_connect(prot):
        """
        This fires with the protocol when connection is established. We
        immediately send off the instruction

        """
        global AMP_CONNECTION
        AMP_CONNECTION = prot
        _send()

    def _on_connect_fail(fail):
        "This is called if portal is not reachable."
        errback(fail)

    def _send():
        if operation == PSTATUS:
            return AMP_CONNECTION.callRemote(MsgStatus, status=b"").addCallbacks(
                _callback, _errback
            )
        else:
            return AMP_CONNECTION.callRemote(
                MsgLauncher2Portal,
                operation=bytes(operation, "utf-8"),
                arguments=pickle.dumps(arguments, pickle.HIGHEST_PROTOCOL),
            ).addCallbacks(_callback, _errback)

    if AMP_CONNECTION:
        # already connected - send right away
        return _send()
    else:
        # we must connect first, send once connected
        point = endpoints.TCP4ClientEndpoint(reactor, AMP_HOST, AMP_PORT)
        deferred = endpoints.connectProtocol(point, AMPLauncherProtocol())
        deferred.addCallbacks(_on_connect, _on_connect_fail)
        REACTOR_RUN = True

def query_status(callback=None):
    """
    Send status ping to portal

    """
    wmap = {True: "RUNNING", False: "NOT RUNNING"}

    def _callback(response):
            callback(response)
        else:
            pstatus, sstatus, ppid, spid, pinfo, sinfo = _parse_status(response)
            print(
                "Portal: {}{}\nServer: {}{}".format(
                    wmap[pstatus],
                    " (pid {})".format(get_pid(PORTAL_PIDFILE, ppid)) if pstatus else "",
                    wmap[sstatus],
                    " (pid {})".format(get_pid(SERVER_PIDFILE, spid)) if sstatus else "",
                )
            )
            _reactor_stop()

        pstatus, sstatus = False, False
        print("Portal: {}\nServer: {}".format(wmap[pstatus], wmap[sstatus]))
        _reactor_stop()

    send_instruction(PSTATUS, None, _callback, _errback)

def wait_for_status_reply(callback):
    """
    Wait for an explicit STATUS signal to be sent back from Evennia.
    """
        AMP_CONNECTION.wait_for_status(callback)
    else:
        print("No Evennia connection established.")

def wait_for_status(
    portal_running=True, server_running=True, callback=None, errback=None, rate=0.5, retries=20
):
    """
    Repeat the status ping until the desired state combination is achieved.

    Args:
        portal_running (bool or None): Desired portal run-state. If None, any state
            is accepted.
        server_running (bool or None): Desired server run-state. If None, any state
            is accepted. The portal must be running.
        callback (callable): Will be called with portal_state, server_state when
            condition is fulfilled.
        errback (callable): Will be called with portal_state, server_state if the
            request is timed out.
        rate (float): How often to retry.
        retries (int): How many times to retry before timing out and calling `errback`.
    """

        prun, srun, _, _, _, _ = _parse_status(response)
        if (portal_running is None or prun == portal_running) and (
            server_running is None or srun == server_running
        ):
            # the correct state was achieved
            if callback:
                callback(prun, srun)
            else:
                _reactor_stop()
        else:
            if retries <= 0:
                if errback:
                    errback(prun, srun)
                else:
                    print("Connection to Evennia timed out. Try again.")
                    _reactor_stop()
            else:
                reactor.callLater(
                    rate,
                    wait_for_status,
                    portal_running,
                    server_running,
                    callback,
                    errback,
                    rate,
                    retries - 1,
                )

        """
        Portal not running
        """
        if not portal_running:
            # this is what we want
                callback(portal_running, server_running)
            else:
        else:
                    errback(portal_running, server_running)
                else:
            else:
                    rate,
                    callback,
                    errback,
                    rate,
                )

    return send_instruction(PSTATUS, None, _callback, _errback)

#
#  Operational functions
#

def collectstatic():
    "Run the collectstatic django command"
    django.core.management.call_command("collectstatic", interactive=False, verbosity=0)

def start_evennia(pprofiler=False, sprofiler=False):
    """
    This will start Evennia anew by launching the Evennia Portal (which in turn
    will start the Server)

    """
    portal_cmd, server_cmd = _get_twistd_cmdline(pprofiler, sprofiler)

    def _fail(fail):
        print(fail)

    def _server_started(response):
        print("... Server started.\nEvennia running.")
        if response:
            _, _, _, _, pinfo, sinfo = response
            _print_info(pinfo, sinfo)

    def _portal_started(*args):
        print(
            "... Portal started.\nServer starting {} ...".format(
                "(under cProfile)" if sprofiler else ""
            )
        )
        wait_for_status_reply(_server_started)
        send_instruction(SSTART, server_cmd)

    def _portal_running(response):
        prun, srun, ppid, spid, _, _ = _parse_status(response)
        print("Portal is already running as process {pid}. Not restarted.".format(pid=ppid))
        if srun:
            print("Server is already running as process {pid}. Not restarted.".format(pid=spid))
        else:
            print("Server starting {}...".format("(under cProfile)" if sprofiler else ""))
            send_instruction(SSTART, server_cmd, _server_started, _fail)

    def _portal_not_running(fail):
        print("Portal starting {}...".format("(under cProfile)" if pprofiler else ""))
        try:
            if _is_windows():
                # Windows requires special care
                create_no_window = 0x08000000
                Popen(portal_cmd, env=getenv(), bufsize=-1, creationflags=create_no_window)
            else:
                Popen(portal_cmd, env=getenv(), bufsize=-1)
            print(PROCESS_ERROR.format(component="Portal", traceback=e))
        wait_for_status(True, None, _portal_started)

    collectstatic()
    send_instruction(PSTATUS, None, _portal_running, _portal_not_running)

def reload_evennia(sprofiler=False, reset=False):
    """
    This will instruct the Portal to reboot the Server component. We
    do this manually by telling the server to shutdown (in reload mode)
    and wait for the portal to report back, at which point we start the
    server again. This way we control the process exactly.

    """
    _, server_cmd = _get_twistd_cmdline(False, sprofiler)

    def _server_restarted(*args):
        print("... Server re-started.")

    def _server_reloaded(status):
        print("... Server {}.".format("reset" if reset else "reloaded"))

    def _server_stopped(status):
        wait_for_status_reply(_server_reloaded)

        _, srun, _, _, _, _ = _parse_status(response)
        if srun:
            print("Server {}...".format("resetting" if reset else "reloading"))
            wait_for_status_reply(_server_stopped)
            send_instruction(SRESET if reset else SRELOAD, {})
        else:
            print("Server down. Re-starting ...")
            wait_for_status_reply(_server_restarted)
            send_instruction(SSTART, server_cmd)

        print("Evennia not running. Starting ...")
        start_evennia()

def stop_evennia():
    """
    This instructs the Portal to stop the Server and then itself.

    """

    def _portal_stopped(*args):
        print("... Portal stopped.\nEvennia shut down.")

    def _server_stopped(*args):
        print("... Server stopped.\nStopping Portal ...")
        send_instruction(PSHUTD, {})
        wait_for_status(False, None, _portal_stopped)

        if srun:
            print("Server stopping ...")
            send_instruction(SSHUTD, {})
        else:
            print("Server already stopped.\nStopping Portal ...")
            send_instruction(PSHUTD, {})
            wait_for_status(False, None, _portal_stopped)

        print("Evennia not running.")

def reboot_evennia(pprofiler=False, sprofiler=False):
    """
    This is essentially an evennia stop && evennia start except we make sure
    the system has successfully shut down before starting it again.

    If evennia was not running, start it.

    """
    global AMP_CONNECTION

        print("... Portal stopped. Evennia shut down. Rebooting ...")
        AMP_CONNECTION = None
        start_evennia(pprofiler, sprofiler)

        if srun:
        else:

def start_only_server():
    """
    Tell portal to start server (debug)
    """
    portal_cmd, server_cmd = _get_twistd_cmdline(False, False)
    print("launcher: Sending to portal: SSTART + {}".format(server_cmd))
    send_instruction(SSTART, server_cmd)

def start_server_interactive():
    """
    Start the Server under control of the launcher process (foreground)

    """

    def _iserver():
        _, server_twistd_cmd = _get_twistd_cmdline(False, False)
        server_twistd_cmd.append("--nodaemon")
        print("Starting Server in interactive mode (stop with Ctrl-C)...")
        try:
            Popen(server_twistd_cmd, env=getenv(), stderr=STDOUT).wait()
        except KeyboardInterrupt:
            print("... Stopped Server with Ctrl-C.")
        else:
            print("... Server stopped (leaving interactive mode).")

    stop_server_only(when_stopped=_iserver, interactive=True)

def start_portal_interactive():
    """
    Start the Portal under control of the launcher process (foreground)

    Notes:
        In a normal start, the launcher waits for the Portal to start, then
        tells it to start the Server. Since we can't do this here, we instead
        start the Server first and then starts the Portal - the Server will
        auto-reconnect to the Portal. To allow the Server to be reloaded, this
        relies on a fixed server server-cmdline stored as a fallback on the
        portal application in evennia/server/portal/portal.py.

    """

    def _iportal(fail):
        portal_twistd_cmd, server_twistd_cmd = _get_twistd_cmdline(False, False)
        portal_twistd_cmd.append("--nodaemon")

        # starting Server first - it will auto-connect once Portal comes up
        if _is_windows():
            # Windows requires special care
            create_no_window = 0x08000000
            Popen(server_twistd_cmd, env=getenv(), bufsize=-1, creationflags=create_no_window)
        else:
            Popen(server_twistd_cmd, env=getenv(), bufsize=-1)

        print("Starting Portal in interactive mode (stop with Ctrl-C)...")
        try:
            Popen(portal_twistd_cmd, env=getenv(), stderr=STDOUT).wait()
            print("... Stopped Portal with Ctrl-C.")
        else:
            print("... Portal stopped (leaving interactive mode).")

        print("Evennia must be shut down completely before running Portal in interactive mode.")

    send_instruction(PSTATUS, None, _portal_running, _iportal)

def stop_server_only(when_stopped=None, interactive=False):
    """
    Only stop the Server-component of Evennia (this is not useful except for debug)

    Args:
        when_stopped (callable): This will be called with no arguments when Server has stopped (or
            if it had already stopped when this is called).
        interactive (bool, optional): Set if this is called as part of the interactive reload
            mechanism.

    """

        if when_stopped:
            when_stopped()
        else:
            print("... Server stopped.")

        if srun:
            if interactive:
                send_instruction(SRELOAD, {})
            else:
                send_instruction(SSHUTD, {})
        else:
            if when_stopped:
                when_stopped()
            else:
                print("Server is not running.")

        print("Evennia is not running.")
        if interactive:
            print("Start Evennia normally first, then use `istart` to switch to interactive mode.")

def query_info():
    """
    Display the info strings from the running Evennia

    """

    def _got_status(status):
        _, _, _, _, pinfo, sinfo = _parse_status(status)
        _print_info(pinfo, sinfo)

        query_status(_got_status)

def tail_log_files(filename1, filename2, start_lines1=20, start_lines2=20, rate=1):
    """
    Tail two logfiles interactively, combining their output to stdout

    When first starting, this will display the tail of the log files. After
    that it will poll the log files repeatedly and display changes.

    Args:
        filename1 (str): Path to first log file.
        filename2 (str): Path to second log file.
        start_lines1 (int): How many lines to show from existing first log.
        start_lines2 (int): How many lines to show from existing second log.
        rate (int, optional): How often to poll the log file.

    """
    global REACTOR_RUN

    def _file_changed(filename, prev_size):
        "Get size of file in bytes, get diff compared with previous size"
        try:
            new_size = os.path.getsize(filename)
        except FileNotFoundError:
            return False, 0
        return new_size != prev_size, new_size

    def _get_new_lines(filehandle, old_linecount):
        "count lines, get the ones not counted before"

        def _block(filehandle, size=65536):
            "File block generator for quick traversal"
            while True:
                dat = filehandle.read(size)
                if not dat:
                    break
                yield dat

        # count number of lines in file
        new_linecount = sum(blck.count("\n") for blck in _block(filehandle))

        if new_linecount < old_linecount:
            # this happens if the file was cycled or manually deleted/edited.
            print(
                " ** Log file {filename} has cycled or been edited. Restarting log. ".format(
                    filename=filehandle.name
                )
            )
            new_linecount = 0
            old_linecount = 0

        lines_to_get = max(0, new_linecount - old_linecount)

        if not lines_to_get:
            return [], old_linecount

        lines_found = []
        buffer_size = 4098
        block_count = -1

        while len(lines_found) < lines_to_get:
            try:
                # scan backwards in file, starting from the end
                filehandle.seek(block_count * buffer_size, os.SEEK_END)
            except IOError:
                # file too small for current seek, include entire file
                filehandle.seek(0)
                lines_found = filehandle.readlines()
                break
            lines_found = filehandle.readlines()
            block_count -= 1

        # only actually return the new lines
        return lines_found[-lines_to_get:], new_linecount

    def _tail_file(filename, file_size, line_count, max_lines=None):
        """This will cycle repeatedly, printing new lines"""

        # poll for changes
        has_changed, file_size = _file_changed(filename, file_size)

        if has_changed:
            try:
                with open(filename, "r") as filehandle:
                    new_lines, line_count = _get_new_lines(filehandle, line_count)
                # the log file might not exist yet. Wait a little, then try again ...
                pass
            else:
                if max_lines == 0:
                    # don't show any lines from old file
                    new_lines = []
                elif max_lines:
                    # show some lines from first startup
                    new_lines = new_lines[-max_lines:]

                # print to stdout without line break (log has its own line feeds)
                sys.stdout.write("".join(new_lines))
                sys.stdout.flush()

        # set up the next poll
        reactor.callLater(rate, _tail_file, filename, file_size, line_count, max_lines=100)

    reactor.callLater(0, _tail_file, filename1, 0, 0, max_lines=start_lines1)
    reactor.callLater(0, _tail_file, filename2, 0, 0, max_lines=start_lines2)

    REACTOR_RUN = True

#
# Environment setup
#

def evennia_version():
    """
    Get the Evennia version info from the main package.

    """
    try:
        version = evennia.__version__
    except (ImportError, AttributeError):
        # even if evennia is not found, we should not crash here.
        pass
    else:
        return version
    try:
        rev = (
            check_output("git rev-parse --short HEAD", shell=True, cwd=EVENNIA_ROOT, stderr=STDOUT)
            .strip()
            .decode()
        )
        # move on if git is not answering
        pass

EVENNIA_VERSION = evennia_version()

def check_main_evennia_dependencies():
    """
    Checks and imports the Evennia dependencies. This must be done
    already before the paths are set up.

    Returns:
        not_error (bool): True if no dependency error was found.

    """

    def _test_python_version():
        """Test Python version"""
        python_version = ".".join(str(num) for num in sys.version_info if isinstance(num, int))
        python_curr = LooseVersion(python_version)
        python_min = LooseVersion(PYTHON_MIN)
        python_max = LooseVersion(PYTHON_MAX_TESTED)

        if python_curr < python_min:
            print(ERROR_PYTHON_VERSION.format(python_version=python_version, python_min=PYTHON_MIN))
        elif python_curr > python_max:
            print(
                WARNING_PYTHON_MAX_TESTED_VERSION.format(
                    python_version=python_version,
                    python_min=PYTHON_MIN,
                    python_max_tested=PYTHON_MAX_TESTED,
                )
            )

    def _test_twisted_version():
        """Test Twisted version"""
        try:
            import twisted
            print(ERROR_NOTWISTED)
        else:
            twisted_version = twisted.version.short()
            twisted_curr = LooseVersion(twisted_version)
            twisted_min = LooseVersion(TWISTED_MIN)

            if twisted_curr < twisted_min:
                print(
                    ERROR_TWISTED_VERSION.format(
                        twisted_version=twisted_version, twisted_min=TWISTED_MIN
                    )
                )
            else:

    def _test_django_version():
        """Test Django version"""
        try:
            import django
            print(ERROR_NODJANGO)
        else:
            django_version = ".".join(str(num) for num in django.VERSION if isinstance(num, int))
            # only the main version (1.5, not 1.5.4.0)
            django_version = ".".join(django_version.split(".")[:2])
            django_curr = LooseVersion(django_version)
            django_min = LooseVersion(DJANGO_MIN)
            django_max = LooseVersion(DJANGO_MAX_TESTED)

            if django_curr < django_min:
                print(
                    ERROR_DJANGO_MIN.format(
                        django_version=django_version,
                        django_min=DJANGO_MIN,
                        django_max_tested=DJANGO_MAX_TESTED,
                    )
                )
            elif django_curr > django_max:
                print(
                    NOTE_DJANGO_NEW.format(
                        django_version=django_version, django_max_tested=DJANGO_MAX_TESTED
                    )
                )

    # return True/False if error was reported or not
    return all((_test_python_version(), _test_twisted_version(), _test_django_version()))

def set_gamedir(path):
    """
    Set GAMEDIR based on path, by figuring out where the setting file
    is inside the directory tree. This allows for running the launcher
    from elsewhere than the top of the gamedir folder.

    """
    global GAMEDIR

    Ndepth = 10
    settings_path = SETTINGS_DOTPATH.replace(".", os.sep) + ".py"
    os.chdir(path)
    for i in range(Ndepth):
            if os.path.isfile(settings_path):
                GAMEDIR = gpath
                return
        os.chdir(os.pardir)
    print(ERROR_NO_GAMEDIR)
    sys.exit()

def create_secret_key():
    """
    Randomly create the secret key for the settings file

    """
    import random
    import string

    secret_key = list(
        (string.ascii_letters + string.digits + string.punctuation)
        .replace("\\", "")
        .replace("'", '"')
        .replace("{", "_")
        .replace("}", "-")
    )
    random.shuffle(secret_key)
    secret_key = "".join(secret_key[:40])
    return secret_key

def create_settings_file(init=True, secret_settings=False):
    """
    Uses the template settings file to build a working settings file.

    Args:
        init (bool): This is part of the normal evennia --init
            operation.  If false, this function will copy a fresh
            template file in (asking if it already exists).
        secret_settings (bool, optional): If False, create settings.py, otherwise
            create the secret_settings.py file.

    """
    if secret_settings:
        settings_path = os.path.join(GAMEDIR, "server", "conf", "secret_settings.py")
        setting_dict = {"secret_key": "'%s'" % create_secret_key()}
    else:
        settings_path = os.path.join(GAMEDIR, "server", "conf", "settings.py")
        setting_dict = {
            "settings_default": os.path.join(EVENNIA_LIB, "settings_default.py"),
            "servername": '"%s"' % GAMEDIR.rsplit(os.path.sep, 1)[1],
            "secret_key": "'%s'" % create_secret_key(),
        }

    if not init:
        # if not --init mode, settings file may already exist from before
        if os.path.exists(settings_path):
            inp = input("%s already exists. Do you want to reset it? y/[N]> " % settings_path)
            if not inp.lower() == "y":
                print("Aborted.")
            else:
                print("Reset the settings file.")

        if secret_settings:
            default_settings_path = os.path.join(
                EVENNIA_TEMPLATE, "server", "conf", "secret_settings.py"
            )
        else:
            default_settings_path = os.path.join(EVENNIA_TEMPLATE, "server", "conf", "settings.py")
        shutil.copy(default_settings_path, settings_path)

    with open(settings_path, "r") as f:
        settings_string = f.read()

    settings_string = settings_string.format_map(setting_dict)

    with open(settings_path, "w") as f:
        f.write(settings_string)

def create_game_directory(dirname):
    """
    Initialize a new game directory named dirname
    at the current path. This means copying the
    template directory from evennia's root.

    Args:
        dirname (str): The directory name to create.

    """
    GAMEDIR = os.path.abspath(os.path.join(CURRENT_DIR, dirname))
    if os.path.exists(GAMEDIR):
        print("Cannot create new Evennia game dir: '%s' already exists." % dirname)
    # copy template directory
    shutil.copytree(EVENNIA_TEMPLATE, GAMEDIR)
    # rename gitignore to .gitignore
    os.rename(os.path.join(GAMEDIR, "gitignore"), os.path.join(GAMEDIR, ".gitignore"))

    # pre-build settings file in the new GAMEDIR
    create_settings_file()
    create_settings_file(secret_settings=True)

def create_superuser():
    """
    Create the superuser account

    """
    print(
        "\nCreate a superuser below. The superuser is Account #1, the 'owner' "
        "account of the server. Email is optional and can be empty.\n"
    )
    from os import environ

    username = environ.get("EVENNIA_SUPERUSER_USERNAME")
    email = environ.get("EVENNIA_SUPERUSER_EMAIL")
    password = environ.get("EVENNIA_SUPERUSER_PASSWORD")

    if (username is not None) and (password is not None) and len(password) > 0:
        from evennia.accounts.models import AccountDB

        superuser = AccountDB.objects.create_superuser(username, email, password)
        superuser.save()
    else:
        django.core.management.call_command("createsuperuser", interactive=True)

def check_database(always_return=False):
    """
    Check so the database exists.

    Args:
        always_return (bool, optional): If set, will always return True/False
            also on critical errors. No output will be printed.
    Returns:
        exists (bool): `True` if the database exists, otherwise `False`.

    """
    # Check so a database exists and is accessible
    from django.db import connection

    tables = connection.introspection.get_table_list(connection.cursor())
    if not tables or not isinstance(tables[0], str):  # django 1.8+
        tables = [tableinfo.name for tableinfo in tables]
    if tables and "accounts_accountdb" in tables:
        # database exists and seems set up. Initialize evennia.
        evennia._init()
    # Try to get Account#1
    from evennia.accounts.models import AccountDB

    try:
        AccountDB.objects.get(id=1)
    except (django.db.utils.OperationalError, ProgrammingError) as e:
        if always_return:
        print(ERROR_DATABASE.format(traceback=e))
    except AccountDB.DoesNotExist:
        # no superuser yet. We need to create it.

        other_superuser = AccountDB.objects.filter(is_superuser=True)
        if other_superuser:
            # Another superuser was found, but not with id=1. This may
            # happen if using flush (the auto-id starts at a higher
            # value). Wwe copy this superuser into id=1. To do
            # this we must deepcopy it, delete it then save the copy
            # with the new id. This allows us to avoid the UNIQUE
            # constraint on usernames.
            other = other_superuser[0]
            other_id = other.id
            other_key = other.username
            print(WARNING_MOVING_SUPERUSER.format(other_key=other_key, other_id=other_id))
            res = ""
            while res.upper() != "Y":
                # ask for permission
                res = eval(input("Continue [Y]/N: "))
                if res.upper() == "N":
                elif not res:
                    break
            # continue with the
            from copy import deepcopy

            new = deepcopy(other)
            other.delete()
            new.id = 1
            new.save()
        else:
            create_superuser()
            check_database(always_return=always_return)

def getenv():
    """
    Get current environment and add PYTHONPATH.

    Returns:
        env (dict): Environment global dict.

    """
    sep = ";" if _is_windows() else ":"
    env = os.environ.copy()
    env["PYTHONPATH"] = sep.join(sys.path)
    return env

def get_pid(pidfile, default=None):
    """
    Get the PID (Process ID) by trying to access an PID file.

    Args:
        pidfile (str): The path of the pid file.
        default (int, optional): What to return if file does not exist.

    Returns:
        pid (str): The process id or `default`.

    """
    if os.path.exists(pidfile):
        with open(pidfile, "r") as f:
            pid = f.read()
            return pid

def del_pid(pidfile):
    """
    The pidfile should normally be removed after a process has
    finished, but when sending certain signals they remain, so we need
    to clean them manually.

    Args:

    """
        os.remove(pidfile)

def kill(pidfile, component="Server", callback=None, errback=None, killsignal=SIG):
    """
    Send a kill signal to a process based on PID. A customized
    success/error message will be returned. If clean=True, the system
    will attempt to manually remove the pid file. On Windows, no arguments
    are useful since Windows has no ability to direct signals except to all
    children of a console.

    Args:
        pidfile (str): The path of the pidfile to get the PID from. This is ignored
            on Windows.
        component (str, optional): Usually one of 'Server' or 'Portal'. This is
            ignored on Windows.
        errback (callable, optional): Called if signal failed to send. This
            is ignored on Windows.
        callback (callable, optional): Called if kill signal was sent successfully.
            This is ignored on Windows.
        killsignal (int, optional): Signal identifier for signal to send. This is

    """
    if _is_windows():
        # Windows signal sending is very limited.
        from win32api import GenerateConsoleCtrlEvent, SetConsoleCtrlHandler

        try:
            # Windows can only send a SIGINT-like signal to
            # *every* process spawned off the same console, so we must
            # avoid killing ourselves here.
            SetConsoleCtrlHandler(None, True)
            GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)
            # We must catch and ignore the interrupt sent.
            pass
        print("Sent kill signal to all spawned processes")

    else:
        # Linux/Unix/Mac can send kill signal directly to specific PIDs.
        pid = get_pid(pidfile)
        if pid:
                os.remove(pidfile)
            try:
                os.kill(int(pid), killsignal)
            except OSError:
                print(
                    "{component} ({pid}) cannot be stopped. "
                    "The PID file '{pidfile}' seems stale. "
                    "Try removing it manually.".format(
                        component=component, pid=pid, pidfile=pidfile
                    )
                )
                return
                callback()
            else:
                print("Sent kill signal to {component}.".format(component=component))
                return
            errback()
        else:
            print(
                "Could not send kill signal - {component} does not appear to be running.".format(
                    component=component
                )
            )

def show_version_info(about=False):
    """
    Display version info.

    Args:
        about (bool): Include ABOUT info as well as version numbers.

    Returns:
        version_info (str): A complete version info string.

    """
    import sys

    import twisted

    return VERSION_INFO.format(
        version=EVENNIA_VERSION,
        about=ABOUT_INFO if about else "",
        os=os.name,
        python=sys.version.split()[0],
        twisted=twisted.version.short(),
        django=django.get_version(),
    )

def error_check_python_modules(show_warnings=False):
    """
    Import settings modules in settings. This will raise exceptions on
    pure python-syntax issues which are hard to catch gracefully with
    exceptions in the engine (since they are formatting errors in the
    python source files themselves). Best they fail already here
    before we get any further.

        show_warnings (bool): If non-fatal warning messages should be shown.

    """

    def _imp(path, split=True):
        "helper method"
        mod, fromlist = path, "None"
        if split:
            mod, fromlist = path.rsplit(".", 1)
        __import__(mod, fromlist=[fromlist])

    # check the historical deprecations
    from evennia.server import deprecations

    try:
        deprecations.check_errors(settings)
    except DeprecationWarning as err:

    if show_warnings:
        deprecations.check_warnings(settings)

    # core modules
    _imp(settings.COMMAND_PARSER)
    _imp(settings.SEARCH_AT_RESULT)
    _imp(settings.CONNECTION_SCREEN_MODULE)
    # imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False)
    for path in settings.LOCK_FUNC_MODULES:
        _imp(path, split=False)

    from evennia.commands import cmdsethandler

    if not cmdsethandler.import_cmdset(settings.CMDSET_UNLOGGEDIN, None):
        print("Warning: CMDSET_UNLOGGED failed to load!")
    if not cmdsethandler.import_cmdset(settings.CMDSET_CHARACTER, None):
        print("Warning: CMDSET_CHARACTER failed to load")
    if not cmdsethandler.import_cmdset(settings.CMDSET_ACCOUNT, None):
        print("Warning: CMDSET_ACCOUNT failed to load")
    # typeclasses
    _imp(settings.BASE_ACCOUNT_TYPECLASS)
    _imp(settings.BASE_OBJECT_TYPECLASS)
    _imp(settings.BASE_CHARACTER_TYPECLASS)
    _imp(settings.BASE_ROOM_TYPECLASS)
    _imp(settings.BASE_EXIT_TYPECLASS)
    _imp(settings.BASE_SCRIPT_TYPECLASS)

#
# Options
#

def init_game_directory(path, check_db=True, need_gamedir=True):
    """
    Try to analyze the given path to find settings.py - this defines
    the game directory and also sets PYTHONPATH as well as the django
    path.

    Args:
        path (str): Path to new game directory, including its name.
        check_db (bool, optional): Check if the databae exists.
        need_gamedir (bool, optional): set to False if Evennia doesn't require to
            be run in a valid game directory.

    """
    # set the GAMEDIR path
    if need_gamedir:
        set_gamedir(path)

    # Add gamedir to python path
    sys.path.insert(0, GAMEDIR)

    if TEST_MODE or not need_gamedir:
        if ENFORCED_SETTING:
            print(NOTE_TEST_CUSTOM.format(settings_dotpath=SETTINGS_DOTPATH))
            os.environ["DJANGO_SETTINGS_MODULE"] = SETTINGS_DOTPATH
        else:
            print(NOTE_TEST_DEFAULT)
            os.environ["DJANGO_SETTINGS_MODULE"] = "evennia.settings_default"
    else:
        os.environ["DJANGO_SETTINGS_MODULE"] = SETTINGS_DOTPATH

    # required since django1.7
    django.setup()

    # test existence of the settings module
    try:
    except Exception as ex:
        if not str(ex).startswith("No module named"):
            import traceback

            print(traceback.format_exc().strip())
        print(ERROR_SETTINGS)

    # this will both check the database and initialize the evennia dir.
    if check_db:
        check_database()

    # if we don't have to check the game directory, return right away
    if not need_gamedir:
        return

    # set up the Evennia executables and log file locations
    global AMP_PORT, AMP_HOST, AMP_INTERFACE
    global SERVER_PY_FILE, PORTAL_PY_FILE
    global SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE
    global SERVER_PIDFILE, PORTAL_PIDFILE
    global SPROFILER_LOGFILE, PPROFILER_LOGFILE
    global EVENNIA_VERSION

    AMP_PORT = settings.AMP_PORT
    AMP_HOST = settings.AMP_HOST
    AMP_INTERFACE = settings.AMP_INTERFACE

    SERVER_PY_FILE = os.path.join(EVENNIA_LIB, "server", "server.py")
    PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, "server", "portal", "portal.py")

    SERVER_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "server.pid")
    PORTAL_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "portal.pid")

    SPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "server.prof")
    PPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "portal.prof")

    SERVER_LOGFILE = settings.SERVER_LOG_FILE
    PORTAL_LOGFILE = settings.PORTAL_LOG_FILE
    HTTP_LOGFILE = settings.HTTP_LOG_FILE

    # verify existence of log file dir (this can be missing e.g.
    # if the game dir itself was cloned since log files are in .gitignore)
    logdirs = [
        logfile.rsplit(os.path.sep, 1) for logfile in (SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE)
    ]
    if not all(os.path.isdir(pathtup[0]) for pathtup in logdirs):
        errstr = "\n    ".join(
            "%s (log file %s)" % (pathtup[0], pathtup[1])
            for pathtup in logdirs
            if not os.path.isdir(pathtup[0])
        )
        print(ERROR_LOGDIR_MISSING.format(logfiles=errstr))

        # We need to handle Windows twisted separately. We create a
        # batchfile in game/server, linking to the actual binary

        global TWISTED_BINARY
        # Windows requires us to use the absolute path for the bat file.
        server_path = os.path.dirname(os.path.abspath(__file__))
        TWISTED_BINARY = os.path.join(server_path, "twistd.bat")

        # add path so system can find the batfile
        sys.path.insert(1, os.path.join(GAMEDIR, SERVERDIR))

        try:
            importlib.import_module("win32api")
            print(ERROR_WINDOWS_WIN32API)
            sys.exit()

        batpath = os.path.join(EVENNIA_SERVER, TWISTED_BINARY)
        if not os.path.exists(batpath):
            # Test for executable twisted batch file. This calls the
            # twistd.py executable that is usually not found on the
            # path in Windows.  It's not enough to locate
            # scripts.twistd, what we want is the executable script
            # C:\PythonXX/Scripts/twistd.py. Alas we cannot hardcode
            # this location since we don't know if user has Python in
            # a non-standard location. So we try to figure it out.
            twistd = importlib.import_module("twisted.scripts.twistd")
            twistd_dir = os.path.dirname(twistd.__file__)

            # note that we hope the twistd package won't change here, since we
            # try to get to the executable by relative path.
            # Update: In 2016, it seems Twisted 16 has changed the name of
            # of its executable from 'twistd.py' to 'twistd.exe'.
            twistd_path = os.path.abspath(
                os.path.join(
                    twistd_dir, os.pardir, os.pardir, os.pardir, os.pardir, "scripts", "twistd.exe"
                )
            )

            with open(batpath, "w") as bat_file:
                # build a custom bat file for windows
                bat_file.write('@"%s" %%*' % twistd_path)

            print(INFO_WINDOWS_BATFILE.format(twistd_path=twistd_path))

def run_dummyrunner(number_of_dummies):
    """
    Start an instance of the dummyrunner

    Args:
        number_of_dummies (int): The number of dummy accounts to start.

    Notes:
        The dummy accounts' behavior can be customized by adding a
        `dummyrunner_settings.py` config file in the game's conf/
        directory.

    """
    number_of_dummies = str(int(number_of_dummies)) if number_of_dummies else 1
    cmdstr = [sys.executable, EVENNIA_DUMMYRUNNER, "-N", number_of_dummies]
    config_file = os.path.join(SETTINGS_PATH, "dummyrunner_settings.py")
    if os.path.exists(config_file):
        cmdstr.extend(["--config", config_file])
    try:
        call(cmdstr, env=getenv())
    except KeyboardInterrupt:
        # this signals the dummyrunner to stop cleanly and should
        # not lead to a traceback here.
        pass

def run_connect_wizard():
    """
    Run the linking wizard, for adding new external connections.

    """
    from .connection_wizard import ConnectionWizard, node_start

    wizard = ConnectionWizard()

def list_settings(keys):
    """
    Display the server settings. We only display the Evennia specific
    settings here. The result will be printed to the terminal.

    Args:
        keys (str or list): Setting key or keys to inspect.

    """
    from importlib import import_module

    from evennia.utils import evtable

    evsettings = import_module(SETTINGS_DOTPATH)
    if len(keys) == 1 and keys[0].upper() == "ALL":
        # show a list of all keys
        # a specific key
        table = evtable.EvTable()
        confs = [key for key in sorted(evsettings.__dict__) if key.isupper()]
        for i in range(0, len(confs), 4):
            table.add_row(*confs[i : i + 4])
    else:
        table = evtable.EvTable(width=131)
        keys = [key.upper() for key in keys]
        confs = dict((key, var) for key, var in evsettings.__dict__.items() if key in keys)
        for key, val in confs.items():
            table.add_row(key, str(val))
    print(table)

def run_custom_commands(option, *args):
    """
    Inject a custom option into the evennia launcher command chain.

    Args:
        option (str): Incoming option - the first argument after `evennia` on
            the command line.
        *args: All args will passed to a found callable.__dict__

    Returns:
        bool: If a custom command was found and handled the option.

    Notes:
        Provide new commands in settings with

            CUSTOM_EVENNIA_LAUNCHER_COMMANDS = {"mycmd": "path.to.callable", ...}

        The callable will be passed any `*args` given on the command line and is expected to
        handle/validate the input correctly. Use like any other evennia command option on
        in the terminal/console, for example:

            evennia mycmd foo bar

    """
    import importlib

    try:
        # a dict of {option: callable(*args), ...}
        custom_commands = settings.EXTRA_LAUNCHER_COMMANDS
    cmdpath = custom_commands.get(option)
    if cmdpath:
        modpath, *cmdname = cmdpath.rsplit(".", 1)
        if cmdname:
            cmdname = cmdname[0]
            command = mod.__dict__.get(cmdname)
            if command:
                command(*args)

def run_menu():
    """
    This launches an interactive menu.

    """
        # menu loop
        gamedir = "/{}".format(os.path.basename(GAMEDIR))
        leninfo = len(gamedir)
        line = "|" + " " * (61 - leninfo) + gamedir + " " * 2 + "|"

        print(MENU.format(gameinfo=line))
        inp = input(" option > ")

        # quitting and help
        if inp.lower() == "q":
            return
        elif inp.lower() == "h":
            print(HELP_ENTRY)
            eval(input("press <return> to continue ..."))
            continue
        elif inp.lower() in ("v", "i", "a"):
            print(show_version_info(about=True))
            continue

        # options
        try:
            inp = int(inp)
            print("Not a valid option.")
            continue
        if inp == 1:
            start_evennia(False, False)
        elif inp == 2:
            reload_evennia(False, False)
        elif inp == 3:
            stop_evennia()
        elif inp == 4:
            reboot_evennia(False, False)
        elif inp == 5:
            reload_evennia(False, True)
        elif inp == 6:
            stop_server_only()
        elif inp == 7:
                print("This option is not supported on Windows.")
            else:
                kill(SERVER_PIDFILE, "Server")
        elif inp == 8:
            else:
                kill(PORTAL_PIDFILE, "Portal")
        elif inp == 9:
            if not SERVER_LOGFILE:
                init_game_directory(CURRENT_DIR, check_db=False)
            tail_log_files(PORTAL_LOGFILE, SERVER_LOGFILE, 20, 20)
            print(
                "   Tailing logfiles {} (Ctrl-C to exit) ...".format(
                    _file_names_compact(SERVER_LOGFILE, PORTAL_LOGFILE)
                )
            )
        elif inp == 10:
            query_status()
        elif inp == 11:
            query_info()
        elif inp == 12:
            print("Running 'evennia --settings settings.py test .' ...")
            Popen(
                [sys.executable, __file__, "--settings", "settings.py", "test", "."], env=getenv()
            ).wait()
        elif inp == 13:
            print("Running 'evennia test evennia' ...")
            Popen([sys.executable, __file__, "test", "evennia"], env=getenv()).wait()
        else:
            continue
        return

def main():
    """
    Run the evennia launcher main program.

    """
    # set up argument parser

    parser = ArgumentParser(description=CMDLINE_HELP, formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument(
        "--gamedir",
        nargs=1,
        action="store",
        dest="altgamedir",
        metavar="<path>",
        help="location of gamedir (default: current location)",
    )
        "--init",
        dest="init",
        metavar="<gamename>",
        help="creates a new gamedir 'name' at current location",
    )
        "--log",
        "-l",
        action="store_true",
        dest="tail_log",
        help="tail the portal and server logfiles and print to stdout",
    )
        "--list",
        nargs="+",
        dest="listsetting",
        metavar="all|<key>",
        help="list settings, use 'all' to list all available keys",
    )
        "--settings",
        nargs=1,
        dest="altsettings",
        default=None,
        help=(
            "start evennia with alternative settings file from\n"
            " gamedir/server/conf/. (default is settings.py)"
        ),
    )
        "--initsettings",
        dest="initsettings",
        help="create a new, empty settings file as\n gamedir/server/conf/settings.py",
    )
        "--initmissing",
        dest="initmissing",
        help=(
            "checks for missing secret_settings or server logs\n directory, and adds them if needed"
        ),
    )
        "--profiler",
        dest="profiler",
        help="start given server component under the Python profiler",
    )
        "--dummyrunner",
        nargs=1,
        dest="dummyrunner",
        metavar="<N>",
        help="test a server by connecting <N> dummy accounts to it",
    )
        "-v",
        "--version",
        dest="show_version",
        help="show version info",
    )

    parser.add_argument("operation", nargs="?", default="noop", help=ARG_OPTIONS)
    parser.epilog = (
        "Common Django-admin commands are shell, dbshell, test and migrate.\n"
        "See the Django documentation for more management commands."
    )

    args, unknown_args = parser.parse_known_args()

    # handle arguments
    option = args.operation

    # make sure we have everything
    check_main_evennia_dependencies()

        # show help pane
        print(CMDLINE_HELP)

    if args.altgamedir:
        # use alternative gamedir path
        global GAMEDIR
        altgamedir = args.altgamedir[0]
        if not os.path.isdir(altgamedir) and not args.init:
            print(ERROR_NO_ALT_GAMEDIR.format(gamedir=altgamedir))
        GAMEDIR = altgamedir

    if args.init:
        # initialization of game directory
        create_game_directory(args.init)
        print(
            CREATED_NEW_GAMEDIR.format(
                gamedir=args.init, settings_path=os.path.join(args.init, SETTINGS_PATH)
            )
        )

    if args.show_version:
        # show the version info
        print(show_version_info(option == "help"))

    if args.altsettings:
        # use alternative settings file
        global SETTINGSFILE, SETTINGS_DOTPATH, ENFORCED_SETTING
        sfile = args.altsettings[0]
        SETTINGSFILE = sfile
        ENFORCED_SETTING = True
        SETTINGS_DOTPATH = "server.conf.%s" % sfile.rstrip(".py")
        print("Using settings file '%s' (%s)." % (SETTINGSFILE, SETTINGS_DOTPATH))

    if args.initsettings:
        # create new settings file
        try:
            create_settings_file(init=False)
            print(RECREATED_SETTINGS)
        except IOError:
            print(ERROR_INITSETTINGS)

    if args.initmissing:
        created = False
        try:
            log_path = os.path.join(SERVERDIR, "logs")
            if not os.path.exists(log_path):
                os.makedirs(log_path)
                print(f"    ... Created missing log dir {log_path}.")
                created = True

            settings_path = os.path.join(CONFDIR, "secret_settings.py")
            if not os.path.exists(settings_path):
                create_settings_file(init=False, secret_settings=True)
                print(f"    ... Created missing secret_settings.py file as {settings_path}.")

                print(RECREATED_MISSING)
            else:
                print("    ... No missing resources to create/init. You are good to go.")
            print(ERROR_INITMISSING)

    if args.tail_log:
        # set up for tailing the log files
        global NO_REACTOR_STOP
        NO_REACTOR_STOP = True
        if not SERVER_LOGFILE:
            init_game_directory(CURRENT_DIR, check_db=False)

        # adjust how many lines we show from existing logs
        start_lines1, start_lines2 = 20, 20
        if option not in ("reload", "reset", "noop"):
            start_lines1, start_lines2 = 0, 0

        tail_log_files(PORTAL_LOGFILE, SERVER_LOGFILE, start_lines1, start_lines2)
        print(
            "   Tailing logfiles {} (Ctrl-C to exit) ...".format(
                _file_names_compact(SERVER_LOGFILE, PORTAL_LOGFILE)
            )
        )
    if args.dummyrunner:
        # launch the dummy runner
        init_game_directory(CURRENT_DIR, check_db=True)
        run_dummyrunner(args.dummyrunner[0])
    elif args.listsetting:
        # display all current server settings
        init_game_directory(CURRENT_DIR, check_db=False)
        list_settings(args.listsetting)
    elif option == "menu":
        # launch menu for operation
        run_menu()
    elif option in (
        "status",
        "info",
        "start",
        "istart",
        "ipstart",
        "reload",
        "restart",
        "reboot",
        "reset",
        "stop",
        "sstop",
        "kill",
        "skill",
        "sstart",
        "connections",
    ):
        # operate the server directly
            init_game_directory(CURRENT_DIR, check_db=True)
        if option == "status":
        elif option == "info":
        elif option == "start":
            error_check_python_modules(show_warnings=args.tail_log)
            start_evennia(args.profiler, args.profiler)
        elif option == "istart":
            start_server_interactive()
        elif option == "ipstart":
            start_portal_interactive()
        elif option in ("reload", "restart"):
            reload_evennia(args.profiler)
        elif option == "reboot":
            reboot_evennia(args.profiler, args.profiler)
        elif option == "reset":
            reload_evennia(args.profiler, reset=True)
        elif option == "stop":
        elif option == "sstop":
        elif option == "sstart":
            start_only_server()
        elif option == "kill":
            else:
        elif option == "skill":
            else:
        elif option == "connections":
            run_connect_wizard()

    elif option != "noop":
        # pass-through to django manager, but set things up first
        check_db = False
        need_gamedir = True

        # handle special django commands
        if option in ("runserver", "testserver"):
            # we don't want the django test-webserver
            print(WARNING_RUNSERVER)
        if option in ("makemessages", "compilemessages"):
            # some commands don't require the presence of a game directory to work
            need_gamedir = False
            if CURRENT_DIR != EVENNIA_LIB:
                print(
                    "You must stand in the evennia/evennia/ folder (where the 'locale/' "
                    "folder is located) to run this command."
                )

        if option in ("shell", "check", "makemigrations", "createsuperuser", "shell_plus"):
            # some django commands requires the database to exist,
            # or evennia._init to have run before they work right.
            check_db = True
        if option == "test":
            global TEST_MODE
            TEST_MODE = True

        # init the db/game dir, if needed
        init_game_directory(CURRENT_DIR, check_db=check_db, need_gamedir=need_gamedir)

        if option == "migrate":
            # we need to bypass some checks here for the first db creation
            if not check_database(always_return=True):
                django.core.management.call_command(*([option] + unknown_args))
                sys.exit(0)

        if option in ("createsuperuser",):
            print(
                "Note: Don't create an additional superuser this way. It will not be set up "
                "correctly.\n Instead, use the web admin or the in-game `py` command to "
                "set `is_superuser=True` on a existing Account."
            )

        if run_custom_commands(option, *unknown_args):
            # run any custom commands
        else:
            # pass on to the core django manager - re-parse the entire input line
            # but keep 'evennia' as the name instead of django-admin. This is
            # an exit condition.
            sys.argv[0] = re.sub(r"(-script\.pyw?|\.exe)?$", "", sys.argv[0])
            sys.exit(execute_from_command_line(sys.argv))

    elif not args.tail_log:
        # no input; print evennia info (don't pring if we're tailing log)
        print(ABOUT_INFO)

    if REACTOR_RUN:
        reactor.run()

if __name__ == "__main__":
    # start Evennia from the command line
    main()

"""
The client for sending data to the Evennia Game Index

"""
import platform
import urllib.error
import urllib.parse
import urllib.request

from evennia.utils import get_evennia_version, logger

_EGI_HOST = "http://evennia-game-index.appspot.com"
_EGI_REPORT_PATH = "/api/v1/game/check_in"

class EvenniaGameIndexClient:
    """
    This client class is used for gathering and sending game details to the
    Evennia Game Index. Since EGI is in the early goings, this isn't
    incredibly configurable as far as to what is being sent.
    """

        """
        on_bad_request (callable, optional): Callable to trigger when a bad request was sent.

        """
        self.report_host = _EGI_HOST
        self.report_path = _EGI_REPORT_PATH
        self.report_url = self.report_host + self.report_path
        self.logged_first_connect = False

        self._on_bad_request = on_bad_request
        # Oh, the humanity. Silence the factory start/stop messages.

    def send_game_details(self):
        """
        This is where the magic happens. Send details about the game to the
        Evennia Game Index.
        """
        status_code, response_body = yield self._form_and_send_request()
            if not self.logged_first_connect:
                logger.log_infomsg("Successfully sent game details to Evennia Game Index.")
                self.logged_first_connect = True
            return
        # At this point, either EGD is having issues or the payload we sent
        # is improperly formed (probably due to mis-configuration).
        logger.log_errmsg(
            "Failed to send game details to Evennia Game Index. HTTP "
            "status code was %s. Message was: %s" % (status_code, response_body)
        )

        if status_code == 400 and self._on_bad_request:
            # Improperly formed request. Defer to the callback as far as what
            # to do. Probably not a great idea to continue attempting to send
            # to EGD, though.
            self._on_bad_request()

    def _form_and_send_request(self):
        """
        Build the request to send to the index.

        """
        agent = Agent(reactor, pool=self._conn_pool)
        headers = {
            b"User-Agent": [b"Evennia Game Index Client"],
            b"Content-Type": [b"application/x-www-form-urlencoded"],
        }
        egi_config = settings.GAME_INDEX_LISTING
        # We are using `or` statements below with dict.get() to avoid sending
        # stringified 'None' values to the server.
        try:
            values = {
                # Game listing stuff
                "game_name": egi_config.get("game_name", settings.SERVERNAME),
                "game_status": egi_config["game_status"],
                "game_website": egi_config.get("game_website", ""),
                "short_description": egi_config["short_description"],
                "long_description": egi_config.get("long_description", ""),
                "listing_contact": egi_config["listing_contact"],
                # How to play
                "telnet_hostname": egi_config.get("telnet_hostname", ""),
                "telnet_port": egi_config.get("telnet_port", ""),
                "web_client_url": egi_config.get("web_client_url", ""),
                # Game stats
                "connected_account_count": evennia.SESSION_HANDLER.account_count(),
                "total_account_count": AccountDB.objects.num_total_accounts() or 0,
                # System info
                "evennia_version": get_evennia_version(),
                "python_version": platform.python_version(),
                "django_version": django.get_version(),
                "server_platform": platform.platform(),
            }
        except KeyError as err:
            raise KeyError(f"Error loading GAME_INDEX_LISTING: {err}")

        data = urllib.parse.urlencode(values)

        d = agent.request(
            b"POST",
            bytes(self.report_url, "utf-8"),
            headers=Headers(headers),
            bodyProducer=StringProducer(data),
        )

        d.addCallback(self.handle_egd_response)
        return d

    def handle_egd_response(self, response):
        if 200 <= response.code < 300:
            d = defer.succeed((response.code, "OK"))
        else:
            # Go through the horrifying process of getting the response body
            # out of Twisted's plumbing.
            d = defer.Deferred()
            response.deliverBody(SimpleResponseReceiver(response.code, d))
        return d

    """
    """

        self.buf = ""
        self.d = d

    """
    Used for feeding a request body to the tx HTTP client.
    """

        pass

        pass

    """
    """

"""
Service for integrating the Evennia Game Index client into Evennia.

"""
from twisted.application.service import Service

from .client import EvenniaGameIndexClient

# How many seconds to wait before triggering the first EGI check-in.
_FIRST_UPDATE_DELAY = 10
# How often to sync to the server
_CLIENT_UPDATE_RATE = 60 * 30

class EvenniaGameIndexService(Service):
    """
    Twisted Service that contains a LoopingCall for regularly sending game details
    to the Evennia Game Index.

    """

    # We didn't stick the Evennia prefix on here because it'd get marked as
    # a core system service.
    name = "GameIndexClient"

        self.client = EvenniaGameIndexClient(on_bad_request=self._die_on_bad_request)
        self.loop = LoopingCall(self.client.send_game_details)

    def startService(self):
        super().startService()
        # Check to make sure that the client is configured.
        # Start the loop, but only after a short delay. This allows the
        # portal and the server time to sync up as far as total player counts.
        # Prevents always reporting a count of 0.
        reactor.callLater(_FIRST_UPDATE_DELAY, self.loop.start, _CLIENT_UPDATE_RATE)

    def stopService(self):
        if self.running == 0:
            # reload errors if we've stopped this service.
            return
        super().stopService()
        if self.loop.running:
            self.loop.stop()

    def _die_on_bad_request(self):
        """
        If it becomes apparent that our configuration is generating improperly
        formed messages to EGI, we don't want to keep sending bad messages.
        Stop the service so we're not wasting resources.
        """
        logger.log_infomsg(
            "Shutting down Evennia Game Index client service due to invalid configuration."
        )
        self.stopService()

"""
This module handles initial database propagation, which is only run the first time the game starts.
It will create some default objects (notably give #1 its evennia-specific properties, and create the
Limbo room). It will also hooks, and then perform an initial restart.

Everything starts at handle_setup()
"""

ERROR_NO_SUPERUSER = """
    No superuser exists yet. The superuser is the 'owner' account on
    the Evennia server. Create a new superuser using the command

       evennia createsuperuser

    Follow the prompts, then restart the server.
    """

LIMBO_DESC = _(
    """
Welcome to your new |wEvennia|n-based game! Visit https://www.evennia.com if you need
help, want to contribute, report issues or just join the community.

As a privileged user, write |wbatchcommand tutorial_world.build|n to build
tutorial content. Once built, try |wintro|n for starting help and |wtutorial|n to
play the demo game.
"""
)

WARNING_POSTGRESQL_FIX = """
    PostgreSQL-psycopg2 compatibility fix:
    The in-game channels {chan1}, {chan2} and {chan3} were created,
    but the superuser was not yet connected to them. Please use in
    game commands to connect Account #1 to those channels when first
    logging in.
"""

def _get_superuser_account():
    """
    Get the superuser (created at the command line) and don't take no for an answer.

    Returns:
        Account: The first superuser (User #1).

    Raises:
        AccountDB.DoesNotExist: If the superuser couldn't be found.

    """
    try:
        superuser = AccountDB.objects.get(id=1)
        raise AccountDB.DoesNotExist(ERROR_NO_SUPERUSER)
    return superuser

def create_objects():
    """
    Creates the #1 account and Limbo room.

    """

    logger.log_info("Initial setup: Creating objects (Account #1 and Limbo room) ...")

    # Set the initial User's account object's username on the #1 object.
    # This object is pure django and only holds name, email and password.
    superuser = _get_superuser_account()
    from evennia.objects.models import ObjectDB

    # Create an Account 'user profile' object to hold eventual
    # mud-specific settings for the AccountDB object.
    account_typeclass = settings.BASE_ACCOUNT_TYPECLASS

    # run all creation hooks on superuser (we must do so manually
    # since the manage.py command does not)
    superuser.swap_typeclass(account_typeclass, clean_attributes=True)
    superuser.basetype_setup()
    superuser.at_account_creation()
    superuser.locks.add(
        "examine:perm(Developer);edit:false();delete:false();boot:false();msg:all()"
    )
    # this is necessary for quelling to work correctly.
    superuser.permissions.add("Developer")

    # Limbo is the default "nowhere" starting room

    # Create the in-game god-character for account #1 and set
    # it to exist in Limbo.
    try:
        superuser_character = ObjectDB.objects.get(id=1)
        superuser_character, errors = superuser.create_character(
            key=superuser.username, nohome=True, description=_("This is User #1.")
        )
        if errors:
            raise Exception(str(errors))

    superuser_character.locks.add(
        "examine:perm(Developer);edit:false();delete:false();boot:false();msg:all();puppet:false()"
    )
    # we set this low so that quelling is more useful
    superuser_character.permissions.add("Developer")
    superuser_character.save()

    superuser.attributes.add("_first_login", True)
    superuser.attributes.add("_last_puppet", superuser_character)

    room_typeclass = settings.BASE_ROOM_TYPECLASS
    try:
        limbo_obj = ObjectDB.objects.get(id=2)
        limbo_obj = create.create_object(room_typeclass, _("Limbo"), nohome=True)

    limbo_obj.db_typeclass_path = room_typeclass
    limbo_obj.db.desc = LIMBO_DESC.strip()
    limbo_obj.save()

    # Now that Limbo exists, try to set the user up in Limbo (unless
    # the creation hooks already fixed this).
    if not superuser_character.location:
        superuser_character.location = limbo_obj
    if not superuser_character.home:
        superuser_character.home = limbo_obj

def at_initial_setup():
    """
    Custom hook for users to overload some or all parts of the initial
    setup. Called very last in the sequence. It tries to import and
    srun a module settings.AT_INITIAL_SETUP_HOOK_MODULE and will fail
    silently if this does not exist or fails to load.

    """
    modname = settings.AT_INITIAL_SETUP_HOOK_MODULE
    if not modname:
        return
    try:
        mod = __import__(modname, fromlist=[None])
    except (ImportError, ValueError):
        return
    logger.log_info("Initial setup: Running at_initial_setup() hook.")
    if mod.__dict__.get("at_initial_setup", None):
        mod.at_initial_setup()

    """
    Run collectstatic to make sure all web assets are loaded.

    """
    from django.core.management import call_command

    logger.log_info("Initial setup: Gathering static resources using 'collectstatic'")
    call_command("collectstatic", "--noinput")

def reset_server():
    """
    We end the initialization by resetting the server. This makes sure
    the first login is the same as all the following ones,
    particularly it cleans all caches for the special objects.  It
    also checks so the warm-reset mechanism works as it should.

    """
    if settings.TEST_ENVIRONMENT:
        return
    ServerConfig.objects.conf("server_epoch", time.time())

    logger.log_info("Initial setup complete. Restarting Server once.")
    evennia.SESSION_HANDLER.portal_reset_server()

def handle_setup(last_step=None):
    """
    Main logic for the module. It allows for restarting the
    initialization at any point if one of the modules should crash.

    Args:
        last_step (str, None): The last stored successful step, for starting
            over on errors. None if starting from scratch. If this is 'done',
            the function will exit immediately.

    """
    # setup sequence
    setup_sequence = {
        "create_objects": create_objects,
        "at_initial_setup": at_initial_setup,
        "collectstatic": collectstatic,
        "done": reset_server,
    }

    if last_step in ("done", -1):
        # this means we don't need to handle setup since
        # it already ran sucessfully once. -1 is the legacy
        # value for existing databases.
        return

    # determine the sequence so we can skip ahead
    steps = list(setup_sequence)
    steps = steps[steps.index(last_step) + 1 if last_step is not None else 0 :]

    # step through queue from last completed function. Once completed,
    # the 'done' key should be set.
    for stepname in steps:
        try:
            setup_sequence[stepname]()
            # we re-raise to make sure to stop startup
            raise
        else:
            # save the step
            ServerConfig.objects.conf("last_initial_setup_step", stepname)
            if stepname == "done":
                # always exit on 'done'
                break

"""
Functions for processing input commands.

All global functions in this module whose name does not start with "_"
is considered an inputfunc. Each function must have the following
callsign (where inputfunc name is always lower-case, no matter what the
OOB input name looked like):

    inputfunc(session, *args, **kwargs)

Where "options" is always one of the kwargs, containing eventual
protocol-options.
There is one special function, the "default" function, which is called
on a no-match. It has this callsign:

    default(session, cmdname, *args, **kwargs)

Evennia knows which modules to use for inputfuncs by
settings.INPUT_FUNC_MODULES.

"""

from evennia.commands.cmdhandler import cmdhandler
from evennia.utils.utils import to_str

BrowserSessionStore = importlib.import_module(settings.SESSION_ENGINE).SessionStore

# always let "idle" work since we use this in the webclient
_IDLE_COMMAND = settings.IDLE_COMMAND
_IDLE_COMMAND = (_IDLE_COMMAND,) if _IDLE_COMMAND == "idle" else (_IDLE_COMMAND, "idle")

_STRIP_INCOMING_MXP = settings.MXP_ENABLED and settings.MXP_OUTGOING_ONLY
_STRIP_MXP = None

def _NA(o):
    return "N/A"

def _maybe_strip_incoming_mxp(txt):
    global _STRIP_MXP
    if _STRIP_INCOMING_MXP:
        if not _STRIP_MXP:
            from evennia.utils.ansi import strip_mxp as _STRIP_MXP
        return _STRIP_MXP(txt)
    return txt

_ERROR_INPUT = "Inputfunc {name}({session}): Wrong/unrecognized input: {inp}"

# All global functions are inputfuncs available to process inputs

def text(session, *args, **kwargs):
    """
    Main text input from the client. This will execute a command
    string on the server.

    Args:
        session (Session): The active Session to receive the input.
        text (str): First arg is used as text-command input. Other
            arguments are ignored.

    """

    # from evennia.server.profiling.timetrace import timetrace
    # text = timetrace(text, "ServerSession.data_in")

    txt = args[0] if args else None

    # explicitly check for None since text can be an empty string, which is
    # also valid
    if txt is None:
        return
    # this is treated as a command input
    # handle the 'idle' command
    if txt.strip() in _IDLE_COMMAND:
        session.update_session_counters(idle=True)
        return

    txt = _maybe_strip_incoming_mxp(txt)

    if session.account:
        # nick replacement
        puppet = session.puppet
        if puppet:
            txt = puppet.nicks.nickreplace(txt, categories=("inputline"), include_account=True)
        else:
            txt = session.account.nicks.nickreplace(
                txt, categories=("inputline"), include_account=False
            )
    kwargs.pop("options", None)
    cmdhandler(session, txt, callertype="session", session=session, **kwargs)
    session.update_session_counters()

def bot_data_in(session, *args, **kwargs):
    """
    Text input from the IRC and RSS bots.
    This will trigger the execute_cmd method on the bots in-game counterpart.

    Args:
        text (str): First arg is text input. Other arguments are ignored.

    """

    # Explicitly check for None since text can be an empty string, which is
        return
        return

    # Trigger the execute_cmd method of the corresponding bot.
    session.account.execute_cmd(session=session, txt=txt, **kwargs)

def echo(session, *args, **kwargs):
    """
    Echo test function
    """
        txt = strip_mxp(txt)

    session.data_out(text="Echo returns: %s" % args)

def default(session, cmdname, *args, **kwargs):
    """
    Default catch-function. This is like all other input functions except
    it will get `cmdname` as the first argument.

    """
    err = (
        "Session {sessid}: Input command not recognized:\n"
        " name: '{cmdname}'\n"
        " args, kwargs: {args}, {kwargs}".format(
            sessid=session.sessid, cmdname=cmdname, args=args, kwargs=kwargs
        )
    )
    if session.protocol_flags.get("INPUTDEBUG", False):
        session.msg(err)
    log_err(err)

_CLIENT_OPTIONS = (
    "ANSI",
    "XTERM256",
    "MXP",
    "UTF-8",
    "SCREENREADER",
    "ENCODING",
    "MCCP",
    "SCREENHEIGHT",
    "SCREENWIDTH",
    "INPUTDEBUG",
    "RAW",
    "NOCOLOR",
    "NOGOAHEAD",
    "LOCALECHO",
)

def client_options(session, *args, **kwargs):
    """
    This allows the client an OOB way to inform us about its name and capabilities.
    This will be integrated into the session settings

        get (bool): If this is true, return the settings as a dict
            (ignore all other kwargs).
        client (str): A client identifier, like "mushclient".
        version (str): A client version
        ansi (bool): Supports ansi colors
        xterm256 (bool): Supports xterm256 colors or not
        mxp (bool): Supports MXP or not
        utf-8 (bool): Supports UTF-8 or not
        screenreader (bool): Screen-reader mode on/off
        mccp (bool): MCCP compression on/off
        screenheight (int): Screen height in lines
        screenwidth (int): Screen width in characters
        inputdebug (bool): Debug input functions
        nocolor (bool): Strip color
        raw (bool): Turn off parsing
        localecho (bool): Turn on server-side echo (for clients not supporting it)

    """
    old_flags = session.protocol_flags
    if not kwargs or kwargs.get("get", False):
        # return current settings
        options = dict((key, old_flags[key]) for key in old_flags if key.upper() in _CLIENT_OPTIONS)
        session.msg(client_options=options)
        return

    def validate_encoding(val):
        # helper: change encoding
        try:
            codecs_lookup(val)
        except LookupError:
            raise RuntimeError("The encoding '|w%s|n' is invalid. " % val)
        return val

    def validate_size(val):
        return {0: int(val)}

    def validate_bool(val):
        if isinstance(val, str):
            return True if val.lower() in ("true", "on", "1") else False
        return bool(val)

    flags = {}
    for key, value in kwargs.items():
        if key == "client":
            flags["CLIENTNAME"] = to_str(value)
        elif key == "version":
            if "CLIENTNAME" in flags:
                flags["CLIENTNAME"] = "%s %s" % (flags["CLIENTNAME"], to_str(value))
        elif key == "ENCODING":
            flags["ENCODING"] = validate_encoding(value)
        elif key == "ansi":
            flags["ANSI"] = validate_bool(value)
        elif key == "xterm256":
            flags["XTERM256"] = validate_bool(value)
        elif key == "mxp":
            flags["MXP"] = validate_bool(value)
        elif key == "utf-8":
            flags["UTF-8"] = validate_bool(value)
        elif key == "screenreader":
            flags["SCREENREADER"] = validate_bool(value)
        elif key == "mccp":
            flags["MCCP"] = validate_bool(value)
        elif key == "screenheight":
            flags["SCREENHEIGHT"] = validate_size(value)
        elif key == "screenwidth":
            flags["SCREENWIDTH"] = validate_size(value)
        elif key == "inputdebug":
            flags["INPUTDEBUG"] = validate_bool(value)
        elif key == "nocolor":
            flags["NOCOLOR"] = validate_bool(value)
        elif key == "raw":
            flags["RAW"] = validate_bool(value)
        elif key == "nogoahead":
            flags["NOGOAHEAD"] = validate_bool(value)
        elif key == "localecho":
            flags["LOCALECHO"] = validate_bool(value)
        elif key in (
            "Char 1",
            "Char.Skills 1",
            "Char.Items 1",
            "Room 1",
            "IRE.Rift 1",
            "IRE.Composer 1",
        ):
            # ignore mudlet's default send (aimed at IRE games)
            pass
        elif key not in ("options", "cmdid"):
            err = _ERROR_INPUT.format(name="client_settings", session=session, inp=key)
            session.msg(text=err)

    session.protocol_flags.update(flags)
    # we must update the protocol flags on the portal session copy as well
    session.sessionhandler.session_portal_partial_sync({session.sessid: {"protocol_flags": flags}})

def get_client_options(session, *args, **kwargs):
    """
    Alias wrapper for getting options.
    """
    client_options(session, get=True)

def get_inputfuncs(session, *args, **kwargs):
    """
    Get the keys of all available inputfuncs. Note that we don't get
    it from this module alone since multiple modules could be added.
    So we get it from the sessionhandler.
    """
    inputfuncsdict = dict(
        (key, func.__doc__) for key, func in session.sessionhandler.get_inputfuncs().items()
    )
    session.msg(get_inputfuncs=inputfuncsdict)

def login(session, *args, **kwargs):
    """
    Peform a login. This only works if session is currently not logged
    in. This will also automatically throttle too quick attempts.

        name (str): Account name
        password (str): Plain-text password

    """
    if not session.logged_in and "name" in kwargs and "password" in kwargs:
        from evennia.commands.default.unloggedin import create_normal_account

        account = create_normal_account(session, kwargs["name"], kwargs["password"])

_gettable = {
    "name": lambda obj: obj.key,
    "key": lambda obj: obj.key,
    "location": lambda obj: obj.location.key if obj.location else "None",
    "servername": lambda obj: settings.SERVERNAME,
}

def get_value(session, *args, **kwargs):
    """
    Return the value of a given attribute or db_property on the
    session's current account or character.

      name (str): Name of info value to return. Only names
        in the _gettable dictionary earlier in this module
        are accepted.

    """
    name = kwargs.get("name", "")
    obj = session.puppet or session.account
    if name in _gettable:
        session.msg(get_value={"name": name, "value": _gettable[name](obj)})

def _testrepeat(**kwargs):
    """
    This is a test function for using with the repeat
    inputfunc.

        session (Session): Session to return to.
    """
    import time

    kwargs["session"].msg(repeat="Repeat called: %s" % time.time())

_repeatable = {"test1": _testrepeat, "test2": _testrepeat}  # example only  # "

def repeat(session, *args, **kwargs):
    """
    Call a named function repeatedly. Note that
    this is meant as an example of limiting the number of
    possible call functions.

        callback (str): The function to call. Only functions
            from the _repeatable dictionary earlier in this
            module are available.
        interval (int): How often to call function (s).
            Defaults to once every 60 seconds with a minimum
                of 5 seconds.
        stop (bool): Stop a previously assigned ticker with
            the above settings.

    """

    name = kwargs.get("callback", "")
    interval = max(5, int(kwargs.get("interval", 60)))

    if name in _repeatable:
        if kwargs.get("stop", False):
                interval, _repeatable[name], idstring=session.sessid, persistent=False
            )
        else:
                interval,
                _repeatable[name],
                idstring=session.sessid,
                persistent=False,
            )
    else:
        session.msg("Allowed repeating functions are: %s" % (", ".join(_repeatable)))

def unrepeat(session, *args, **kwargs):
    "Wrapper for OOB use"
    kwargs["stop"] = True
    repeat(session, *args, **kwargs)

_monitorable = {"name": "db_key", "location": "db_location", "desc": "desc"}

def _on_monitor_change(**kwargs):
    fieldname = kwargs["fieldname"]
    obj = kwargs["obj"]
    name = kwargs["name"]
    outputfunc_name = kwargs["outputfunc_name"]

    # the session may be None if the char quits and someone
    # else then edits the object

        callsign = {outputfunc_name: {"name": name, "value": _GA(obj, fieldname)}}
        session.msg(**callsign)

def monitor(session, *args, **kwargs):
    """
    Adds monitoring to a given property or Attribute.

      name (str): The name of the property or Attribute
        to report. No db_* prefix is needed. Only names
        in the _monitorable dict earlier in this module
      stop (bool): Stop monitoring the above name.
      outputfunc_name (str, optional): Change the name of
        the outputfunc name. This is used e.g. by MSDP which
        has its own specific output format.

    """
    from evennia.scripts.monitorhandler import MONITOR_HANDLER

    name = kwargs.get("name", None)
    outputfunc_name = kwargs.get("outputfunc_name", "monitor")
    category = kwargs.get("category", None)
    if name and name in _monitorable and session.puppet:
        field_name = _monitorable[name]
            MONITOR_HANDLER.remove(obj, field_name, idstring=session.sessid)
        else:
            # the handler will add fieldname and obj to the kwargs automatically
            MONITOR_HANDLER.add(
                obj,
                field_name,
                _on_monitor_change,
                name=name,
                outputfunc_name=outputfunc_name,
                category=category,
            )

def unmonitor(session, *args, **kwargs):
    """
    Wrapper for turning off monitoring
    """
    monitor(session, *args, **kwargs)

def monitored(session, *args, **kwargs):
    """
    Report on what is being monitored

    """

    obj = session.puppet
    monitors = MONITOR_HANDLER.all(obj=obj)
    session.msg(monitored=(monitors, {}))

def _on_webclient_options_change(**kwargs):
    """
    Called when the webclient options stored on the account changes.
    Inform the interested clients of this change.
    """
    clientoptions = _GA(obj, fieldname)

        session.msg(webclient_options=clientoptions)

def webclient_options(session, *args, **kwargs):
    """
    Handles retrieving and changing of options related to the webclient.

    If kwargs is empty (or contains just a "cmdid"), the saved options will be
    sent back to the session.
    A monitor handler will be created to inform the client of any future options
    that changes.

    If kwargs is not empty, the key/values stored in there will be persisted
    to the account object.

        <option name>: an option to save
    """
    account = session.account

    clientoptions = account.db._saved_webclient_options
    if not clientoptions:
        # No saved options for this account, copy and save the default.
        account.db._saved_webclient_options = settings.WEBCLIENT_OPTIONS.copy()
        # Get the _SaverDict created by the database.
        clientoptions = account.db._saved_webclient_options

    # The webclient adds a cmdid to every kwargs, but we don't need it.
    try:
        del kwargs["cmdid"]
        pass

    if not kwargs:
        # No kwargs: we are getting the stored options
        # Convert clientoptions to regular dict for sending.
        session.msg(webclient_options=dict(clientoptions))

        # Create a monitor. If a monitor already exists then it will replace
        # the previous one since it would use the same idstring
        from evennia.scripts.monitorhandler import MONITOR_HANDLER

        MONITOR_HANDLER.add(
            account,
            "_saved_webclient_options",
            _on_webclient_options_change,
            idstring=session.sessid,
            persistent=False,
            session=session,
        )
    else:
        # kwargs provided: persist them to the account object.
        clientoptions.update(kwargs)

# OOB protocol-specific aliases and wrappers

# GMCP aliases
hello = client_options
supports_set = client_options

# MSDP aliases (some of the the generic MSDP commands defined in the MSDP spec are prefixed
# by msdp_ at the protocol level)
# See https://tintin.sourceforge.io/protocols/msdp/

def msdp_list(session, *args, **kwargs):
    """
    MSDP LIST command

    """

    args_lower = [arg.lower() for arg in args]
    if "commands" in args_lower:
        inputfuncs = [
            key[5:] if key.startswith("msdp_") else key
            for key in session.sessionhandler.get_inputfuncs().keys()
        ]
        session.msg(commands=(inputfuncs, {}))
    if "lists" in args_lower:
        session.msg(
            lists=(
                [
                    "commands",
                    "lists",
                    "configurable_variables",
                    "reportable_variables",
                    "reported_variables",
                    "sendable_variables",
                ],
                {},
            )
        )
    if "configurable_variables" in args_lower:
        session.msg(configurable_variables=(_CLIENT_OPTIONS, {}))
    if "reportable_variables" in args_lower:
        session.msg(reportable_variables=(_monitorable, {}))
    if "reported_variables" in args_lower:
        monitor_infos = MONITOR_HANDLER.all(obj=obj)
        fieldnames = [tup[1] for tup in monitor_infos]
        session.msg(reported_variables=(fieldnames, {}))
    if "sendable_variables" in args_lower:
        session.msg(sendable_variables=(_monitorable, {}))

def msdp_report(session, *args, **kwargs):
    """
    MSDP REPORT command

    """
    kwargs["outputfunc_name":"report"]

def msdp_unreport(session, *args, **kwargs):
    """
    MSDP UNREPORT command

    """
    unmonitor(session, *args, **kwargs)

def msdp_send(session, *args, **kwargs):
    """
    MSDP SEND command
    """
    out = {}
    for varname in args:
        if varname.lower() in _monitorable:
            out[varname] = _monitorable[varname.lower()]
    session.msg(send=((), out))

# client specific

def _not_implemented(session, *args, **kwargs):
    """
    Dummy used to swallow missing-inputfunc errors for
    common clients.
    """
    pass

# GMCP External.Discord.Hello is sent by Mudlet as a greeting
# (see https://wiki.mudlet.org/w/Manual:Technical_Manual)
external_discord_hello = _not_implemented

# GMCP Client.Gui is sent by Mudlet for gui setup.
client_gui = _not_implemented

"""
Custom manager for ServerConfig objects.
"""

class ServerConfigManager(models.Manager):
    """
    This ServerConfigManager implements methods for searching and
    manipulating ServerConfigs directly from the database.

    directly.

    ServerConfigs are used to store certain persistent settings for
    the server at run-time.

    """

    def conf(self, key=None, value=None, delete=False, default=None):
        """
        Add, retrieve and manipulate config values.

        Args:
            key (str, optional): Name of config.
            value (str, optional): Data to store in this config value.
            delete (bool, optional): If `True`, delete config with `key`.
            default (str, optional): Use when retrieving a config value
                by a key that does not exist.
        Returns:
            all (list): If `key` was not given - all stored config values.
            value (str): If `key` was given, this is the stored value, or
                `default` if no matching `key` was found.

        """
        if not key:
            return self.all()
        elif delete is True:
            for conf in self.filter(db_key=key):
                conf.delete()
            conf = self.filter(db_key=key)
            if conf:
                conf = conf[0]
            else:
                conf = self.model(db_key=key)
            conf.value = value  # this will pickle
        else:
            if not conf:
            return conf[0].value

"""

Server Configuration flags

This holds persistent server configuration flags.

Config values should usually be set through the
manager's conf() method.

"""

from evennia.server.manager import ServerConfigManager
from evennia.utils import logger, picklefield, utils
from evennia.utils.dbserialize import from_pickle, to_pickle
from evennia.utils.idmapper.models import WeakSharedMemoryModel

#
# ServerConfig
#

class ServerConfig(WeakSharedMemoryModel):
    """
    On-the fly storage of global settings.

    Properties defined on ServerConfig:

      - key: Main identifier
      - value: Value stored in key. This is a pickled storage.

    """

    #
    # ServerConfig database model setup
    #
    #
    # named same as the field, but without the db_* prefix.

    # main name of the database entry
    db_key = models.CharField(max_length=64, unique=True)
    # config value
    # db_value = models.BinaryField(blank=True)

    db_value = picklefield.PickledObjectField(
        "value",
        help_text="The data returned when the config value is accessed. Must be "
        "written as a Python literal if editing through the admin "
        "interface. Attribute values which are not Python literals "
        "cannot be edited through the admin interface.",
    )

    objects = ServerConfigManager()

    # key property (wraps db_key)
    def __key_get(self):
        "Getter. Allows for value = self.key"
        return self.db_key

    # @key.setter
    def __key_set(self, value):
        "Setter. Allows for self.key = value"
        self.db_key = value

    # @key.deleter
    def __key_del(self):
        "Deleter. Allows for del self.key. Deletes entry."

    key = property(__key_get, __key_set, __key_del)

    # value property (wraps db_value)
    def __value_get(self):
        "Getter. Allows for value = self.value"
        return from_pickle(self.db_value, db_obj=self)

    # @value.setter
    def __value_set(self, value):
        "Setter. Allows for self.value = value"
        if utils.has_parent("django.db.models.base.Model", value):
            # we have to protect against storing db objects.
            logger.log_err("ServerConfig cannot store db objects! (%s)" % value)
            return
        self.db_value = to_pickle(value)

    # @value.deleter
    def __value_del(self):
        "Deleter. Allows for del self.value. Deletes entry."

    value = property(__value_get, __value_set, __value_del)

        verbose_name = "Server Config value"
        verbose_name_plural = "Server Config values"

    #
    # ServerConfig other methods
    #
        return "<{} {}>".format(self.__class__.__name__, self.key)

    def store(self, key, value):
        """
        Wrap the storage.

        Args:
            key (str): The name of this store.
            value (str): The data to store with this `key`.

        """

"""
The Evennia Portal service acts as an AMP-server, handling AMP
communication to the AMP clients connecting to it (by default
these are the Evennia Server and the evennia launcher).

"""
import os
import sys
from subprocess import STDOUT, Popen

    """

    Returns:

    """
    return env

class AMPServerFactory(protocol.ServerFactory):

    """
    This factory creates AMP Server connection. This acts as the 'Portal'-side communication to the
    'Server' process.

    """

    def logPrefix(self):
        """
        How this is named in logs

        """
        return "AMP"

    def __init__(self, portal):
        """
        Initialize the factory. This is called as the Portal service starts.

        Args:
            portal (Portal): The Evennia Portal service instance.
            protocol (Protocol): The protocol the factory creates
                instances of.

        """
        self.portal = portal
        self.protocol = class_from_module(settings.AMP_SERVER_PROTOCOL_CLASS)
        self.server_connection = None
        self.launcher_connection = None
        self.disconnect_callbacks = {}
        self.server_connect_callbacks = []

        """
        Start a new connection, and store it on the service object.

        Args:

        Returns:
            protocol (Protocol): The created protocol.

        """
        self.portal.amp_protocol = self.protocol()
        self.portal.amp_protocol.factory = self
        return self.portal.amp_protocol

class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
    """
    Protocol subclass for the AMP-server run by the Portal.

    """

        """
        Set up a simple callback mechanism to let the amp-server wait for a connection to close.

        """
        # wipe broadcast and data memory
        super().connectionLost(reason)
        if self.factory.server_connection == self:
            self.factory.server_connection = None
            self.factory.portal.server_info_dict = {}
        if self.factory.launcher_connection == self:
            self.factory.launcher_connection = None

        callback, args, kwargs = self.factory.disconnect_callbacks.pop(self, (None, None, None))
            try:
                callback(*args, **kwargs)

        """
        Return status for the Evennia infrastructure.

        Returns:
            status (tuple): The portal/server status and pids
                (portal_live, server_live, portal_PID, server_PID).

        """
        server_connected = bool(
            self.factory.server_connection and self.factory.server_connection.transport.connected
        )
        portal_info_dict = self.factory.portal.get_info_dict()
        server_info_dict = self.factory.portal.server_info_dict
        server_pid = self.factory.portal.server_process_id
        portal_pid = os.getpid()
        return (True, server_connected, portal_pid, server_pid, portal_info_dict, server_info_dict)

    def data_to_server(self, command, sessid, **kwargs):
        """
        Send data across the wire to the Server.

        Args:
            kwargs (any): Data to send. This will be pickled.

        Returns:

        Notes:

        """
        # print("portal data_to_server: {}, {}, {}".format(command, sessid, kwargs))
        if self.factory.server_connection:
            return self.factory.server_connection.callRemote(
                command, packed_data=amp.dumps((sessid, kwargs))
            ).addErrback(self.errback, command.key)
        else:
            # if no server connection is available, broadcast
            return self.broadcast(command, sessid, packed_data=amp.dumps((sessid, kwargs)))

    def start_server(self, server_twistd_cmd):
        """
        (Re-)Launch the Evennia server.

        Args:
            server_twisted_cmd (list): The server start instruction
                to pass to POpen to start the server.

        """
        # start the Server
        print("Portal starting server ... ")
        process = None
        with open(settings.SERVER_LOG_FILE, "a") as logfile:
            # we link stdout to a file in order to catch
            # eventual errors happening before the Server has
            # opened its logger.
            try:
                if _is_windows():
                    # Windows requires special care
                    create_no_window = 0x08000000
                    process = Popen(
                        server_twistd_cmd,
                        env=getenv(),
                        bufsize=-1,
                        stdout=logfile,
                        stderr=STDOUT,
                        creationflags=create_no_window,
                    )

                else:
                        server_twistd_cmd, env=getenv(), bufsize=-1, stdout=logfile, stderr=STDOUT
                    )

            self.factory.portal.server_twistd_cmd = server_twistd_cmd
            logfile.flush()
        if process and not _is_windows():
            # avoid zombie-process on Unix/BSD
            process.wait()
        return

    def wait_for_disconnect(self, callback, *args, **kwargs):
        """
        Add a callback for when this connection is lost.

        Args:
            callback (callable): Will be called with *args, **kwargs
                once this protocol is disconnected.

        """
        self.factory.disconnect_callbacks[self] = (callback, args, kwargs)

    def wait_for_server_connect(self, callback, *args, **kwargs):
        """
        Add a callback for when the Server is sure to have connected.

        Args:
                once the Server handshake with Portal is complete.

        """
        self.factory.server_connect_callbacks.append((callback, args, kwargs))

    def stop_server(self, mode="shutdown"):
        """
        Shut down server in one or more modes.

        Args:
            mode (str): One of 'shutdown', 'reload' or 'reset'.

        """
        if mode == "reload":
            self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRELOAD)
        elif mode == "reset":
            self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRESET)
        elif mode == "shutdown":
            self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SSHUTD)
        self.factory.portal.server_restart_mode = mode

    # sending amp data

    def send_Status2Launcher(self):
        """
        Send a status stanza to the launcher.

        """
        # print("send status to launcher")
        # print("self.get_status(): {}".format(self.get_status()))
        if self.factory.launcher_connection:
            self.factory.launcher_connection.callRemote(
                amp.MsgStatus, status=amp.dumps(self.get_status())
            ).addErrback(self.errback, amp.MsgStatus.key)

    def send_MsgPortal2Server(self, session, **kwargs):
        """
        Access method called by the Portal and executed on the Portal.

        Args:
            session (session): Session
            kwargs (any, optional): Optional data.

        Returns:
            deferred (Deferred): Asynchronous return.

        """
        return self.data_to_server(amp.MsgPortal2Server, session.sessid, **kwargs)

    def send_AdminPortal2Server(self, session, operation="", **kwargs):
        """
        Send Admin instructions from the Portal to the Server.
        Executed on the Portal.

        Args:
            operation (char, optional): Identifier for the server operation, as defined by the
                global variables in `evennia/server/amp.py`.
            data (str or dict, optional): Data used in the administrative operation.

        """
        return self.data_to_server(
            amp.AdminPortal2Server, session.sessid, operation=operation, **kwargs
        )

    # receive amp data

    def portal_receive_status(self, status):
        """
        Returns run-status for the server/portal.

        Args:
            status (str): Not used.
        Returns:
            status (dict): The status is a tuple
                (portal_running, server_running, portal_pid, server_pid).

        """
        # print('Received PSTATUS request')
        return {"status": amp.dumps(self.get_status())}

    @amp.MsgLauncher2Portal.responder
    def portal_receive_launcher2portal(self, operation, arguments):
        """
        Receives message arriving from evennia_launcher.
        This method is executed on the Portal.

        Args:
            operation (str): The action to perform.
            arguments (str): Possible argument to the instruction, or the empty string.

        Returns:
            result (dict): The result back to the launcher.

        Notes:
            This is the entrypoint for controlling the entire Evennia system from the evennia
            launcher. It can obviously only accessed when the Portal is already up and running.

        """
        # Since the launcher command uses amp.String() we need to convert from byte here.
        operation = str(operation, "utf-8")
        self.factory.launcher_connection = self
        _, server_connected, _, _, _, _ = self.get_status()

        # logger.log_msg("Evennia Launcher->Portal operation %s:%s received" % (ord(operation), arguments))

        # logger.log_msg("operation == amp.SSTART: {}: {}".format(operation == amp.SSTART, amp.loads(arguments)))

        if operation == amp.SSTART:  # portal start  #15
            # first, check if server is already running
            if not server_connected:
                self.wait_for_server_connect(self.send_Status2Launcher)
                self.start_server(amp.loads(arguments))

        elif operation == amp.SRELOAD:  # reload server #14
            if server_connected:
                # We let the launcher restart us once they get the signal
                self.factory.server_connection.wait_for_disconnect(self.send_Status2Launcher)
                self.stop_server(mode="reload")
            else:

        elif operation == amp.SRESET:  # reload server #19
                self.stop_server(mode="reset")
            else:

        elif operation == amp.SSHUTD:  # server-only shutdown #17
                self.stop_server(mode="shutdown")

        elif operation == amp.PSHUTD:  # portal + server shutdown  #16
                self.factory.server_connection.wait_for_disconnect(self.factory.portal.shutdown)
            else:
                self.factory.portal.shutdown()

        else:
            logger.log_err("Operation {} not recognized".format(operation))

        return {}

    @amp.MsgServer2Portal.responder
    def portal_receive_server2portal(self, packed_data):
        """
        Receives message arriving to Portal from Server.

        Args:
            packed_data (str): Pickled data (sessid, kwargs) coming over the wire.

        """
        try:
            sessid, kwargs = self.data_in(packed_data)
            session = evennia.PORTAL_SESSION_HANDLER.get(sessid, None)
                evennia.PORTAL_SESSION_HANDLER.data_out(session, **kwargs)
            logger.log_trace("packed_data len {}".format(len(packed_data)))
        return {}

    @amp.AdminServer2Portal.responder
    def portal_receive_adminserver2portal(self, packed_data):
        """

        Receives and handles admin operations sent to the Portal
        This is executed on the Portal.

        Args:
            packed_data (str): Data received, a pickled tuple (sessid, kwargs).

        """
        self.factory.server_connection = self

        # logger.log_msg("Evennia Server->Portal admin data %s:%s received" % (sessid, kwargs))

        operation = kwargs.pop("operation")
        portal_sessionhandler = evennia.PORTAL_SESSION_HANDLER

        if operation == amp.SLOGIN:  # server_session_login
            # a session has authenticated; sync it.
            session = portal_sessionhandler.get(sessid)
                portal_sessionhandler.server_logged_in(session, kwargs.get("sessiondata"))

        elif operation == amp.SDISCONN:  # server_session_disconnect
            # the server is ordering to disconnect the session
                portal_sessionhandler.server_disconnect(session, reason=kwargs.get("reason"))

        elif operation == amp.SDISCONNALL:  # server_session_disconnect_all
            # server orders all sessions to disconnect
            portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason"))

            self.factory.server_connection.wait_for_disconnect(
                self.start_server, self.factory.portal.server_twistd_cmd
            )
            self.stop_server(mode="reload")

        elif operation == amp.SRESET:  # server reset
            )
            self.stop_server(mode="reset")

        elif operation == amp.SSHUTD:  # server-only shutdown
            self.stop_server(mode="shutdown")

        elif operation == amp.PSHUTD:  # full server+server shutdown
            self.factory.server_connection.wait_for_disconnect(self.factory.portal.shutdown)

        elif operation == amp.PSYNC:  # portal sync
            # Server has (re-)connected and wants the session data from portal
            self.factory.portal.server_info_dict = kwargs.get("info_dict", {})
            self.factory.portal.server_process_id = kwargs.get("spid", None)
            # this defaults to 'shutdown' or whatever value set in server_stop
            server_restart_mode = self.factory.portal.server_restart_mode

            sessdata = evennia.PORTAL_SESSION_HANDLER.get_all_sync_data()
            self.send_AdminPortal2Server(
                amp.DUMMYSESSION,
                amp.PSYNC,
                server_restart_mode=server_restart_mode,
                sessiondata=sessdata,
                portal_start_time=self.factory.portal.start_time,
            )
            evennia.PORTAL_SESSION_HANDLER.at_server_connection()

            if self.factory.server_connection:
                # this is an indication the server has successfully connected, so
                # we trigger any callbacks (usually to tell the launcher server is up)
                for callback, args, kwargs in self.factory.server_connect_callbacks:
                    try:
                        callback(*args, **kwargs)
                self.factory.server_connect_callbacks = []

        elif operation == amp.SSYNC:  # server_session_sync
            # server wants to save session data to the portal,
            # maybe because it's about to shut down.
            portal_sessionhandler.server_session_sync(
                kwargs.get("sessiondata"), kwargs.get("clean", True)
            )

            # set a flag in case we are about to shut down soon
            self.factory.server_restart_mode = True

        elif operation == amp.SCONN:  # server_force_connection (for irc/etc)
            portal_sessionhandler.server_connect(**kwargs)

        else:
        return {}

"""
Implements Discord chat channel integration.

The Discord API uses a mix of websockets and REST API endpoints.

In order for this integration to work, you need to have your own
discord bot set up via https://discord.com/developers/applications
with the MESSAGE CONTENT toggle switched on, and your bot token
added to `server/conf/secret_settings.py` as your  DISCORD_BOT_TOKEN
"""
import os

from autobahn.twisted.websocket import (
    WebSocketClientFactory,
    WebSocketClientProtocol,
    connectWS,
)
from twisted.internet import protocol, reactor, ssl, task
from twisted.web.client import Agent, FileBodyProducer, HTTPConnectionPool, readBody

from evennia.server.session import Session
from evennia.utils import class_from_module, get_evennia_version, logger
from evennia.utils.utils import delay

_BASE_SESSION_CLASS = class_from_module(settings.BASE_SESSION_CLASS)

DISCORD_API_VERSION = 10
# include version number to prevent automatically updating to breaking changes
DISCORD_API_BASE_URL = f"https://discord.com/api/v{DISCORD_API_VERSION}"

DISCORD_USER_AGENT = f"Evennia (https://www.evennia.com, {get_evennia_version(mode='short')})"
DISCORD_BOT_TOKEN = settings.DISCORD_BOT_TOKEN
DISCORD_BOT_INTENTS = settings.DISCORD_BOT_INTENTS

# Discord OP codes, alphabetic
OP_DISPATCH = 0
OP_HEARTBEAT = 1
OP_HEARTBEAT_ACK = 11
OP_HELLO = 10
OP_IDENTIFY = 2
OP_INVALID_SESSION = 9
OP_RECONNECT = 7
OP_RESUME = 6

# create quiet HTTP pool to muffle GET/POST requests
class QuietConnectionPool(HTTPConnectionPool):
    """
    A quiet version of the HTTPConnectionPool which sets the factory's
    `noisy` property to False to muffle log output.
    """

    def __init__(self, reactor, persistent=True):
        super().__init__(reactor, persistent)
        self._factory.noisy = False

_AGENT = Agent(reactor, pool=QuietConnectionPool(reactor))

def should_retry(status_code):
    """
    Helper function to check if the request should be retried later.

    Args:
        status_code (int) - The HTTP status code

    Returns:
        retry (bool) - True if request should be retried False otherwise
    """
    if status_code >= 500 and status_code <= 504:
        # these are common server error codes when the server is temporarily malfunctioning
        # in these cases, we should retry
    else:
        # handle all other cases; this can be expanded later if needed for special cases

class DiscordWebsocketServerFactory(WebSocketClientFactory, protocol.ReconnectingClientFactory):
    """
    A customized websocket client factory that navigates the Discord gateway process.

    """

    maxDelay = 60
    gateway = None
    resume_url = None
    is_connecting = False

    def __init__(self, sessionhandler, *args, **kwargs):
        self.uid = kwargs.get("uid")
        self.sessionhandler = sessionhandler
        self.port = None
        self.bot = None

    def get_gateway_url(self, *args, **kwargs):
        # get the websocket gateway URL from Discord
        d = _AGENT.request(
            b"GET",
            f"{DISCORD_API_BASE_URL}/gateway".encode("utf-8"),
            Headers(
                {
                    "User-Agent": [DISCORD_USER_AGENT],
                    "Authorization": [f"Bot {DISCORD_BOT_TOKEN}"],
                    "Content-Type": ["application/json"],
                }
            ),
            None,
        )

        def cbResponse(response):
            if response.code == 200:
                d = readBody(response)
                d.addCallback(self.websocket_init, *args, **kwargs)
                return d
            else:
                logger.log_warn("Discord gateway request failed.")

        d.addCallback(cbResponse)

    def websocket_init(self, payload, *args, **kwargs):
        """
        callback for when the URL is gotten
        """
        data = json.loads(str(payload, "utf-8"))
        self.is_connecting = False
        if url := data.get("url"):
            self.gateway = f"{url}/?v={DISCORD_API_VERSION}&encoding=json".encode("utf-8")
            useragent = kwargs.pop("useragent", DISCORD_USER_AGENT)
            headers = kwargs.pop(
                "headers",
                {
                },
            )

            logger.log_info("Connecting to Discord Gateway...")
            WebSocketClientFactory.__init__(
                self, url, *args, headers=headers, useragent=useragent, **kwargs
            )
            self.start()
        else:
            logger.log_err("Discord did not return a websocket URL; connection cancelled.")

        """
        Build new instance of protocol

        Args:
            addr (str): Not used, using factory/settings data

        """
        if hasattr(settings, "DISCORD_SESSION_CLASS"):
            protocol_class = class_from_module(
                settings.DISCORD_SESSION_CLASS, fallback=DiscordClient
            )
            protocol = protocol_class()
        else:
            protocol = DiscordClient()

        protocol.factory = self
        protocol.sessionhandler = self.sessionhandler
        return protocol

        """
        Tracks reconnections for debugging.

        Args:
            connector (Connector): Represents the connection.

        """
        logger.log_info("Connecting to Discord...")

        """
        Force a reconnection of the bot protocol. This requires
        de-registering the session and then reattaching a new one.

        """
        # set up the reconnection
        if self.resume_url:
            self.url = self.resume_url
        elif self.gateway:
            self.url = self.gateway
        else:
            # we don't know where to reconnect to! we'll start from the beginning
            self.url = None
        # reset the internal delay, since this is a deliberate disconnect
        self.delay = self.initialDelay
        # disconnect to allow the reconnection process to kick in
        self.bot.sendClose()
        self.sessionhandler.server_disconnect(self.bot)

        "Connect protocol to remote server"

        if not self.gateway:
            # we don't know where to connect to
            # get the gateway URL from Discord
            self.is_connecting = True
            self.get_gateway_url()
        elif not self.is_connecting:
            # everything is good, connect
            connectWS(self)

class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS):
    """
    Implements the Discord client
    """

    nextHeartbeatCall = None
    pending_heartbeat = False
    heartbeat_interval = None
    last_sequence = 0
    session_id = None
    discord_id = None

        WebSocketClientProtocol.__init__(self)
        _BASE_SESSION_CLASS.__init__(self)

    def at_login(self):
        pass

    def onOpen(self):
        """
        Called when connection is established.

        """
        logger.log_msg("Discord connection established.")
        self.factory.bot = self

        self.init_session("discord", "discord.gg", self.factory.sessionhandler)
        self.uid = int(self.factory.uid)
        self.logged_in = True
        self.sessionhandler.connect(self)

    def onMessage(self, payload, isBinary):
        """
        Callback fired when a complete WebSocket message was received.

        Args:
            payload (bytes): The WebSocket message received.
            isBinary (bool): Flag indicating whether payload is binary or
                             UTF-8 encoded text.

        """
        if isBinary:
            logger.log_info("DISCORD: got a binary payload for some reason")
            return
        if seqid := data.get("s"):
            self.last_sequence = seqid

        # not sure if that error json format is for websockets, so
        # check for it just in case
        if "errors" in data:
            self.handle_error(data)
            return

        # check for discord gateway API op codes first
        if data["op"] == OP_HELLO:
            self.interval = data["d"]["heartbeat_interval"] / 1000  # convert millisec to seconds
            if self.nextHeartbeatCall:
                self.nextHeartbeatCall.cancel()
            self.nextHeartbeatCall = self.factory._batched_timer.call_later(
                self.interval * random(),
                self.doHeartbeat,
            )
            if self.session_id:
                # we already have a session; try to resume instead
                self.resume()
            else:
                self.identify()
        elif data["op"] == OP_HEARTBEAT_ACK:
            # our last heartbeat was acknowledged, so reset the "pending" flag
            self.pending_heartbeat = False
        elif data["op"] == OP_HEARTBEAT:
            # Discord wants us to send a heartbeat immediately
            self.doHeartbeat(force=True)
        elif data["op"] == OP_INVALID_SESSION:
            # Discord doesn't like our current session; reconnect for a new one
            logger.log_msg("Discord: received 'Invalid Session' opcode. Reconnecting.")
            if data["d"] == False:
                # can't resume, clear existing resume data
                self.session_id = None
                self.factory.resume_url = None
            self.factory.reconnect()
        elif data["op"] == OP_RECONNECT:
            # reconnect as requested; Discord does this regularly for server load balancing
            logger.log_msg("Discord: received 'Reconnect' opcode. Reconnecting.")
        elif data["op"] == OP_DISPATCH:
            # handle the general dispatch opcode events by type
            if data["t"] == "READY":
                # our recent identification is valid; process new session info
                self.connection_ready(data["d"])
            else:
                # general message, pass on to data_in
                self.data_in(data=data)

    def onClose(self, wasClean, code=None, reason=None):
        """
        This is executed when the connection is lost for whatever
        reason. it can also be called directly, from the disconnect
        method.

        Args:
            wasClean (bool): ``True`` if the WebSocket was closed cleanly.
            code (int or None): Close status as sent by the WebSocket peer.
            reason (str or None): Close reason as sent by the WebSocket peer.

        """
        self.sessionhandler.disconnect(self)
        if self.nextHeartbeatCall:
            self.nextHeartbeatCall.cancel()
            self.nextHeartbeatCall = None
        if wasClean:
            logger.log_info(f"Discord connection closed ({code}) reason: {reason}")
        else:
            logger.log_info(f"Discord connection lost.")

    def _send_json(self, data):
        """
        Post JSON data to the websocket

        Args:
            data (dict): content to send.

        """
        return self.sendMessage(json.dumps(data).encode("utf-8"))

    def _post_json(self, url, data, **kwargs):
        """
        Post JSON data to a REST API endpoint

        Args:
            url (str) - The API path which is being posted to
            data (dict) - Content to be sent
        """
        url = f"{DISCORD_API_BASE_URL}/{url}"
        body = FileBodyProducer(BytesIO(json.dumps(data).encode("utf-8")))
        request_type = kwargs.pop("type", "POST")

            request_type.encode("utf-8"),
            url.encode("utf-8"),
            Headers(
                {
                }
            ),
            body,
        )

            if response.code == 200 or response.code == 204:
                d.addCallback(self.post_response)
                return d
            elif should_retry(response.code):
                delay(300, self._post_json, url, data, **kwargs)

    def post_response(self, body, **kwargs):
        """
        Process the response from sending a POST request

        Args:
            body (bytes) - The post response body
        """
        data = json.loads(body)

    def handle_error(self, data, **kwargs):
        """
        General hook for processing errors.

        Args:
            data (dict) - The received error data

        """
        logger.log_err(str(data))

    def resume(self):
        """
        Called after a reconnection to re-identify and replay missed events

        """
        if not self.last_sequence or not self.session_id:
            # we have no known state to resume from, identify normally
            self.identify()

        # build a RESUME request for Discord and send it
        data = {
            "op": OP_RESUME,
            "d": {
                "token": DISCORD_BOT_TOKEN,
                "session_id": self.session_id,
                "s": self.sequence_id,
            },
        }
        self._send_json(data)

    def disconnect(self, reason=None):
        """
        Generic hook for the engine to call in order to
        disconnect this protocol.

        Args:
            reason (str or None): Motivation for the disconnection.

        """
        self.sendClose(self.CLOSE_STATUS_CODE_NORMAL, reason)

    def identify(self, *args, **kwargs):
        """
        Send Discord authentication. This should be sent once heartbeats begin.

        """
        data = {
            "op": 2,
            "d": {
                "intents": DISCORD_BOT_INTENTS,
                "properties": {
                    "os": os.name,
                    "browser": DISCORD_USER_AGENT,
                    "device": DISCORD_USER_AGENT,
                },
            },
        }

    def connection_ready(self, data):
        """
        Process READY data for relevant bot info.
        """
        self.factory.resume_url = data["resume_gateway_url"]
        self.session_id = data["session_id"]
        self.discord_id = data["user"]["id"]

    def doHeartbeat(self, *args, **kwargs):
        """
        Send heartbeat to Discord.

        """
        if not self.pending_heartbeat or kwargs.get("force"):
            # send the heartbeat
            data = {"op": 1, "d": self.last_sequence}
            self._send_json(data)
            # track that we sent a heartbeat, in case we don't receive an ACK
            self.pending_heartbeat = True
                self.interval,
            )
        else:
            # we didn't get a response since the last heartbeat; reconnect

    def send_channel(self, text, channel_id, **kwargs):
        """
        Send a message from an Evennia channel to a Discord channel.

        Use with session.msg(channel=(message, channel, sender))

        """

        data = {"content": text}
        data.update(kwargs)
        self._post_json(f"channels/{channel_id}/messages", data)

    def send_nickname(self, text, guild_id, user_id, **kwargs):
        """
        Changes a user's nickname on a Discord server.

        Use with session.msg(nickname=(new_nickname, guild_id, user_id))
        """

        data = {"nick": text}
        self._post_json(f"guilds/{guild_id}/members/{user_id}", data, type="PATCH")

    def send_role(self, role_id, guild_id, user_id, **kwargs):
        data = kwargs
        self._post_json(f"guilds/{guild_id}/members/{user_id}/roles/{role_id}", data, type="PUT")

    def send_default(self, *args, **kwargs):
        """
        Ignore other outputfuncs

        """
        pass

    def data_in(self, data, **kwargs):
        """
        Process incoming data from Discord and sent to the Evennia server

        Args:
            data (dict): Converted json data.

        """
        action_type = data.get("t", "UNKNOWN")

        if action_type == "MESSAGE_CREATE":
            # someone posted a message on Discord that the bot can see
            data = data["d"]
            if data["author"]["id"] == self.discord_id:
                # it's by the bot itself! disregard
                return
            message = data["content"]
            channel_id = data["channel_id"]
            keywords = {"channel_id": channel_id}
            if "guild_id" in data:
                # message received to a Discord channel
                keywords["type"] = "channel"
                author = data["member"]["nick"] or data["author"]["username"]
                author_id = data["author"]["id"]
                keywords["sender"] = (author_id, author)
                keywords["guild_id"] = data["guild_id"]

            else:
                # message sent directly to the bot account via DM
                keywords["type"] = "direct"
                author = data["author"]["username"]

            # pass the processed data to the server
            self.sessionhandler.data_in(self, bot_data_in=(message, keywords))

        elif action_type in ("GUILD_CREATE", "GUILD_UPDATE"):
            # we received the current status of a guild the bot is on; process relevant info
            keywords = {"type": "guild", "guild_id": data["id"], "guild_name": data["name"]}
            keywords["channels"] = {
                chan["id"]: {"name": chan["name"], "guild": data["name"]}
                for chan in data["channels"]
                if chan["type"] == 0
            }
            # send the possibly-updated guild and channel data to the server
            self.sessionhandler.data_in(self, bot_data_in=("", keywords))

        elif "DELETE" in action_type:
            # deletes should possibly be handled separately to check for channel removal
            # for now, just ignore
            pass

        else:
            # send the data for any other action types on to the bot as-is for optional server-side handling
            keywords = {"type": action_type}
            keywords.update(data["d"])

"""
Grapevine network connection

This is an implementation of the Grapevine Websocket protocol v 1.0.0 as
outlined here: https://grapevine.haus/docs

This will allow the linked game to transfer status as well as connects
the grapevine client to in-game channels.

"""

)

from evennia.utils import get_evennia_version
from evennia.utils.logger import log_err, log_info

# There is only one at this time
GRAPEVINE_URI = "wss://grapevine.haus/socket"

GRAPEVINE_CLIENT_ID = settings.GRAPEVINE_CLIENT_ID
GRAPEVINE_CLIENT_SECRET = settings.GRAPEVINE_CLIENT_SECRET
GRAPEVINE_CHANNELS = settings.GRAPEVINE_CHANNELS

# defined error codes
CLOSE_NORMAL = 1000
GRAPEVINE_AUTH_ERROR = 4000
GRAPEVINE_HEARTBEAT_FAILURE = 4001

class RestartingWebsocketServerFactory(WebSocketClientFactory, protocol.ReconnectingClientFactory):
    """
    A variant of the websocket-factory that auto-reconnects.

    """

        self.uid = kwargs.pop("uid")
        self.channel = kwargs.pop("grapevine_channel")

        # self.noisy = False

        WebSocketClientFactory.__init__(self, GRAPEVINE_URI, *args, **kwargs)

        """

        Args:

        """
        protocol = GrapevineClient()
        protocol.channel = self.channel

        """

        Args:

        """
        log_info("(re)connecting to grapevine channel '%s'" % self.channel)

        """
        Called when Client failed to connect.

        Args:
            connector (Connection): Represents the connection.
            reason (str): The reason for the failure.

        """

        """
        Called when Client loses connection.

        Args:

        """
        if not (self.bot or (self.bot and self.bot.stopping)):
            self.retry(connector)

        """
        de-registering the session and then reattaching a new one,
        otherwise you end up with an ever growing number of bot
        sessions.

        """
        self.bot.stopping = True
        self.bot.transport.loseConnection()
        self.start()

        try:
            from twisted.internet import ssl
            log_err("To use Grapevine, The PyOpenSSL module must be installed.")
        else:
            context_factory = ssl.ClientContextFactory() if self.isSecure else None
            connectWS(self, context_factory)
            # service.name = "websocket/grapevine"
            # self.sessionhandler.portal.services.addService(service)

class GrapevineClient(WebSocketClientProtocol, Session):
    """
    Implements the grapevine client
    """

        Session.__init__(self)
        self.restart_downtime = None

        pass

        """

        """
        self.restart_task = None

        self.stopping = False

        self.init_session("grapevine", GRAPEVINE_URI, self.factory.sessionhandler)

        self.send_authenticate()

        """

        Args:

        """
        if not isBinary:
            data = json.loads(str(payload, "utf-8"))
            self.data_in(data=data)
            self.retry_task = None

        """
        method.

        Args:

        """
        self.disconnect(reason)

        if code == GRAPEVINE_HEARTBEAT_FAILURE:
            log_err("Grapevine connection lost (Heartbeat error)")
        elif code == GRAPEVINE_AUTH_ERROR:
            log_err("Grapevine connection lost (Auth error)")
        elif self.restart_downtime:
            # server previously warned us about downtime and told us to be
            # ready to reconnect.
            log_info("Grapevine connection lost (Server restart).")

        """
        Send (json-) data to client.

        Args:
            data (str): Text to send.

        """

        """

        Args:

        """
        # autobahn-python: 1000 for a normal close, 3000-4999 for app. specific,
        # in case anyone wants to expose this functionality later.
        #
        # sendClose() under autobahn/websocket/interfaces.py
        self.sendClose(CLOSE_NORMAL, reason)

    # send_* method are automatically callable through .msg(heartbeat={}) etc

    def send_authenticate(self, *args, **kwargs):
        """
        Send grapevine authentication. This should be send immediately upon connection.

        """
        data = {
            "event": "authenticate",
            "payload": {
                "client_id": GRAPEVINE_CLIENT_ID,
                "client_secret": GRAPEVINE_CLIENT_SECRET,
                "supports": ["channels"],
                "channels": GRAPEVINE_CHANNELS,
                "version": "1.0.0",
                "user_agent": get_evennia_version("pretty"),
            },
        }
        # override on-the-fly

    def send_heartbeat(self, *args, **kwargs):
        """
        Send heartbeat to remote grapevine server.

        """
        # pass along all connected players
        data = {"event": "heartbeat", "payload": {}}
        sessions = self.sessionhandler.get_sessions(include_unloggedin=False)
        data["payload"]["players"] = [
            sess.account.key for sess in sessions if hasattr(sess, "account")
        ]

    def send_subscribe(self, channelname, *args, **kwargs):
        """
        Subscribe to new grapevine channel

        Use with session.msg(subscribe="channelname")
        """
        data = {"event": "channels/subscribe", "payload": {"channel": channelname}}

    def send_unsubscribe(self, channelname, *args, **kwargs):
        """
        Un-subscribe to a grapevine channel

        Use with session.msg(unsubscribe="channelname")
        """
        data = {"event": "channels/unsubscribe", "payload": {"channel": channelname}}

    def send_channel(self, text, channel, sender, *args, **kwargs):
        """
        Send text type Evennia -> grapevine

        This is the channels/send message type

        """

        data = {
            "event": "channels/send",
            "payload": {"message": text, "channel": channel, "name": sender},
        }

        """

        """
        pass

        """
        Send data grapevine -> Evennia

        """
        event = data["event"]
        if event == "authenticate":
            # server replies to our auth handshake
            if data["status"] != "success":
                log_err("Grapevine authentication failed.")
                self.disconnect()
            else:
                log_info("Connected and authenticated to Grapevine network.")
        elif event == "heartbeat":
            # server sends heartbeat - we have to send one back
            self.send_heartbeat()
        elif event == "restart":
            # set the expected downtime
            self.restart_downtime = data["payload"]["downtime"]
        elif event == "channels/subscribe":
            # subscription verification
            if data.get("status", "success") == "failure":
                err = data.get("error", "N/A")
                self.sessionhandler.data_in(
                    bot_data_in=((f"Grapevine error: {err}"), {"event": event})
                )
        elif event == "channels/unsubscribe":
            # unsubscribe-verification
            pass
        elif event == "channels/broadcast":
            # incoming broadcast from network
            payload = data["payload"]

            # print("channels/broadcast:", payload["channel"], self.channel)
            if str(payload["channel"]) != self.channel:
                # only echo from channels this particular bot actually listens to
                return
            else:
                # correct channel
                    self,
                    bot_data_in=(
                        str(payload["message"]),
                        {
                            "event": event,
                            "grapevine_channel": str(payload["channel"]),
                            "sender": str(payload["name"]),
                            "game": str(payload["game"]),
                        },
                    ),
                )
        elif event == "channels/send":
            pass
        else:
            self.sessionhandler.data_in(self, bot_data_in=("", kwargs))

"""
This connects to an IRC network/channel and launches an 'bot' onto it.
The bot then pipes what is being said between the IRC channel and one or
more Evennia channels.
"""

import re

from twisted.internet import protocol, reactor
from twisted.words.protocols import irc

from evennia.utils import ansi, logger, utils

# IRC colors

IRC_BOLD = "\002"
IRC_COLOR = "\003"
IRC_RESET = "\017"
IRC_ITALIC = "\026"
IRC_INVERT = "\x16"
IRC_NORMAL = "99"
IRC_UNDERLINE = "37"

IRC_WHITE = "0"
IRC_BLACK = "1"
IRC_DBLUE = "2"
IRC_DGREEN = "3"
IRC_RED = "4"
IRC_DRED = "5"
IRC_DMAGENTA = "6"
IRC_DYELLOW = "7"
IRC_YELLOW = "8"
IRC_GREEN = "9"
IRC_DCYAN = "10"
IRC_CYAN = "11"
IRC_BLUE = "12"
IRC_MAGENTA = "13"
IRC_DGREY = "14"
IRC_GREY = "15"

# obsolete test:

# test evennia->irc:
# |rred |ggreen |yyellow |bblue |mmagenta |ccyan |wwhite |xdgrey
# |Rdred |Gdgreen |Ydyellow |Bdblue |Mdmagenta |Cdcyan |Wlgrey |Xblack
# |[rredbg |[ggreenbg |[yyellowbg |[bbluebg |[mmagentabg |[ccyanbg |[wlgreybg |[xblackbg

# test irc->evennia
# Use Ctrl+C <num> to produce mIRC colors in e.g. irssi

IRC_COLOR_MAP = dict(
    (
        (r"|n", IRC_COLOR + IRC_NORMAL),  # normal mode
        (r"|H", IRC_RESET),  # un-highlight
        (r"|/", "\n"),  # line break
        (r"|t", "    "),  # tab
        (r"|-", "    "),  # fixed tab
        (r"|_", " "),  # space
        (r"|*", IRC_INVERT),  # invert
        (r"|^", ""),  # blinking text
        (r"|h", IRC_BOLD),  # highlight, use bold instead
        (r"|r", IRC_COLOR + IRC_RED),
        (r"|g", IRC_COLOR + IRC_GREEN),
        (r"|y", IRC_COLOR + IRC_YELLOW),
        (r"|b", IRC_COLOR + IRC_BLUE),
        (r"|m", IRC_COLOR + IRC_MAGENTA),
        (r"|c", IRC_COLOR + IRC_CYAN),
        (r"|w", IRC_COLOR + IRC_WHITE),  # pure white
        (r"|x", IRC_COLOR + IRC_DGREY),  # dark grey
        (r"|R", IRC_COLOR + IRC_DRED),
        (r"|G", IRC_COLOR + IRC_DGREEN),
        (r"|Y", IRC_COLOR + IRC_DYELLOW),
        (r"|B", IRC_COLOR + IRC_DBLUE),
        (r"|M", IRC_COLOR + IRC_DMAGENTA),
        (r"|C", IRC_COLOR + IRC_DCYAN),
        (r"|W", IRC_COLOR + IRC_GREY),  # light grey
        (r"|X", IRC_COLOR + IRC_BLACK),  # pure black
        (r"|[r", IRC_COLOR + IRC_NORMAL + "," + IRC_DRED),
        (r"|[g", IRC_COLOR + IRC_NORMAL + "," + IRC_DGREEN),
        (r"|[y", IRC_COLOR + IRC_NORMAL + "," + IRC_DYELLOW),
        (r"|[b", IRC_COLOR + IRC_NORMAL + "," + IRC_DBLUE),
        (r"|[m", IRC_COLOR + IRC_NORMAL + "," + IRC_DMAGENTA),
        (r"|[c", IRC_COLOR + IRC_NORMAL + "," + IRC_DCYAN),
        (r"|[w", IRC_COLOR + IRC_NORMAL + "," + IRC_GREY),  # light grey background
        (r"|[x", IRC_COLOR + IRC_NORMAL + "," + IRC_BLACK),  # pure black background
    )
)
# ansi->irc
RE_ANSI_COLOR = re.compile(r"|".join([re.escape(key) for key in IRC_COLOR_MAP.keys()]), re.DOTALL)
RE_MXP = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
RE_ANSI_ESCAPES = re.compile(r"(%s)" % "|".join(("{{", "%%", "\\\\")), re.DOTALL)
# irc->ansi
_CLR_LIST = [
    re.escape(val) for val in sorted(IRC_COLOR_MAP.values(), key=len, reverse=True) if val.strip()
]
_CLR_LIST = _CLR_LIST[-2:] + _CLR_LIST[:-2]
RE_IRC_COLOR = re.compile(r"|".join(_CLR_LIST), re.DOTALL)
ANSI_COLOR_MAP = dict((tup[1], tup[0]) for tup in IRC_COLOR_MAP.items() if tup[1].strip())

def parse_ansi_to_irc(string):
    """
    Parse |-type syntax and replace with IRC color markers

    Args:
        string (str): String to parse for ANSI colors.

    Returns:
        parsed_string (str): String with replaced ANSI colors.

    """

    def _sub_to_irc(ansi_match):
        return IRC_COLOR_MAP.get(ansi_match.group(), "")

    in_string = utils.to_str(string)
    parsed_string = []
    parts = RE_ANSI_ESCAPES.split(in_string) + [" "]
    for part, sep in zip(parts[::2], parts[1::2]):
        pstring = RE_ANSI_COLOR.sub(_sub_to_irc, part)
        parsed_string.append("%s%s" % (pstring, sep[0].strip()))
    # strip mxp
    parsed_string = RE_MXP.sub(r"\2", "".join(parsed_string))
    return parsed_string

def parse_irc_to_ansi(string):
    """
    Parse IRC mIRC color syntax and replace with Evennia ANSI color markers

    Args:
        string (str): String to parse for IRC colors.

    Returns:
        parsed_string (str): String with replaced IRC colors.

    """

    def _sub_to_ansi(irc_match):
        return ANSI_COLOR_MAP.get(irc_match.group(), "")

    pstring = RE_IRC_COLOR.sub(_sub_to_ansi, in_string)
    return pstring

# IRC bot

class IRCBot(irc.IRCClient, Session):
    """
    An IRC bot that tracks activity in a channel as well
    as sends text to it when prompted

    """

    lineRate = 1

    # assigned by factory at creation

    nickname = None
    logger = None
    factory = None
    channel = None
    sourceURL = "http://code.evennia.com"

    def signedOn(self):
        """
        This is called when we successfully connect to the network. We
        make sure to now register with the game as a full session.

        """
        self.join(self.channel)
        address = "%s@%s" % (self.channel, self.network)
        self.init_session("ircbot", address, self.factory.sessionhandler)
        # we link back to our bot and log in
        self.factory.sessionhandler.connect(self)
        logger.log_info(
            "IRC bot '%s' connected to %s at %s:%s."
            % (self.nickname, self.channel, self.network, self.port)
        )

    def disconnect(self, reason=""):
        """
        Called by sessionhandler to disconnect this protocol.

        Args:
            reason (str): Motivation for the disconnect.

        """
        self.stopping = True
        self.transport.loseConnection()

        pass

    def privmsg(self, user, channel, msg):
        """
        Called when the connected channel receives a message.

        Args:
            user (str): User name sending the message.
            channel (str): Channel name seeing the message.
            msg (str): The message arriving from channel.

        """
        if channel == self.nickname:
            # private message
            user = user.split("!", 1)[0]
            self.data_in(text=msg, type="privmsg", user=user, channel=channel)
        elif not msg.startswith("***"):
            # channel message
            user = ansi.raw(user)
            self.data_in(text=msg, type="msg", user=user, channel=channel)

    def action(self, user, channel, msg):
        """
        Called when an action is detected in channel.

        Args:

        """
        if not msg.startswith("**"):
            self.data_in(text=msg, type="action", user=user, channel=channel)

    def get_nicklist(self):
        """
        Retrieve name list from the channel. The return
        is handled by the catch methods below.

        """
        if not self.nicklist:
            self.sendLine("NAMES %s" % self.channel)

    def irc_RPL_NAMREPLY(self, prefix, params):
        """ "Handles IRC NAME request returns (nicklist)"""
        channel = params[2].lower()
        if channel != self.channel.lower():
            return
        self.nicklist += params[3].split(" ")

    def irc_RPL_ENDOFNAMES(self, prefix, params):
        """Called when the nicklist has finished being returned."""
        channel = params[1].lower()
            return
        self.data_in(
            text="", type="nicklist", user="server", channel=channel, nicklist=self.nicklist
        )
        self.nicklist = []

    def pong(self, user, time):
        """
        Called with the return timing from a PING.

        Args:
            user (str): Name of user
            time (float): Ping time in secs.

        """
        self.data_in(text="", type="ping", user="server", channel=self.channel, timing=time)

    def data_in(self, text=None, **kwargs):
        """
        Data IRC -> Server.

            text (str): Ingoing text.
            kwargs (any): Other data from protocol.

        """
        self.sessionhandler.data_in(self, bot_data_in=[parse_irc_to_ansi(text), kwargs])

    def send_channel(self, *args, **kwargs):
        """
        Send channel text to IRC channel (visible to all). Note that
        we don't handle the "text" send (it's rerouted to send_default
        which does nothing) - this is because the IRC bot is a normal
        session and would otherwise report anything that happens to it
        to the IRC channel (such as it seeing server reload messages).

        Args:
            text (str): Outgoing text

        """
        text = args[0] if args else ""
        if text:
            text = parse_ansi_to_irc(text)
            self.say(self.channel, text)

    def send_privmsg(self, *args, **kwargs):
        """
        Send message only to specific user.

        Args:
            text (str): Outgoing text.

            user (str): the nick to send
                privately to.

        """
        user = kwargs.get("user", None)
        if text and user:
            self.msg(user, text)

    def send_request_nicklist(self, *args, **kwargs):
        """
        Send a request for the channel nicklist. The return (handled
        by `self.irc_RPL_ENDOFNAMES`) will be sent back as a message
        with type `nicklist'.
        """
        self.get_nicklist()

    def send_ping(self, *args, **kwargs):
        """
        Send a ping. The return (handled by `self.pong`) will be sent
        back as a message of type 'ping'.
        """
        self.ping(self.nickname)

    def send_reconnect(self, *args, **kwargs):
        """
        The server instructs us to rebuild the connection by force,
        probably because the client silently lost connection.
        """
        self.factory.reconnect()

        """
        Ignore other types of sends.

        """
        pass

class IRCBotFactory(protocol.ReconnectingClientFactory):
    """
    Creates instances of IRCBot, connecting with a staggered
    increase in delay

    """

    # scaling reconnect time

        self,
        sessionhandler,
        uid=None,
        botname=None,
        channel=None,
        network=None,
        port=None,
        ssl=None,
    ):
        """
        Storing some important protocol properties.

        Args:
            sessionhandler (SessionHandler): Reference to the main Sessionhandler.

            uid (int): Bot user id.
            botname (str): Bot name (seen in IRC channel).
            channel (str): IRC channel to connect to.
            network (str): Network address to connect to.
            port (str): Port of the network.
            ssl (bool): Indicates SSL connection.

        """
        self.uid = uid
        self.nickname = str(botname)
        self.channel = str(channel)
        self.network = str(network)
        self.port = port
        self.ssl = ssl
        self.nicklists = {}

        """
        Build the protocol and assign it some properties.

        Args:
            addr (str): Not used; using factory data.

        """
        protocol = IRCBot()
        protocol.nickname = self.nickname
        protocol.network = self.network
        protocol.port = self.port
        protocol.ssl = self.ssl
        protocol.nicklist = []

        """

        Args:

        """
        logger.log_info("(re)connecting to %s" % self.channel)

        """

        Args:

        """
        self.retry(connector)

        """

        Args:

        """

        """
        sessions.

        """

        """
        Connect session to sessionhandler.

        """
        if self.port:
            if self.ssl:
                try:
                    from twisted.internet import ssl

                    service = reactor.connectSSL(
                        self.network, int(self.port), self, ssl.ClientContextFactory()
                    )
                    logger.log_err("To use SSL, the PyOpenSSL module must be installed.")
            else:
                service = internet.TCPClient(self.network, int(self.port), self)
            self.sessionhandler.portal.services.addService(service)

"""

MCCP - Mud Client Compression Protocol

This implements the MCCP v2 telnet protocol as per
http://tintin.sourceforge.net/mccp/. MCCP allows for the server to
compress data when sending to supporting clients, reducing bandwidth
by 70-90%.. The compression is done using Python's builtin zlib
library. If the client doesn't support MCCP, server sends uncompressed
as normal.  Note: On modern hardware you are not likely to notice the
effect of MCCP unless you have extremely heavy traffic or sits on a
terribly slow connection.

This protocol is implemented by the telnet protocol importing
mccp_compress and calling it from its write methods.
"""
import zlib

# negotiations for v1 and v2 of the protocol
MCCP = bytes([86])  # b"\x56"
FLUSH = zlib.Z_SYNC_FLUSH

def mccp_compress(protocol, data):
    """
    Handles zlib compression, if applicable.

    Args:
        data (str): Incoming data to compress.

    Returns:
        stream (binary): Zlib-compressed data.

    """
    if hasattr(protocol, "zlib"):
        return protocol.zlib.compress(data) + protocol.zlib.flush(FLUSH)
    return data

class Mccp:
    """
    Implements the MCCP protocol. Add this to a
    variable on the telnet protocol to set it up.

    """

    def __init__(self, protocol):
        """
        initialize MCCP by storing protocol on
        ourselves and calling the client to see if
        it supports MCCP. Sets callbacks to
        start zlib compression in that case.

        Args:
            protocol (Protocol): The active protocol instance.

        """

        self.protocol = protocol
        self.protocol.protocol_flags["MCCP"] = False
        # ask if client will mccp, connect callbacks to handle answer
        self.protocol.will(MCCP).addCallbacks(self.do_mccp, self.no_mccp)

    def no_mccp(self, option):
        """
        Called if client doesn't support mccp or chooses to turn it off.

        Args:
            option (Option): Option dict (not used).

        """
        if hasattr(self.protocol, "zlib"):
            del self.protocol.zlib
        self.protocol.handshake_done()

    def do_mccp(self, option):
        """
        The client supports MCCP. Set things up by
        creating a zlib compression stream.

        Args:

        """
        self.protocol.protocol_flags["MCCP"] = True
        self.protocol.requestNegotiation(MCCP, b"")
        self.protocol.zlib = zlib.compressobj(9)

"""

MSSP - Mud Server Status Protocol

This implements the MSSP telnet protocol as per
http://tintin.sourceforge.net/mssp/.  MSSP allows web portals and
listings to have their crawlers find the mud and automatically
extract relevant information about it, such as genre, how many
active players and so on.

"""

MSSP = bytes([70])  # b"\x46"
MSSP_VAR = bytes([1])  # b"\x01"
MSSP_VAL = bytes([2])  # b"\x02"

# try to get the customized mssp info, if it exists.
MSSPTable_CUSTOM = utils.variable_from_module(settings.MSSP_META_MODULE, "MSSPTable", default={})

class Mssp:
    """
    Implements the MSSP protocol. Add this to a variable on the telnet
    protocol to set it up.

    """

        """
        initialize MSSP by storing protocol on ourselves and calling
        the client to see if it supports MSSP.

        Args:

        """
        self.protocol.will(MSSP).addCallbacks(self.do_mssp, self.no_mssp)

    def get_player_count(self):
        """
        Get number of logged-in players.

        Returns:
            count (int): The number of players in the MUD.

        """
        return str(self.protocol.sessionhandler.count_loggedin())

    def get_uptime(self):
        """
        Get how long the portal has been online (reloads are not counted).

        Returns:
            uptime (int): Number of seconds of uptime.

        """
        return str(self.protocol.sessionhandler.uptime)

    def no_mssp(self, option):
        """
        Called when mssp is not requested. This is the normal
        operation.

        Args:
            option (Option): Not used.

        """

    def do_mssp(self, option):
        """
        Negotiate all the information.

        Args:

        """

        self.mssp_table = {
            # Required fields
            "NAME": settings.SERVERNAME,
            "PLAYERS": self.get_player_count,
            "UPTIME": self.get_uptime,
            "PORT": list(
                str(port) for port in reversed(settings.TELNET_PORTS)
            ),  # most important port should be last in list
            # Evennia auto-filled
            "CRAWL DELAY": "-1",
            "CODEBASE": utils.get_evennia_version(mode="pretty"),
            "FAMILY": "Custom",
            "ANSI": "1",
            "GMCP": "1" if settings.TELNET_OOB_ENABLED else "0",
            "ATCP": "0",
            "MCCP": "1",
            "MCP": "0",
            "MSDP": "1" if settings.TELNET_OOB_ENABLED else "0",
            "MSP": "0",
            "MXP": "1",
            "PUEBLO": "0",
            "SSL": "1" if settings.SSL_ENABLED else "0",
            "UTF-8": "1",
            "ZMP": "0",
            "VT100": "1",
            "XTERM 256 COLORS": "1",
        }

        # update the static table with the custom one
        if MSSPTable_CUSTOM:
            self.mssp_table.update(MSSPTable_CUSTOM)

        varlist = b""
        for variable, value in self.mssp_table.items():
            if callable(value):
                value = value()
            if utils.is_iter(value):
                for partval in value:
                    varlist += (
                        MSSP_VAR
                        + bytes(str(variable), "utf-8")
                        + MSSP_VAL
                        + bytes(str(partval), "utf-8")
                    )
            else:
                varlist += (
                    MSSP_VAR + bytes(str(variable), "utf-8") + MSSP_VAL + bytes(str(value), "utf-8")
                )

        # send to crawler by subnegotiation
        self.protocol.requestNegotiation(MSSP, varlist)

"""
MXP - Mud eXtension Protocol.

Partial implementation of the MXP protocol.
The MXP protocol allows more advanced formatting options for telnet clients
that supports it (mudlet, zmud, mushclient are a few)

This only implements the SEND tag.

More information can be found on the following links:
http://www.zuggsoft.com/zmud/mxp.htm
http://www.mushclient.com/mushclient/mxp.htm
http://www.gammon.com.au/mushclient/addingservermxp.htm

"""
import re

LINKS_SUB = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
URL_SUB = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)

# MXP Telnet option
MXP = bytes([91])  # b"\x5b"

MXP_TEMPSECURE = "\x1B[4z"
MXP_SEND = MXP_TEMPSECURE + '<SEND HREF="\\1">' + "\\2" + MXP_TEMPSECURE + "</SEND>"
MXP_URL = MXP_TEMPSECURE + '<A HREF="\\1">' + "\\2" + MXP_TEMPSECURE + "</A>"

def mxp_parse(text):
    """
    Replaces links to the correct format for MXP.

    Args:
        text (str): The text to parse.

    Returns:
        parsed (str): The parsed text.

    """
    text = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

    text = LINKS_SUB.sub(MXP_SEND, text)
    text = URL_SUB.sub(MXP_URL, text)

class Mxp:
    """
    Implements the MXP protocol.

    """

        """
        Initializes the protocol by checking if the client supports it.

        Args:

        """
        self.protocol.protocol_flags["MXP"] = False
        if settings.MXP_ENABLED:
            self.protocol.will(MXP).addCallbacks(self.do_mxp, self.no_mxp)

    def no_mxp(self, option):
        """
        Called when the Client reports to not support MXP.

        Args:

        """

    def do_mxp(self, option):
        """
        Called when the Client reports to support MXP.

        Args:

        """
            self.protocol.protocol_flags["MXP"] = True
            self.protocol.requestNegotiation(MXP, b"")
        else:
            self.protocol.wont(MXP)

"""

NAWS - Negotiate About Window Size

This implements the NAWS telnet option as per
https://www.ietf.org/rfc/rfc1073.txt

NAWS allows telnet clients to report their current window size to the
client and update it when the size changes

"""
from codecs import encode as codecs_encode

NAWS = bytes([31])  # b"\x1f"
IS = bytes([0])  # b"\x00"

# default taken from telnet specification
DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
DEFAULT_HEIGHT = settings.CLIENT_DEFAULT_HEIGHT

class Naws:
    """
    Implements the NAWS protocol. Add this to a variable on the telnet

    """

        """
        initialize NAWS by storing protocol on ourselves and calling
        the client to see if it supports NAWS.

        Args:

        """
        self.naws_step = 0
        self.protocol.protocol_flags["SCREENWIDTH"] = {
            0: DEFAULT_WIDTH
        }  # windowID (0 is root):width
        self.protocol.protocol_flags["SCREENHEIGHT"] = {0: DEFAULT_HEIGHT}  # windowID:width
        self.protocol.negotiationMap[NAWS] = self.negotiate_sizes
        self.protocol.do(NAWS).addCallbacks(self.do_naws, self.no_naws)

    def no_naws(self, option):
        """
        Called when client is not reporting NAWS. This is the normal

        Args:

        """

    def do_naws(self, option):
        """
        Client wants to negotiate all the NAWS information.

        Args:

        """

    def negotiate_sizes(self, options):
        """
        Step through the NAWS handshake.

        Args:
            option (list): The incoming NAWS options.

        """
        if len(options) == 4:
            # NAWS is negotiated with 16bit words
            width = options[0] + options[1]
            self.protocol.protocol_flags["SCREENWIDTH"][0] = int(codecs_encode(width, "hex"), 16)
            height = options[2] + options[3]
            self.protocol.protocol_flags["SCREENHEIGHT"][0] = int(codecs_encode(height, "hex"), 16)

"""
Sessionhandler for portal sessions.

"""

from collections import deque, namedtuple

from evennia.server.portal.amp import PCONN, PCONNSYNC, PDISCONN, PDISCONNALL
from evennia.server.sessionhandler import SessionHandler

# module import
_MOD_IMPORT = None

# global throttles
_MAX_CONNECTION_RATE = float(settings.MAX_CONNECTION_RATE)
# per-session throttles
_MAX_COMMAND_RATE = float(settings.MAX_COMMAND_RATE)
_MAX_CHAR_LIMIT = int(settings.MAX_CHAR_LIMIT)

_MIN_TIME_BETWEEN_CONNECTS = 1.0 / float(_MAX_CONNECTION_RATE)
_MIN_TIME_BETWEEN_COMMANDS = 1.0 / float(_MAX_COMMAND_RATE)

_ERROR_COMMAND_OVERFLOW = settings.COMMAND_RATE_WARNING
_ERROR_MAX_CHAR = settings.MAX_CHAR_LIMIT_WARNING

_CONNECTION_QUEUE = deque()

# Portal-SessionHandler class

DOS_PROTECTION_MSG = _(
    "{servername} DoS protection is active.You are queued to connect in {num} seconds ..."
)

class PortalSessionHandler(SessionHandler):
    """
    This object holds the sessions connected to the portal at any time.
    It is synced with the server's equivalent SessionHandler over the AMP
    connection.

    Sessions register with the handler using the connect() method. This
    will assign a new unique sessionid to the session and send that sessid
    to the server using the AMP connection.

    """

        """
        Init the handler

        """
        self.latest_sessid = 0
        self.uptime = time.time()
        self.connection_time = 0

        self.connection_last = self.uptime
        self.connection_task = None

    def at_server_connection(self):
        """
        Called when the Portal establishes connection with the Server.
        At this point, the AMP connection is already established.

        """
        self.connection_time = time.time()

    def generate_sessid(self):
        """
        Simply generates a sessid that's guaranteed to be unique for this Portal run.

        Returns:
            sessid

        """
        self.latest_sessid += 1
        if self.latest_sessid in self:
            return self.generate_sessid()
        return self.latest_sessid

    def connect(self, session):
        """
        Called by protocol at first connect. This adds a not-yet
        authenticated session using an ever-increasing counter for
        sessid.

        Args:
            session (PortalSession): The Session connecting.

        Notes:
            We implement a throttling mechanism here to limit the speed at
            which new connections are accepted - this is both a stop
            against DoS attacks as well as helps using the Dummyrunner
            tester with a large number of connector dummies.

        """
        global _CONNECTION_QUEUE

            # assign if we are first-connectors
            if not session.sessid:
                # if the session already has a sessid (e.g. being inherited in the
                # case of a webclient auto-reconnect), keep it
                session.sessid = self.generate_sessid()
            session.server_connected = False
            _CONNECTION_QUEUE.appendleft(session)
            if len(_CONNECTION_QUEUE) > 1:
                session.data_out(
                    text=(
                        (
                            DOS_PROTECTION_MSG.format(
                                servername=settings.SERVERNAME,
                                num=len(_CONNECTION_QUEUE) * _MIN_TIME_BETWEEN_CONNECTS,
                            ),
                        ),
                        {},
                    )
                )
        if (
            now - self.connection_last < _MIN_TIME_BETWEEN_CONNECTS
        ) or not evennia.EVENNIA_PORTAL_SERVICE.amp_protocol:
            if not session or not self.connection_task:
                self.connection_task = reactor.callLater(
                    _MIN_TIME_BETWEEN_CONNECTS, self.connect, None
                )
            self.connection_last = now
            return
        elif not session:
            if _CONNECTION_QUEUE:
                # keep launching tasks until queue is empty
                )
            else:
                self.connection_task = None
        self.connection_last = now

        if _CONNECTION_QUEUE:
            # sync with server-side
            session = _CONNECTION_QUEUE.pop()
            sessdata = session.get_sync_data()

            self[session.sessid] = session
            session.server_connected = True
            evennia.EVENNIA_PORTAL_SERVICE.amp_protocol.send_AdminPortal2Server(
                session, operation=PCONN, sessiondata=sessdata
            )

    def sync(self, session):
        """
        Called by the protocol of an already connected session. This
        can be used to sync the session info in a delayed manner, such
        as when negotiation and handshakes are delayed.

        Args:
            session (PortalSession): Session to sync.

        """
        if session.sessid and session.server_connected:
            # only use if session already has sessid and has already connected
            # once to the server - if so we must re-sync woth the server, otherwise
            # we skip this step.
            if evennia.EVENNIA_PORTAL_SERVICE.amp_protocol:
                # we only send sessdata that should not have changed
                # at the server level at this point
                sessdata = dict(
                    (key, val)
                    for key, val in sessdata.items()
                    if key
                    in (
                        "protocol_key",
                        "address",
                        "sessid",
                        "csessid",
                        "conn_time",
                        "protocol_flags",
                        "server_data",
                    )
                )
                evennia.EVENNIA_PORTAL_SERVICE.amp_protocol.send_AdminPortal2Server(
                    session, operation=PCONNSYNC, sessiondata=sessdata
                )

    def disconnect(self, session):
        """
        Called from portal when the connection is closed from the
        portal side.

        Args:
            session (PortalSession): Session to disconnect.
            delete (bool, optional): Delete the session from
                the handler. Only time to not do this is when
                this is called from a loop, such as from
                self.disconnect_all().

        """
        if session in _CONNECTION_QUEUE:
            # connection was already dropped before we had time
            # to forward this to the Server, so now we just remove it.
            _CONNECTION_QUEUE.remove(session)
            return

        if session.sessid in self and not hasattr(self, "_disconnect_all"):
            # if this was called directly from the protocol, the
            # connection is already dead and we just need to cleanup
            del self[session.sessid]

        # Tell the Server to disconnect its version of the Session as well.
        evennia.EVENNIA_PORTAL_SERVICE.amp_protocol.send_AdminPortal2Server(
            session, operation=PDISCONN
        )

    def disconnect_all(self):
        """
        Disconnect all sessions, informing the Server.

        """
        if settings.TEST_ENVIRONMENT:
            return

        def _callback(result, sessionhandler):
            # we set a watchdog to stop self.disconnect from deleting
            # sessions while we are looping over them.
            sessionhandler._disconnect_all = True
            for session in sessionhandler.values():
                session.disconnect()
            del sessionhandler._disconnect_all

        # inform Server; wait until finished sending before we continue
        # removing all the sessions.

            DUMMYSESSION, operation=PDISCONNALL
        ).addCallback(_callback, self)

    def server_connect(self, protocol_path="", config=dict()):
        """
        Called by server to force the initialization of a new protocol
        instance. Server wants this instance to get a unique sessid and to be
        connected back as normal. This is used to initiate irc/rss etc
        connections.

        Args:
            protocol_path (str): Full python path to the class factory
                for the protocol used, eg
                'evennia.server.portal.irc.IRCClientFactory'
            config (dict): Dictionary of configuration options, fed as
                `**kwarg` to protocol class `__init__` method.

        Raises:
            RuntimeError: If The correct factory class is not found.

        Notes:
            The called protocol class must have a method start()
            that calls the portalsession.connect() as a normal protocol.

        """
        global _MOD_IMPORT
        if not _MOD_IMPORT:
            from evennia.utils.utils import variable_from_module as _MOD_IMPORT
        path, clsname = protocol_path.rsplit(".", 1)
        cls = _MOD_IMPORT(path, clsname)
        if not cls:
            raise RuntimeError("ServerConnect: protocol factory '%s' not found." % protocol_path)
        protocol = cls(self, **config)
        protocol.start()

    def server_disconnect(self, session, reason=""):
        """
        Called by server to force a disconnect by sessid.

        Args:
            session (portalsession): Session to disconnect.
            reason (str, optional): Motivation for disconnect.

        """
            session.disconnect(reason)
            if session.sessid in self:
                # in case sess.disconnect doesn't delete it
                del self[session.sessid]
            del session

    def server_disconnect_all(self, reason=""):
        """
        Called by server when forcing a clean disconnect for everyone.

        Args:

        """
        for session in list(self.values()):

    def server_logged_in(self, session, data):
        """
        The server tells us that the session has been authenticated.
        Update it. Called by the Server.

        Args:
            session (Session): Session logging in.
            data (dict): The session sync data.

        """
        session.load_sync_data(data)
        session.at_login()

    def server_session_sync(self, serversessions, clean=True):
        """
        Server wants to save data to the portal, maybe because it's
        about to shut down. We don't overwrite any sessions here, just
        update them in-place.

        Args:
            serversessions (dict): This is a dictionary

                `{sessid:{property:value},...}` describing
                the properties to sync on all sessions.
            clean (bool): If True, remove any Portal sessions that are
                not included in serversessions.
        """
        to_save = [sessid for sessid in serversessions if sessid in self]
        # save protocols
        for sessid in to_save:
            self[sessid].load_sync_data(serversessions[sessid])
        if clean:
            # disconnect out-of-sync missing protocols
            to_delete = [sessid for sessid in self if sessid not in to_save]
            for sessid in to_delete:
                self.server_disconnect(sessid)

    def count_loggedin(self, include_unloggedin=False):
        """
        Count loggedin connections, alternatively count all connections.

        Args:
            include_unloggedin (bool): Also count sessions that have
            not yet authenticated.

        Returns:
            count (int): Number of sessions.

        """
        return len(self.get_sessions(include_unloggedin=include_unloggedin))

    def sessions_from_csessid(self, csessid):
        """
        Given a session id, retrieve the session (this is primarily
        intended to be called by web clients)

        Args:
            csessid (int): Session id.

        Returns:
            session (list): The matching session, if found.

        """
        return [
            sess
            for sess in self.get_sessions(include_unloggedin=True)
            if hasattr(sess, "csessid") and sess.csessid and sess.csessid == csessid
        ]

    def announce_all(self, message):
        """
        Send message to all connected sessions.

        Args:
            message (str):  Message to relay.

        Notes:
            This will create an on-the fly text-type
            send command.

        """
        for session in self.values():
            self.data_out(session, text=[[message], {}])

    def data_in(self, session, **kwargs):
        """
        Called by portal sessions for relaying data coming
        in from the protocol to the server.

        Args:
            session (PortalSession): Session receiving data.

        Notes:
            Data is serialized before passed on.

        """
        try:
            text = kwargs["text"]
            if (_MAX_CHAR_LIMIT > 0) and len(text) > _MAX_CHAR_LIMIT:
                if session:
                    self.data_out(session, text=[[_ERROR_MAX_CHAR], {}])
                return
            # if there is a problem to send, we continue
            pass
            now = time.time()

            try:
                command_counter_reset = session.command_counter_reset
                command_counter_reset = session.command_counter_reset = now
                session.command_counter = 0

            # global command-rate limit
            if max(0, now - command_counter_reset) > 1.0:
                # more than a second since resetting the counter. Refresh.
                session.command_counter_reset = now

            session.command_counter += 1

            if session.command_counter * _MIN_TIME_BETWEEN_COMMANDS > 1.0:
                self.data_out(session, text=[[_ERROR_COMMAND_OVERFLOW], {}])
                return

            if not evennia.EVENNIA_PORTAL_SERVICE.amp_protocol:
                # this can happen if someone connects before AMP connection
                # was established (usually on first start)
                reactor.callLater(1.0, self.data_in, session, **kwargs)
                return

            # scrub data
            kwargs = self.clean_senddata(session, kwargs)

            # relay data to Server
            session.cmd_last = now
            evennia.EVENNIA_PORTAL_SERVICE.amp_protocol.send_MsgPortal2Server(session, **kwargs)

            # eventual local echo (text input only)
            if "text" in kwargs and session.protocol_flags.get("LOCALECHO", False):
                self.data_out(session, text=kwargs["text"])

    def data_out(self, session, **kwargs):
        """
        Called by server for having the portal relay messages and data
        to the correct session protocol.

        Args:
            session (Session): Session sending data.

            kwargs (any): Each key is a command instruction to the
                protocol on the form key = [[args],{kwargs}]. This will
                call a method send_<key> on the protocol. If no such
                method exixts, it sends the data to a method send_default.

        """
        # from evennia.server.profiling.timetrace import timetrace  # DEBUG
        # text = timetrace(text, "portalsessionhandler.data_out")  # DEBUG

        # distribute outgoing data to the correct session methods.
            for cmdname, (cmdargs, cmdkwargs) in kwargs.items():
                funcname = "send_%s" % cmdname.strip().lower()
                if hasattr(session, funcname):
                    # better to use hassattr here over try..except
                    # - avoids hiding AttributeErrors in the call.
                    try:
                        getattr(session, funcname)(*cmdargs, **cmdkwargs)
                        log_trace()
                else:
                    try:
                        # note that send_default always takes cmdname
                        # as arg too.
                        session.send_default(cmdname, *cmdargs, **cmdkwargs)

# This will be filled in when the portal boots.
PORTAL_SESSIONS = None

"""
RSS parser for Evennia

This connects an RSS feed to an in-game Evennia channel, sending messages
to the channel whenever the feed updates.

"""
from twisted.internet import task, threads

RSS_ENABLED = settings.RSS_ENABLED
# RETAG = re.compile(r'<[^>]*?>')

if RSS_ENABLED:
    try:
        import feedparser
        raise ImportError(
            "RSS requires python-feedparser to be installed. Install or set RSS_ENABLED=False."
        )

class RSSReader(Session):
    """
    A simple RSS reader using the feedparser module.

    """

    def __init__(self, factory, url, rate):
        """
        Initialize the reader.

        Args:
            factory (RSSFactory): The protocol factory.
            url (str): The RSS url.
            rate (int): The seconds between RSS lookups.

        """
        self.url = url
        self.rate = rate
        self.factory = factory
        self.old_entries = {}

    def get_new(self):
        """
        Returns list of new items.

        """
        feed = feedparser.parse(self.url)
        new_entries = []
        for entry in feed["entries"]:
            idval = entry["id"] + entry.get("updated", "")
            if idval not in self.old_entries:
                self.old_entries[idval] = entry
                new_entries.append(entry)
        return new_entries

        """
        Disconnect from feed.

        Args:
            reason (str, optional): Motivation for the disconnect.

        """
        if self.factory.task and self.factory.task.running:
            self.factory.task.stop()

    def _callback(self, new_entries, init):
        """
        Called when RSS returns.

        Args:
            new_entries (list): List of new RSS entries since last.
            init (bool): If this is a startup operation (at which
                point all entries are considered new).

        """
        if not init:
            # for initialization we just ignore old entries
            for entry in reversed(new_entries):
                self.data_in(entry)

        """
        Data RSS -> Evennia.

            text (str): Incoming text
            kwargs (any): Options from protocol.

        """
        self.sessionhandler.data_in(self, bot_data_in=text, **kwargs)

    def _errback(self, fail):
        "Report error"
        logger.log_err("RSS feed error: %s" % fail.value)

    def update(self, init=False):
        """
        Request the latest version of feed.

        Args:
            init (bool, optional): If this is an initialization call
                or not (during init, all entries are conidered new).

        Notes:
            This call is done in a separate thread to avoid blocking
            on slow connections.

        """
        return (
            threads.deferToThread(self.get_new)
            .addCallback(self._callback, init)
            .addErrback(self._errback)
        )

class RSSBotFactory(object):
    """
    Initializes new bots.
    """

    def __init__(self, sessionhandler, uid=None, url=None, rate=None):
        """
        Initialize the bot.

        Args:
            sessionhandler (PortalSessionHandler): The main sessionhandler object.
            uid (int): User id for the bot.
            url (str): The RSS URL.
            rate (int): How often for the RSS to request the latest RSS entries.

        """
        self.bot = RSSReader(self, url, rate)
        self.task = None

        """
        Called by portalsessionhandler. Starts the bot.

        """

        def errback(fail):
            logger.log_err(fail.value)

        # set up session and connect it to sessionhandler
        self.bot.init_session("rssbot", self.url, self.sessionhandler)
        self.bot.uid = self.uid
        self.bot.logged_in = True
        self.sessionhandler.connect(self.bot)

        # start repeater task
        self.bot.update(init=True)
        self.task = task.LoopingCall(self.bot.update)
        if self.rate:
            self.task.start(self.rate, now=False).addErrback(errback)

import os
import sys
from os.path import abspath, dirname

from django.db import connection
from twisted.application import internet, service
from twisted.application.service import MultiService

    get_evennia_version,
    mod_import,
)

class EvenniaPortalService(MultiService):
        self.amp_protocol = None
        self.server_process_id = None
        self.server_restart_mode = "shutdown"
        self.server_info_dict = dict()
        self.plugins = list()

        self.start_time = 0
        self._maintenance_count = 0
        self.maintenance_task = None

        self.info_dict = {
            "servername": settings.SERVERNAME,
            "errors": "",
            "info": "",
            "lockdown_mode": "",
            "amp": "",
            "telnet": [],
            "telnet_ssl": [],
            "ssh": [],
            "webclient": [],
            "webserver_proxy": [],
            "webserver_internal": [],
        }

        # in non-interactive portal mode, this gets overwritten by
        # cmdline sent by the evennia launcher
        self.server_twistd_cmd = self._get_backup_server_twistd_cmd()

    def portal_maintenance(self):
        """
        Repeated maintenance tasks for the portal.

        """

        self._maintenance_count += 1

        if self._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)

    def privilegedStartService(self):
        self.maintenance_task = LoopingCall(self.portal_maintenance)
        self.maintenance_task.start(60, now=True)  # call every minute
        # set a callback if the server is killed abruptly,
        # by Ctrl-C, reboot etc.
        reactor.addSystemEventTrigger(
            "before", "shutdown", self.shutdown, _reactor_stopping=True, _stop_server=True
        )

        if settings.AMP_HOST and settings.AMP_PORT and settings.AMP_INTERFACE:
            self.register_amp()

        if settings.TELNET_ENABLED and settings.TELNET_PORTS and settings.TELNET_INTERFACES:
            self.register_telnet()

        if settings.SSL_ENABLED and settings.SSL_PORTS and settings.SSL_INTERFACES:
            self.register_ssl()

        if settings.SSH_ENABLED and settings.SSH_PORTS and settings.SSH_INTERFACES:
            self.register_ssh()

        if settings.WEBSERVER_ENABLED:
            self.register_webserver()

        if settings.LOCKDOWN_MODE:
            self.info_dict["lockdown_mode"] = "  LOCKDOWN_MODE active: Only local connections."

        self.register_plugins()

        super().privilegedStartService()

    def register_plugins(self):
        self.plugins.extend(
            mod_import(module) for module in make_iter(settings.PORTAL_SERVICES_PLUGIN_MODULES)
        )
        for plugin_module in self.plugins:
            # external plugin services to start
            if plugin_module:
                plugin_module.start_plugin_services(self)

    def check_lockdown(self, interfaces: list[str]):
            return ["127.0.0.1"]
        return interfaces

    def register_ssl(self):
        # Start Telnet+SSL game connection (requires PyOpenSSL).

        from evennia.server.portal import telnet_ssl

        _ssl_protocol = class_from_module(settings.SSL_PROTOCOL_CLASS)

        interfaces = self.check_lockdown(settings.SSL_INTERFACES)

        for interface in interfaces:
            ifacestr = ""
            if interface not in ("0.0.0.0", "::") or len(interfaces) > 1:
                ifacestr = "-%s" % interface
            for port in settings.SSL_PORTS:
                pstring = "%s:%s" % (ifacestr, port)
                factory = protocol.ServerFactory()
                factory.noisy = False
                factory.sessionhandler = evennia.PORTAL_SESSION_HANDLER
                factory.protocol = _ssl_protocol

                ssl_context = telnet_ssl.getSSLContext()
                if ssl_context:
                    ssl_service = internet.SSLServer(
                        port, factory, telnet_ssl.getSSLContext(), interface=interface
                    )
                    ssl_service.setName("EvenniaSSL%s" % pstring)
                    ssl_service.setServiceParent(self)

                    self.info_dict["telnet_ssl"].append("telnet+ssl%s: %s" % (ifacestr, port))
                else:
                    self.info_dict["telnet_ssl"].append(
                        "telnet+ssl%s: %s (deactivated - keys/cert unset)" % (ifacestr, port)
                    )

    def register_ssh(self):
        # Start SSH game connections. Will create a keypair in
        # evennia/game if necessary.

        from evennia.server.portal import ssh

        _ssh_protocol = class_from_module(settings.SSH_PROTOCOL_CLASS)

        interfaces = self.check_lockdown(settings.SSH_INTERFACES)

            for port in settings.SSH_PORTS:
                factory = ssh.makeFactory(
                    {
                        "protocolFactory": _ssh_protocol,
                        "protocolArgs": (),
                        "sessions": evennia.PORTAL_SESSION_HANDLER,
                    }
                )
                ssh_service = internet.TCPServer(port, factory, interface=interface)
                ssh_service.setName("EvenniaSSH%s" % pstring)
                ssh_service.setServiceParent(self)

                self.info_dict["ssh"].append("ssh%s: %s" % (ifacestr, port))

    def register_webserver(self):
        from evennia.server.webserver import EvenniaReverseProxyResource, Website

        # Start a reverse proxy to relay data to the Server-side webserver
        interfaces = self.check_lockdown(settings.WEBSERVER_INTERFACES)
        websocket_started = False
        _websocket_protocol = class_from_module(settings.WEBSOCKET_PROTOCOL_CLASS)

            for proxyport, serverport in settings.WEBSERVER_PORTS:
                web_root = EvenniaReverseProxyResource("127.0.0.1", serverport, "")
                webclientstr = ""
                if settings.WEBCLIENT_ENABLED:
                    # create ajax client processes at /webclientdata
                    from evennia.server.portal import webclient_ajax

                    ajax_webclient = webclient_ajax.AjaxWebClient()
                    ajax_webclient.sessionhandler = evennia.PORTAL_SESSION_HANDLER
                    web_root.putChild(b"webclientdata", ajax_webclient)
                    webclientstr = "webclient (ajax only)"

                    if (
                        settings.WEBSOCKET_CLIENT_ENABLED
                        and settings.WEBSOCKET_CLIENT_PORT
                        and settings.WEBSOCKET_CLIENT_INTERFACE
                    ) and not websocket_started:
                        # start websocket client port for the webclient
                        # we only support one websocket client
                        from autobahn.twisted.websocket import WebSocketServerFactory

                        from evennia.server.portal import webclient  # noqa

                        w_interface = (
                            "127.0.0.1"
                            if settings.LOCKDOWN_MODE
                            else settings.WEBSOCKET_CLIENT_INTERFACE
                        )
                        w_ifacestr = ""
                        if (
                            w_interface not in ("0.0.0.0", "::")
                            or len(settings.WEBSERVER_INTERFACES) > 1
                        ):
                            w_ifacestr = "-%s" % w_interface
                        port = settings.WEBSOCKET_CLIENT_PORT

                        class Websocket(WebSocketServerFactory):
                            "Only here for better naming in logs"
                            pass

                        factory = Websocket()
                        factory.noisy = False
                        factory.protocol = _websocket_protocol
                        factory.sessionhandler = evennia.PORTAL_SESSION_HANDLER
                        websocket_service = internet.TCPServer(port, factory, interface=w_interface)
                        websocket_service.setName("EvenniaWebSocket%s:%s" % (w_ifacestr, port))
                        websocket_service.setServiceParent(self)
                        websocket_started = True
                        webclientstr = "webclient-websocket%s: %s" % (w_ifacestr, port)
                    self.info_dict["webclient"].append(webclientstr)

                try:
                    WEB_PLUGINS_MODULE = mod_import(settings.WEB_PLUGINS_MODULE)
                    WEB_PLUGINS_MODULE = None
                    self.info_dict["errors"] = (
                        "WARNING: settings.WEB_PLUGINS_MODULE not found - "
                        "copy 'evennia/game_template/server/conf/web_plugins.py to "
                        "mygame/server/conf."
                    )

                if WEB_PLUGINS_MODULE:
                    try:
                        web_root = WEB_PLUGINS_MODULE.at_webproxy_root_creation(web_root)
                        # Legacy user has not added an at_webproxy_root_creation function in existing
                        # web plugins file
                        self.info_dict["errors"] = (
                            "WARNING: WEB_PLUGINS_MODULE is enabled but at_webproxy_root_creation() "
                            "not found copy 'evennia/game_template/server/conf/web_plugins.py to "
                            "mygame/server/conf."
                        )
                web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE)
                web_root.is_portal = True
                proxy_service = internet.TCPServer(proxyport, web_root, interface=interface)
                proxy_service.setName("EvenniaWebProxy%s:%s" % (ifacestr, proxyport))
                proxy_service.setServiceParent(self)
                self.info_dict["webserver_proxy"].append(
                    "webserver-proxy%s: %s" % (ifacestr, proxyport)
                )
                self.info_dict["webserver_internal"].append("webserver: %s" % serverport)

    def register_telnet(self):
        # Start telnet game connections

        from evennia.server.portal import telnet

        _telnet_protocol = class_from_module(settings.TELNET_PROTOCOL_CLASS)

        interfaces = self.check_lockdown(settings.TELNET_INTERFACES)

            for port in settings.TELNET_PORTS:
                factory = telnet.TelnetServerFactory()
                factory.protocol = _telnet_protocol
                telnet_service = internet.TCPServer(port, factory, interface=interface)
                telnet_service.setName("EvenniaTelnet%s" % pstring)
                telnet_service.setServiceParent(self)

                self.info_dict["telnet"].append("telnet%s: %s" % (ifacestr, port))

    def register_amp(self):
        # The AMP protocol handles the communication between
        # the portal and the mud server. Only reason to ever deactivate
        # it would be during testing and debugging.

        from evennia.server.portal import amp_server

        self.info_dict["amp"] = "amp: %s" % settings.AMP_PORT

        factory = amp_server.AMPServerFactory(self)
        amp_service = internet.TCPServer(
            settings.AMP_PORT, factory, interface=settings.AMP_INTERFACE
        )
        amp_service.setName("PortalAMPServer")
        amp_service.setServiceParent(self)

    def _get_backup_server_twistd_cmd(self):
        """
        For interactive Portal mode there is no way to get the server cmdline from the launcher, so
        we need to guess it here (it's very likely to not change)

        Returns:
            server_twistd_cmd (list): An instruction for starting the server, to pass to Popen.

        """
        server_twistd_cmd = [
            "twistd",
            "--python={}".format(os.path.join(dirname(dirname(abspath(__file__))), "server.py")),
        ]
        if os.name != "nt":
            gamedir = os.getcwd()
            server_twistd_cmd.append(
                "--pidfile={}".format(os.path.join(gamedir, "server", "server.pid"))
            )
        return server_twistd_cmd

    def get_info_dict(self):
        """
        Return the Portal info, for display.

        """
        return self.info_dict

    def shutdown(self, _reactor_stopping=False, _stop_server=False):
        """
        Shuts down the server from inside it.

        Args:
            _reactor_stopping (bool, optional): This is set if server
                is already in the process of shutting down; in this case
                we don't need to stop it again.
            _stop_server (bool, optional): Only used in portal-interactive mode;
                makes sure to stop the Server cleanly.

        Note that restarting (regardless of the setting) will not work
        if the Portal is currently running in daemon mode. In that
        case it always needs to be restarted manually.

        """
        if _reactor_stopping and hasattr(self, "shutdown_complete"):
            # we get here due to us calling reactor.stop below. No need
            # to do the shutdown procedure again.
            return

        evennia.PORTAL_SESSION_HANDLER.disconnect_all()
        if _stop_server:
            self.amp_protocol.stop_server(mode="shutdown")
        if not _reactor_stopping:
            # shutting down the reactor will trigger another signal. We set
            # a flag to avoid loops.
            self.shutdown_complete = True
            reactor.callLater(0, reactor.stop)

"""
This module implements the ssh (Secure SHell) protocol for encrypted
connections.

This depends on a generic session module that implements the actual
login procedure of the game, tracks sessions etc.

Using standard ssh client,

"""

import os
import re

from twisted.conch.interfaces import IConchUser
from twisted.cred import checkers
from twisted.cred.portal import Portal

_SSH_IMPORT_ERROR = """
ERROR: Missing crypto library for SSH. Install it with

       pip install cryptography pyasn1 bcrypt

(On older Twisted versions you may have to do 'pip install pycrypto pyasn1' instead).

If you get a compilation error you must install a C compiler and the
SSL dev headers (On Debian-derived systems this is the gcc and libssl-dev
packages).
"""

try:
    from twisted.conch.ssh.keys import Key
    raise ImportError(_SSH_IMPORT_ERROR)

from evennia.utils.utils import class_from_module, to_str
from twisted.conch import interfaces as iconch
from twisted.conch.insults import insults
from twisted.conch.manhole import Manhole, recvline
from twisted.conch.manhole_ssh import ConchFactory, TerminalRealm, _Glue
from twisted.conch.ssh import common
from twisted.conch.ssh.userauth import SSHUserAuthServer
from twisted.internet import defer, protocol
from twisted.python import components

_RE_N = re.compile(r"\|n$")
_RE_SCREENREADER_REGEX = re.compile(
    r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE
)
_PRIVATE_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssh-private.key")
_PUBLIC_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssh-public.key")
_KEY_LENGTH = 2048

CTRL_C = "\x03"
CTRL_D = "\x04"
CTRL_BACKSLASH = "\x1c"
CTRL_L = "\x0c"

_NO_AUTOGEN = f"""
Evennia could not generate SSH private- and public keys ({{err}})
Using conch default keys instead.

If this error persists, create the keys manually (using the tools for your OS)
and put them here:
    {_PRIVATE_KEY_FILE}
    {_PUBLIC_KEY_FILE}
"""

# not used atm
class SSHServerFactory(protocol.ServerFactory):
    """
    This is only to name this better in logs

    """

        return "SSH"

class SshProtocol(Manhole, _BASE_SESSION_CLASS):
    """
    Each account connecting over ssh gets this protocol assigned to
    them.  All communication between game and account goes through
    here.

    """

    def __init__(self, starttuple):
        """
        For setting up the account.  If account is not None then we'll
        login automatically.

        Args:
            starttuple (tuple): A (account, factory) tuple.

        """
        self.protocol_key = "ssh"
        self.authenticated_account = starttuple[0]
        # obs must not be called self.factory, that gets overwritten!
        self.cfactory = starttuple[1]

    def terminalSize(self, width, height):
        """
        Initialize the terminal and connect to the new session.

        Args:
            width (int): Width of terminal.
            height (int): Height of terminal.

        """
        # Clear the previous input line, redraw it at the new
        # cursor position
        self.terminal.eraseDisplay()
        self.terminal.cursorHome()
        self.width = width
        self.height = height

        # Set color defaults
        for color in ("ANSI", "XTERM256", "TRUECOLOR"):
            self.protocol_flags[color] = True

        # initialize the session
        client_address = self.getClientAddress()
        client_address = client_address.host if client_address else None
        self.init_session("ssh", client_address, self.cfactory.sessionhandler)

        # since we might have authenticated already, we might set this here.
        if self.authenticated_account:
            self.logged_in = True
            self.uid = self.authenticated_account.id

        """
        This is called when the connection is first established.

        """
        recvline.HistoricRecvLine.connectionMade(self)
        self.keyHandlers[CTRL_C] = self.handle_INT
        self.keyHandlers[CTRL_D] = self.handle_EOF
        self.keyHandlers[CTRL_L] = self.handle_FF
        self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT

        # initalize

    def handle_INT(self):
        """
        Handle ^C as an interrupt keystroke by resetting the current
        input variables to their initial state.

        """
        self.lineBuffer = []
        self.lineBufferIndex = 0

        self.terminal.nextLine()
        self.terminal.write("KeyboardInterrupt")

    def handle_EOF(self):
        """
        Handles EOF generally used to exit.

        """
        if self.lineBuffer:
            self.terminal.write("\a")
        else:
            self.handle_QUIT()

    def handle_FF(self):
        """
        Handle a 'form feed' byte - generally used to request a screen
        refresh/redraw.

        """

    def handle_QUIT(self):
        """
        Quit, end, and lose the connection.

        """
        self.terminal.loseConnection()

    def connectionLost(self, reason=None):
        """
        reason. It can also be called directly, from the disconnect
        method.

        Args:
            reason (str): Motivation for loosing connection.

        """
        insults.TerminalProtocol.connectionLost(self, reason)

    def getClientAddress(self):
        """
        Get client address.

        Returns:
            address_and_port (tuple): The client's address and port in
                a tuple. For example `('127.0.0.1', 41917)`.

        """
        return self.terminal.transport.getPeer()

    def lineReceived(self, string):
        """
        Communication User -> Evennia. Any line return indicates a
        command for the purpose of the MUD.  So we take the user input
        and pass it on to the game engine.

        Args:
            string (str): Input text.

        """
        self.sessionhandler.data_in(self, text=string)

    def sendLine(self, string):
        """
        Communication Evennia -> User.  Any string sent should
        already have been properly formatted and processed before
        reaching this point.

        Args:
            string (str): Output text.

        """
        for line in string.split("\n"):
            # the telnet-specific method for sending
            self.terminal.write(line)
            self.terminal.nextLine()

    # session-general method hooks

        """
        Called when this session gets authenticated by the server.
        """
        pass

    def disconnect(self, reason="Connection closed. Goodbye for now."):
        """
        Disconnect from server.

        Args:
            reason (str): Motivation for disconnect.

        """
        if reason:
            self.data_out(text=((reason,), {}))
        self.connectionLost(reason)

        """
        Data Evennia -> User

            kwargs (any): Options to the protocol.

        """
        self.sessionhandler.data_out(self, **kwargs)

        """
        Send text data. This is an in-band telnet operation.

        Args:
            text (str): The first argument is always the text string to send. No other arguments
                are considered.
            options (dict): Send-option flags (booleans)

                - mxp: enforce mxp link support.
                - ansi: enforce no ansi colors.
                - xterm256: enforce xterm256 colors, regardless of ttype setting.
                - nocolor: strip all colors.
                - raw: pass string through without any ansi processing
                  (i.e. include evennia ansi markers but do not
                  convert them into ansi tokens)
                - echo: turn on/off line echo on the client. turn
                  off line echo for client, for example for password.
                  note that it must be actively turned back on again!

        """
        # print "telnet.send_text", args,kwargs  # DEBUG
            return
        text = to_str(text)

        # handle arguments
        options = kwargs.get("options", {})
        xterm256 = options.get("xterm256", flags.get("XTERM256", True))
        useansi = options.get("ansi", flags.get("ANSI", True))
        raw = options.get("raw", flags.get("RAW", False))
        nocolor = options.get("nocolor", flags.get("NOCOLOR") or not (xterm256 or useansi))
        # echo = options.get("echo", None)  # DEBUG
        screenreader = options.get("screenreader", flags.get("SCREENREADER", False))

        if screenreader:
            # screenreader mode cleans up output
            text = ansi.parse_ansi(text, strip_ansi=True, xterm256=False, mxp=False)
            text = _RE_SCREENREADER_REGEX.sub("", text)

        if raw:
            # no processing
            self.sendLine(text)
            return
        else:
            # we need to make sure to kill the color at the end in order
            # to match the webclient output.
            linetosend = ansi.parse_ansi(
                _RE_N.sub("", text) + ("||n" if text.endswith("|") else "|n"),
                strip_ansi=nocolor,
                xterm256=xterm256,
                mxp=False,
            )
            self.sendLine(linetosend)

    def send_prompt(self, *args, **kwargs):
        self.send_text(*args, **kwargs)

        pass

class ExtraInfoAuthServer(SSHUserAuthServer):

    def auth_password(self, packet):
        """
        Password authentication.

        Used mostly for setting up the transport so we can query
        username and password later.

        Args:
            packet (Packet): Auth packet.

        """
        password = common.getNS(packet[1:])[0]
        c = checkers.UsernamePassword(self.user, password)
        c.transport = self.transport
        return self.portal.login(c, None, IConchUser).addErrback(self._ebPassword)

class AccountDBPasswordChecker(object):
    """
    Checks the django db for the correct credentials for
    username/password otherwise it returns the account or None which is
    useful for the Realm.

    """

    credentialInterfaces = (checkers.IUsernamePassword,)

    def __init__(self, factory):
        """
        Initialize the factory.

        Args:
            factory (SSHFactory): Checker factory.

        """

    def requestAvatarId(self, c):
        """
        Generic credentials.

        """
        up = checkers.IUsernamePassword(c, None)
        username = up.username
        password = up.password
        account = AccountDB.objects.get_account_from_name(username)
        res = (None, self.factory)
        if account and account.check_password(password):
            res = (account, self.factory)
        return defer.succeed(res)

class PassAvatarIdTerminalRealm(TerminalRealm):
    """
    Returns an avatar that passes the avatarId through to the
    protocol.  This is probably not the best way to do it.

    """

    def _getAvatar(self, avatarId):
        comp = components.Componentized()
        user = self.userFactory(comp, avatarId)
        sess = self.sessionFactory(comp)

        sess.transportFactory = self.transportFactory
        sess.chainedProtocolFactory = lambda: self.chainedProtocolFactory(avatarId)

        comp.setComponent(iconch.IConchUser, user)
        comp.setComponent(iconch.ISession, sess)

        return user

class TerminalSessionTransport_getPeer(object):
    """
    Taken from twisted's TerminalSessionTransport which doesn't
    provide getPeer to the transport.  This one does.

    """

    def __init__(self, proto, chainedProtocol, avatar, width, height):
        self.proto = proto
        self.avatar = avatar
        self.chainedProtocol = chainedProtocol

        session = self.proto.session

        self.proto.makeConnection(
            _Glue(
                write=self.chainedProtocol.dataReceived,
                loseConnection=lambda: avatar.conn.sendClose(session),
                name="SSH Proto Transport",
            )
        )

        def loseConnection():
            self.proto.loseConnection()

        def getPeer():
            return session.conn.transport.transport.getPeer()

        self.chainedProtocol.makeConnection(
            _Glue(
                getPeer=getPeer,
                write=self.proto.write,
                loseConnection=loseConnection,
                name="Chained Proto Transport",
            )
        )

        self.chainedProtocol.terminalProtocol.terminalSize(width, height)

def getKeyPair(pubkeyfile, privkeyfile):
    """
    This function looks for RSA keypair files in the current directory. If they
    do not exist, the keypair is created.
    """

    if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)):
        # No keypair exists. Generate a new RSA keypair
        from cryptography.hazmat.backends import default_backend
        from cryptography.hazmat.primitives.asymmetric import rsa

        rsa_key = Key(
            rsa.generate_private_key(
                public_exponent=65537, key_size=_KEY_LENGTH, backend=default_backend()
            )
        )
        public_key_string = rsa_key.public().toString(type="OPENSSH").decode()
        private_key_string = rsa_key.toString(type="OPENSSH").decode()

        # save keys for the future.
        with open(privkeyfile, "wt") as pfile:
            pfile.write(private_key_string)
            print("Created SSH private key in '{}'".format(_PRIVATE_KEY_FILE))
        with open(pubkeyfile, "wt") as pfile:
            pfile.write(public_key_string)
            print("Created SSH public key in '{}'".format(_PUBLIC_KEY_FILE))
    else:
        with open(pubkeyfile) as pfile:
            public_key_string = pfile.read()
        with open(privkeyfile) as pfile:
            private_key_string = pfile.read()

    return Key.fromString(public_key_string), Key.fromString(private_key_string)

def makeFactory(configdict):
    """
    Creates the ssh server factory.
    """

    def chainProtocolFactory(username=None):
        return insults.ServerProtocol(
            configdict["protocolFactory"],
            *configdict.get("protocolConfigdict", (username,)),
            **configdict.get("protocolKwArgs", {}),
        )

    rlm = PassAvatarIdTerminalRealm()
    rlm.transportFactory = TerminalSessionTransport_getPeer
    rlm.chainedProtocolFactory = chainProtocolFactory
    factory = ConchFactory(Portal(rlm))
    factory.sessionhandler = configdict["sessions"]

    try:
        # create/get RSA keypair
        publicKey, privateKey = getKeyPair(_PUBLIC_KEY_FILE, _PRIVATE_KEY_FILE)
        factory.publicKeys = {b"ssh-rsa": publicKey}
        factory.privateKeys = {b"ssh-rsa": privateKey}
        print(_NO_AUTOGEN.format(err=err))

    factory.services = factory.services.copy()
    factory.services["ssh-userauth"] = ExtraInfoAuthServer

    factory.portal.registerChecker(AccountDBPasswordChecker(factory))

    return factory

"""
This is a simple context factory for auto-creating
SSL keys and certificates.

"""
import os
import sys

try:
    import OpenSSL
    from twisted.internet import ssl as twisted_ssl
except ImportError as error:
    errstr = """
    {err}
    SSL requires the PyOpenSSL library:
        pip install pyopenssl
    """
    raise ImportError(errstr.format(err=error))

# messages

NO_AUTOGEN = """

{err}
Evennia could not auto-generate the SSL private key. If this error
persists, create {keyfile} yourself using third-party tools.
"""

NO_AUTOCERT = """

{err}
Evennia's SSL context factory could not automatically, create an SSL
certificate {certfile}.

A private key {keyfile} was already created. Please create {certfile}
manually using the commands valid  for your operating system, for
example (linux, using the openssl program):
    {exestring}
"""

_TELNET_PROTOCOL_CLASS = class_from_module(settings.TELNET_PROTOCOL_CLASS)

class SSLProtocol(_TELNET_PROTOCOL_CLASS):
    """
    Communication is the same as telnet, except data transfer
    is done with encryption.

    """

        self.protocol_name = "ssl"

def verify_SSL_key_and_cert(keyfile, certfile):
    """
    This function looks for RSA key and certificate in the current
    directory. If files ssl.key and ssl.cert does not exist, they
    are created.

    """

    if not (os.path.exists(keyfile) and os.path.exists(certfile)):
        # key/cert does not exist. Create.
        import subprocess

        from Crypto.PublicKey import RSA
        from twisted.conch.ssh.keys import Key

        print("  Creating SSL key and certificate ... ", end=" ")

        try:
            # create the RSA key and store it.
            KEY_LENGTH = 2048
            rsa_key = Key(RSA.generate(KEY_LENGTH))
            key_string = rsa_key.toString(type="OPENSSH")
            with open(keyfile, "w+b") as fil:
                fil.write(key_string)
            print(NO_AUTOGEN.format(err=err, keyfile=keyfile))
            sys.exit(5)

        # try to create the certificate
        CERT_EXPIRE = 365 * 20  # twenty years validity
        # default:
        # openssl req -new -x509 -key ssl.key -out ssl.cert -days 7300
        exestring = "openssl req -new -x509 -key %s -out %s -days %s" % (
            keyfile,
            certfile,
            CERT_EXPIRE,
        )
        try:
            subprocess.call(exestring)
        except OSError as err:
            raise OSError(
                NO_AUTOCERT.format(err=err, certfile=certfile, keyfile=keyfile, exestring=exestring)
            )
        print("done.")

def getSSLContext():
    """
    This is called by the portal when creating the SSL context
    server-side.

    Returns:
        ssl_context (tuple): A key and certificate that is either
            existing previously or or created on the fly.

    """
    keyfile = os.path.join(_GAME_DIR, "server", "ssl.key")
    certfile = os.path.join(_GAME_DIR, "server", "ssl.cert")

    verify_SSL_key_and_cert(keyfile, certfile)
    return twisted_ssl.DefaultOpenSSLContextFactory(keyfile, certfile)

"""

SUPPRESS-GO-AHEAD

This supports suppressing or activating Evennia
the GO-AHEAD telnet operation after every server reply.
If the client sends no explicit DONT SUPRESS GO-AHEAD,
Evennia will default to supressing it since many clients
will fail to use it and has no knowledge of this standard.

It is set as the NOGOAHEAD protocol_flag option.

http://www.faqs.org/rfcs/rfc858.html

"""

SUPPRESS_GA = bytes([3])  # b"\x03"

class SuppressGA:
    """
    Implements the SUPRESS-GO-AHEAD protocol. Add this to a variable on the telnet

    """

        """
        Initialize suppression of GO-AHEADs.

        Args:

        """

        self.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)

    def wont_suppress_ga(self, option):
        """
        Called when client requests to not suppress GA.

        Args:

        """
        self.protocol.protocol_flags["NOGOAHEAD"] = False

    def will_suppress_ga(self, option):
        """
        Client will suppress GA

        Args:

        """

"""
This module implements the telnet protocol.

This depends on a generic session module that implements
the actual login procedure of the game, tracks
sessions etc.

"""

import re

from twisted.conch.telnet import (
    ECHO,
    GA,
    IAC,
    LINEMODE,
    LINEMODE_EDIT,
    LINEMODE_TRAPSIG,
    MODE,
    NOP,
    NULL,
    WILL,
    WONT,
    StatefulTelnetProtocol,
    Telnet,
)

from evennia.server.portal import mssp, naws, suppress_ga, telnet_oob, ttype
from evennia.server.portal.mccp import MCCP, Mccp, mccp_compress
from evennia.server.portal.mxp import Mxp, mxp_parse
from evennia.utils.utils import class_from_module, to_bytes

_RE_LEND = re.compile(rb"\n$|\r$|\r\n$|\r\x00$|", re.MULTILINE)
_RE_LINEBREAK = re.compile(rb"\n\r|\r\n|\n|\r", re.DOTALL + re.MULTILINE)
)
_IDLE_COMMAND = str.encode(settings.IDLE_COMMAND + "\n")

# identify HTTP indata
_HTTP_REGEX = re.compile(
    b"(GET|HEAD|POST|PUT|DELETE|TRACE|OPTIONS|CONNECT|PATCH) (.*? HTTP/[0-9]\.[0-9])", re.I
)

    """
    This is Evennia's Telnet port and cannot be used for regular HTTP traffic.
    Use a telnet client to connect here and point your browser to the server's
    dedicated web port instead.

    """.strip(),
    "utf-8",
)

class TelnetServerFactory(protocol.ServerFactory):
    """
    This exists only to name this better in logs.

    """

        return "Telnet"

class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
    """
    Each player connecting over telnet (ie using most traditional mud
    clients) gets a telnet protocol instance assigned to them.  All
    communication between game and player goes through here.

    """

        self.protocol_key = "telnet"

        """
        Unused by default, but a good place to put debug printouts
        of incoming data.

        """
        # print(f"telnet dataReceived: {data}")
        try:
            super().dataReceived(data)
        except ValueError as err:

            logger.log_err(f"Malformed telnet input: {err}")

        """

        """
        # important in order to work normally with standard telnet
        self.do(LINEMODE).addErrback(self._wont_linemode)
        self.line_buffer = b""
        client_address = self.transport.client
        client_address = client_address[0] if client_address else None
        # this number is counted down for every handshake that completes.
        # when it reaches 0 the portal/server syncs their data
        self.handshakes = 8  # suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp

        self.init_session(self.protocol_key, client_address, self.factory.sessionhandler)
        self.protocol_flags["ENCODING"] = settings.ENCODINGS[0] if settings.ENCODINGS else "utf-8"
        # add this new connection to sessionhandler so
        # the Server becomes aware of it.
        # change encoding to ENCODINGS[0] which reflects Telnet default encoding

        # suppress go-ahead
        self.sga = suppress_ga.SuppressGA(self)
        # negotiate client size
        self.naws = naws.Naws(self)
        # negotiate ttype (client info)
        # Obs: mudlet ttype does not seem to work if we start mccp before ttype. /Griatch
        self.ttype = ttype.Ttype(self)
        # negotiate mccp (data compression) - turn this off for wireshark analysis
        self.mccp = Mccp(self)
        # negotiate mssp (crawler communication)
        self.mssp = mssp.Mssp(self)
        # oob communication (MSDP, GMCP) - two handshake calls!
        self.oob = telnet_oob.TelnetOOB(self)
        # mxp support
        self.mxp = Mxp(self)

        from evennia.utils.utils import delay

        # timeout the handshakes in case the client doesn't reply at all
        self._handshake_delay = delay(2, callback=self.handshake_done, timeout=True)

        # TCP/IP keepalive watches for dead links
        self.transport.setTcpKeepAlive(1)
        # The TCP/IP keepalive is not enough for some networks;
        # we have to complement it with a NOP keep-alive.
        self.protocol_flags["NOPKEEPALIVE"] = True
        self.nop_keep_alive = None
        self.toggle_nop_keepalive()

    def _wont_linemode(self, *args):
        """
        Client refuses do(linemode). This is common for MUD-specific
        clients, but we must ask for the sake of raw telnet. We ignore
        this error.

        """
        pass

    def _send_nop_keepalive(self):
        """
        Send NOP keepalive unless flag is set

        """
        if self.protocol_flags.get("NOPKEEPALIVE"):
            self._write(IAC + NOP)

    def toggle_nop_keepalive(self):
        """
        Allow to toggle the NOP keepalive for those sad clients that
        can't even handle a NOP instruction. This is turned off by the
        protocol_flag NOPKEEPALIVE (settable e.g. by the default
        `option` command).

        """
        if self.nop_keep_alive and self.nop_keep_alive.running:
            self.nop_keep_alive.stop()
        else:
            self.nop_keep_alive = LoopingCall(self._send_nop_keepalive)
            self.nop_keep_alive.start(30, now=False)

    def handshake_done(self, timeout=False):
        """
        This is called by all telnet extensions once they are finished.
        When all have reported, a sync with the server is performed.
        The system will force-call this sync after a small time to handle
        clients that don't reply to handshakes at all.

        """
        if timeout:
            if self.handshakes > 0:
                self.handshakes = 0
                self.sessionhandler.sync(self)
        else:
            self.handshakes -= 1
            if self.handshakes <= 0:
                # do the sync

        """

        """
        pass

    def enableRemote(self, option):
        """
        This sets up the remote-activated options we allow for this protocol.

        Args:
            option (char): The telnet option to enable.

        Returns:
            enable (bool): If this option should be enabled.

        """
        if option == LINEMODE:
            # make sure to activate line mode with local editing for all clients
            self.requestNegotiation(
                LINEMODE, MODE + bytes(chr(ord(LINEMODE_EDIT) + ord(LINEMODE_TRAPSIG)), "ascii")
            )
        else:
            return (
                option == ttype.TTYPE
                or option == naws.NAWS
                or option == MCCP
                or option == mssp.MSSP
                or option == ECHO
                or option == suppress_ga.SUPPRESS_GA
            )

    def disableRemote(self, option):
        return (
            option == LINEMODE
            or option == ttype.TTYPE
            or option == naws.NAWS
            or option == MCCP
            or option == mssp.MSSP
            or option == ECHO
            or option == suppress_ga.SUPPRESS_GA
        )

    def enableLocal(self, option):
        """
        Call to allow the activation of options for this protocol

        Args:
            option (char): The telnet option to enable locally.

        Returns:

        """
        return (
        )

    def disableLocal(self, option):
        """
        Disable a given option locally.

        Args:
            option (char): The telnet option to disable locally.

        """
        if option == ECHO:
        if option == MCCP:
            self.mccp.no_mccp(option)
        else:
            try:
                return super().disableLocal(option)
                from evennia.utils import logger

        """
        this is executed when the connection is lost for whatever
        method

        Args:
            reason (str): Motivation for losing connection.

        """

    def applicationDataReceived(self, data):
        """
        Telnet method called when non-telnet-command data is coming in
        over the telnet connection. We pass it on to the game engine
        directly.

        Args:
            data (str): Incoming data.

        """
        if not data:
            data = [data]
        elif data.strip() == NULL:
            # this is an ancient type of keepalive used by some
            # legacy clients. There should never be a reason to send a
            # lone NULL character so this seems to be a safe thing to
            # support for backwards compatibility. It also stops the
            # NULL from continuously popping up as an unknown command.
            data = [_IDLE_COMMAND]
        else:
            data = _RE_LINEBREAK.split(data)

            if len(data) > 2 and _HTTP_REGEX.match(data[0]):
                # guard against HTTP request on the Telnet port; we
                # block and kill the connection.
                self.transport.write(_HTTP_WARNING)
                self.transport.loseConnection()
                return

            if self.line_buffer and len(data) > 1:
                # buffer exists, it is terminated by the first line feed
                data[0] = self.line_buffer + data[0]
                self.line_buffer = b""
            # if the last data split is empty, it means all splits have
            # line breaks, if not, it is unterminated and must be
            # buffered.
            self.line_buffer += data.pop()
        # send all data chunks
        for dat in data:
            self.data_in(text=dat + b"\n")

    def _write(self, data):
        """
        Hook overloading the one used in plain telnet

        """
        data = data.replace(b"\n", b"\r\n").replace(b"\r\r\n", b"\r\n")
        super()._write(mccp_compress(self, data))

    def sendLine(self, line):
        """
        Hook overloading the one used by linereceiver.

        Args:
            line (str): Line to send.

        """
        line = to_bytes(line, self)
        # escape IAC in line mode, and correctly add \r\n (the TELNET end-of-line)
        line = line.replace(IAC, IAC + IAC)
        line = line.replace(b"\n", b"\r\n")
        if not line.endswith(b"\r\n") and self.protocol_flags.get("FORCEDENDLINE", True):
            line += b"\r\n"
        if not self.protocol_flags.get("NOGOAHEAD", True):
            line += IAC + GA
        return self.transport.write(mccp_compress(self, line))

    # Session hooks

        """

        Args:
            reason (str, optional): Reason for disconnecting.

        """
        self.data_out(text=((reason,), {}))

        """
        Data User -> Evennia

            kwargs (any): Options from the protocol.

        """
        # text = timetrace(text, "telnet.data_in")  # DEBUG

        self.sessionhandler.data_in(self, **kwargs)

        """

            kwargs (any): Options to the protocol

        """

    # send_* methods

        """

        Args:
            options (dict): Send-option flags

               - mxp: Enforce MXP link support.
               - ansi: Enforce no ANSI colors.
               - xterm256: Enforce xterm256 colors, regardless of TTYPE.
               - noxterm256: Enforce no xterm256 color support, regardless of TTYPE.
               - nocolor: Strip all Color, regardless of ansi/xterm256 setting.
               - raw: Pass string through without any ansi processing
                    (i.e. include Evennia ansi markers but do not
                    convert them into ansi tokens)
               - echo: Turn on/off line echo on the client. Turn
                    off line echo for client, for example for password.
                    Note that it must be actively turned back on again!

        """
            return

        xterm256 = options.get(
            "xterm256", flags.get("XTERM256", False) if flags.get("TTYPE", False) else True
        )
        useansi = options.get(
            "ansi", flags.get("ANSI", False) if flags.get("TTYPE", False) else True
        )
        echo = options.get("echo", None)
        mxp = options.get("mxp", flags.get("MXP", False))

        if options.get("send_prompt"):
            # send a prompt instead.
            prompt = text
            if not raw:
                # processing
                prompt = ansi.parse_ansi(
                    _RE_N.sub("", prompt) + ("||n" if prompt.endswith("|") else "|n"),
                    strip_ansi=nocolor,
                    xterm256=xterm256,
                )
                if mxp:
                    prompt = mxp_parse(prompt)
            prompt = to_bytes(prompt, self)
            prompt = prompt.replace(IAC, IAC + IAC).replace(b"\n", b"\r\n")
            if not self.protocol_flags.get(
                "NOPROMPTGOAHEAD", self.protocol_flags.get("NOGOAHEAD", True)
            ):
                prompt += IAC + GA
            self.transport.write(mccp_compress(self, prompt))
        else:
            if echo is not None:
                # turn on/off echo. Note that this is a bit turned around since we use
                # echo as if we are "turning off the client's echo" when telnet really
                # handles it the other way around.
                if echo:
                    # by telling the client that WE WON'T echo, the client knows
                    # that IT should echo. This is the expected behavior from
                    # our perspective.
                    self.transport.write(mccp_compress(self, IAC + WONT + ECHO))
                else:
                    # by telling the client that WE WILL echo, the client can
                    # safely turn OFF its OWN echo.
                    self.transport.write(mccp_compress(self, IAC + WILL + ECHO))
            if raw:
                # no processing
                self.sendLine(text)
                return
            else:
                # we need to make sure to kill the color at the end in order
                # to match the webclient output.
                linetosend = ansi.parse_ansi(
                    _RE_N.sub("", text) + ("||n" if text.endswith("|") else "|n"),
                    mxp=mxp,
                )
                if mxp:
                    linetosend = mxp_parse(linetosend)
                self.sendLine(linetosend)

        """
        Send a prompt - a text without a line end. See send_text for argument options.

        """
        kwargs["options"].update({"send_prompt": True})

    def send_default(self, cmdname, *args, **kwargs):
        """
        Send other oob data

        """
        if not cmdname == "options":
            self.oob.data_out(cmdname, *args, **kwargs)

"""

Telnet OOB (Out of band communication)

OOB protocols allow for asynchronous communication between Evennia and
compliant telnet clients. The "text" type of send command will always
be sent "in-band", appearing in the client's main text output. OOB
commands, by contrast, can have many forms and it is up to the client
how and if they are handled.  Examples of OOB instructions could be to
instruct the client to play sounds or to update a graphical health
bar.

Note that in Evennia's Web client, all send commands are "OOB
commands", (including the "text" one), there is no equivalence to
MSDP/GMCP for the webclient since it doesn't need it.

This implements the following telnet OOB communication protocols:

- MSDP (Mud Server Data Protocol), as per http://tintin.sourceforge.net/msdp/
- GMCP (Generic Mud Communication Protocol) as per
  http://www.ironrealms.com/rapture/manual/files/FeatGMCP-txt.html#Generic_MUD_Communication_Protocol%28GMCP%29

----

"""
import re

# General Telnet
from twisted.conch.telnet import IAC, SB, SE

# MSDP-relevant telnet cmd/opt-codes
MSDP = bytes([69])
MSDP_VAR = bytes([1])
MSDP_VAL = bytes([2])
MSDP_TABLE_OPEN = bytes([3])
MSDP_TABLE_CLOSE = bytes([4])

MSDP_ARRAY_OPEN = bytes([5])
MSDP_ARRAY_CLOSE = bytes([6])

# GMCP
GMCP = bytes([201])

# pre-compiled regexes
# returns 2-tuple
msdp_regex_table = re.compile(
    rb"%s\s*(\w*?)\s*%s\s*%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_TABLE_OPEN, MSDP_TABLE_CLOSE)
)
msdp_regex_array = re.compile(
    rb"%s\s*(\w*?)\s*%s\s*%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_ARRAY_OPEN, MSDP_ARRAY_CLOSE)
)
msdp_regex_var = re.compile(rb"%s" % MSDP_VAR)
msdp_regex_val = re.compile(rb"%s" % MSDP_VAL)

EVENNIA_TO_GMCP = {
    "client_options": "Core.Supports.Get",
    "get_inputfuncs": "Core.Commands.Get",
    "get_value": "Char.Value.Get",
    "repeat": "Char.Repeat.Update",
    "monitor": "Char.Monitor.Update",
}

# MSDP/GMCP communication handler

class TelnetOOB:
    """
    Implements the MSDP and GMCP protocols.
    """

        """
        Initiates by storing the protocol on itself and trying to
        determine if the client supports MSDP.

        Args:
            protocol (Protocol): The active protocol.

        """
        self.protocol.protocol_flags["OOB"] = False
        self.MSDP = False
        self.GMCP = False
        # ask for the available protocols and assign decoders
        # (note that handshake_done() will be called twice!)
        self.protocol.negotiationMap[MSDP] = self.decode_msdp
        self.protocol.negotiationMap[GMCP] = self.decode_gmcp
        self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp)
        self.protocol.will(GMCP).addCallbacks(self.do_gmcp, self.no_gmcp)
        self.oob_reported = {}

    def no_msdp(self, option):
        """
        Client reports No msdp supported or wanted.

        Args:

        """
        # no msdp, check GMCP

    def do_msdp(self, option):
        """
        Client reports that it supports msdp.

        Args:

        """
        self.MSDP = True
        self.protocol.protocol_flags["OOB"] = True

    def no_gmcp(self, option):
        """
        If this is reached, it means neither MSDP nor GMCP is
        supported.

        Args:

        """

    def do_gmcp(self, option):
        """
        Called when client confirms that it can do MSDP or GMCP.

        Args:

        """
        self.GMCP = True

    # encoders

    def encode_msdp(self, cmdname, *args, **kwargs):
        """
        Encode into a valid MSDP command.

        Args:
            cmdname (str): Name of send instruction.
            args, kwargs (any): Arguments to OOB command.

        Notes:
            The output of this encoding will be
            MSDP structures on these forms:
            ::

                [cmdname, [], {}]           -> VAR cmdname VAL ""
                [cmdname, [arg], {}]        -> VAR cmdname VAL arg
                [cmdname, [args],{}]        -> VAR cmdname VAL ARRAYOPEN VAL arg VAL arg ... ARRAYCLOSE
                [cmdname, [], {kwargs}]     -> VAR cmdname VAL TABLEOPEN VAR key VAL val ... TABLECLOSE
                [cmdname, [args], {kwargs}] -> VAR cmdname VAL ARRAYOPEN VAL arg VAL arg ... ARRAYCLOSE
                                               VAR cmdname VAL TABLEOPEN VAR key VAL val ... TABLECLOSE

            Further nesting is not supported, so if an array argument
            consists of an array (for example), that array will be
            json-converted to a string.

        """
        msdp_cmdname = "{msdp_var}{msdp_cmdname}{msdp_val}".format(
            msdp_var=MSDP_VAR.decode(), msdp_cmdname=cmdname, msdp_val=MSDP_VAL.decode()
        )

        if not (args or kwargs):
            return msdp_cmdname.encode()

        # print("encode_msdp in:", cmdname, args, kwargs)  # DEBUG

        msdp_args = ""
        if args:
            msdp_args = msdp_cmdname
            if len(args) == 1:
                msdp_args += args[0]
            else:
                msdp_args += (
                    "{msdp_array_open}"
                    "{msdp_args}"
                    "{msdp_array_close}".format(
                        msdp_array_open=MSDP_ARRAY_OPEN.decode(),
                        msdp_array_close=MSDP_ARRAY_CLOSE.decode(),
                        msdp_args="".join("%s%s" % (MSDP_VAL.decode(), val) for val in args),
                    )
                )

        msdp_kwargs = ""
        if kwargs:
            msdp_kwargs = msdp_cmdname
            msdp_kwargs += (
                "{msdp_table_open}"
                "{msdp_kwargs}"
                "{msdp_table_close}".format(
                    msdp_table_open=MSDP_TABLE_OPEN.decode(),
                    msdp_table_close=MSDP_TABLE_CLOSE.decode(),
                    msdp_kwargs="".join(
                        "%s%s%s%s" % (MSDP_VAR.decode(), key, MSDP_VAL.decode(), val)
                        for key, val in kwargs.items()
                    ),
                )
            )

        msdp_string = msdp_args + msdp_kwargs

        # print("msdp_string:", msdp_string)  # DEBUG
        return msdp_string.encode()

    def encode_gmcp(self, cmdname, *args, **kwargs):
        """
        Encode into GMCP messages.

        Args:
            cmdname (str): GMCP OOB command name.

        Notes:
            GMCP messages will be outgoing on the following
            form (the non-JSON cmdname at the start is what
            IRE games use, supposedly, and what clients appear
            to have adopted). A cmdname without Package will end
            up in the Core package, while Core package names will
            be stripped on the Evennia side.
            ::

                [cmd_name, [], {}]          -> Cmd.Name
                [cmd_name, [arg], {}]       -> Cmd.Name arg
                [cmd_name, [args],{}]       -> Cmd.Name [args]
                [cmd_name, [], {kwargs}]    -> Cmd.Name {kwargs}
                [cmdname, [args, {kwargs}]  -> Core.Cmdname [[args],{kwargs}]

            For more flexibility with certain clients, if `cmd_name` is capitalized,
            Evennia will leave its current capitalization (So CMD_nAmE would be sent
            as CMD.nAmE but cMD_Name would be Cmd.Name)

        Notes:
            There are also a few default mappings between evennia outputcmds and GMCP:
            ::

                client_options -> Core.Supports.Get
                get_inputfuncs -> Core.Commands.Get
                get_value      -> Char.Value.Get
                repeat         -> Char.Repeat.Update
                monitor        -> Char.Monitor.Update

        """

        if cmdname in EVENNIA_TO_GMCP:
            gmcp_cmdname = EVENNIA_TO_GMCP[cmdname]
        elif "_" in cmdname:
            if cmdname.istitle():
                # leave without capitalization
                gmcp_cmdname = ".".join(word for word in cmdname.split("_"))
            else:
                gmcp_cmdname = ".".join(word.capitalize() for word in cmdname.split("_"))
        else:
            gmcp_cmdname = "Core.%s" % (cmdname if cmdname.istitle() else cmdname.capitalize())

            gmcp_string = gmcp_cmdname
        elif args:
                args = args[0]
            if kwargs:
                gmcp_string = "%s %s" % (gmcp_cmdname, json.dumps([args, kwargs]))
            else:
                gmcp_string = "%s %s" % (gmcp_cmdname, json.dumps(args))
        else:  # only kwargs
            gmcp_string = "%s %s" % (gmcp_cmdname, json.dumps(kwargs))

        # print("gmcp string", gmcp_string)  # DEBUG
        return gmcp_string.encode()

    def decode_msdp(self, data):
        """
        Decodes incoming MSDP data.

        Args:
            data (str or list): MSDP data.

        Notes:
            Clients should always send MSDP data on
            one of the following forms:
            ::

                cmdname ''          -> [cmdname, [], {}]
                cmdname val         -> [cmdname, [val], {}]
                cmdname array       -> [cmdname, [array], {}]
                cmdname table       -> [cmdname, [], {table}]
                cmdname array cmdname table -> [cmdname, [array], {table}]

            Observe that all MSDP_VARS are used to identify cmdnames,
            so if there are multiple arrays with the same cmdname
            given, they will be merged into one argument array, same
            for tables. Different MSDP_VARS (outside tables) will be
            identified as separate cmdnames.

        """
        if isinstance(data, list):
            data = b"".join(data)

        # print("decode_msdp in:", data)  # DEBUG

        tables = {}
        arrays = {}
        variables = {}

        # decode tables
        for key, table in msdp_regex_table.findall(data):
            key = key.decode()
            tables[key] = {} if key not in tables else tables[key]
            for varval in msdp_regex_var.split(table)[1:]:
                var, val = msdp_regex_val.split(varval, 1)
                var, val = var.decode(), val.decode()
                if var:
                    tables[key][var] = val

        # decode arrays from all that was not a table
        data_no_tables = msdp_regex_table.sub(b"", data)
        for key, array in msdp_regex_array.findall(data_no_tables):
            arrays[key] = [] if key not in arrays else arrays[key]
            parts = msdp_regex_val.split(array)
            parts = [part.decode() for part in parts]
            if len(parts) == 2:
                arrays[key].append(parts[1])
            elif len(parts) > 1:
                arrays[key].extend(parts[1:])

        # decode remainders from all that were not tables or arrays
        data_no_tables_or_arrays = msdp_regex_array.sub(b"", data_no_tables)
        for varval in msdp_regex_var.split(data_no_tables_or_arrays):
            # get remaining varvals after cleaning away tables/arrays. If mathcing
            # an existing key in arrays, it will be added as an argument to that command,
            # otherwise it will be treated as a command without argument.
            parts = msdp_regex_val.split(varval)
                variables[parts[0]] = parts[1]
                variables[parts[0]] = parts[1:]

        cmds = {}
        # merge matching table/array/variables together
        for key, table in tables.items():
            args, kwargs = [], table
            if key in arrays:
                args.extend(arrays.pop(key))
            if key in variables:
                args.append(variables.pop(key))
            cmds[key] = [args, kwargs]

        for key, arr in arrays.items():
            args, kwargs = arr, {}

        for key, var in variables.items():
            cmds[key] = [[var], {}]

        # remap the 'generic msdp commands' to avoid colliding with builtins etc
        # by prepending "msdp_"
        lower_case = {key.lower(): key for key in cmds}
        for remap in ("list", "report", "reset", "send", "unreport"):
            if remap in lower_case:
                cmds["msdp_{}".format(remap)] = cmds.pop(lower_case[remap])

        # print("msdp data in:", cmds)  # DEBUG
        self.protocol.data_in(**cmds)

    def decode_gmcp(self, data):
        """
        Decodes incoming GMCP data on the form 'varname <structure>'.

        Args:
            data (str or list): GMCP data.

        Notes:
            Clients send data on the form "Module.Submodule.Cmdname <structure>".
            We assume the structure is valid JSON.

            The following is parsed into Evennia's formal structure:
            ::

                Core.Name                         -> [name, [], {}]
                Core.Name string                  -> [name, [string], {}]
                Core.Name [arg, arg,...]          -> [name, [args], {}]
                Core.Name {key:arg, key:arg, ...} -> [name, [], {kwargs}]
                Core.Name [[args], {kwargs}]      -> [name, [args], {kwargs}]

        """

        # print("decode_gmcp in:", data)  # DEBUG
        if data:
            try:
                cmdname, structure = data.split(None, 1)
                cmdname, structure = data, b""
            cmdname = cmdname.replace(b".", b"_")
            try:
                structure = json.loads(structure)
                # maybe the structure is not json-serialized at all
                pass
            args, kwargs = [], {}
            if is_iter(structure):
                if isinstance(structure, dict):
                    kwargs = {key: value for key, value in structure.items() if key}
                else:
                    args = list(structure)
            else:
                args = (structure,)
            if cmdname.lower().startswith(b"core_"):
                # if Core.cmdname, then use cmdname
                cmdname = cmdname[5:]
            self.protocol.data_in(**{cmdname.lower().decode(): [args, kwargs]})

    # access methods

    def data_out(self, cmdname, *args, **kwargs):
        """
        Return a MSDP- or GMCP-valid subnegotiation across the protocol.

        Args:
            cmdname (str): OOB-command name.

        """
        kwargs.pop("options", None)

        if self.MSDP:
            encoded_oob = self.encode_msdp(cmdname, *args, **kwargs)
            self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE)

        if self.GMCP:
            encoded_oob = self.encode_gmcp(cmdname, *args, **kwargs)
            self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE)

"""
This allows for running the telnet communication over an encrypted SSL tunnel. To use it, requires a
client supporting Telnet SSL.

The protocol will try to automatically create the private key and certificate on the server side
when starting and will warn if this was not possible. These will appear as files ssl.key and
ssl.cert in mygame/server/.

"""
import os

try:
    from OpenSSL import crypto
    {err}
    Telnet-SSL requires the PyOpenSSL library and dependencies:

        pip install pyopenssl pycrypto enum pyasn1 service_identity

    Stop and start Evennia again. If no certificate can be generated, you'll
    get a suggestion for a (linux) command to generate this locally.

    """

from evennia.server.portal.telnet import TelnetProtocol

_PRIVATE_KEY_LENGTH = 2048
_PRIVATE_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssl.key")
_PUBLIC_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssl-public.key")
_CERTIFICATE_FILE = os.path.join(_GAME_DIR, "server", "ssl.cert")
_CERTIFICATE_EXPIRE = 365 * 24 * 60 * 60 * 20  # 20 years
_CERTIFICATE_ISSUER = settings.SSL_CERTIFICATE_ISSUER

# messages

NO_AUTOGEN = f"""
Evennia could not auto-generate the SSL private- and public keys ({{err}}).
If this error persists, create them manually (using the tools for your OS). The files
should be placed and named like this:
"""

Evennia's could not auto-generate the SSL certificate ({{err}}).
The private key already exists here:
If this error persists, create the certificate manually (using the private key and
the tools for your OS). The file should be placed and named like this:
    {_CERTIFICATE_FILE}
"""

class SSLProtocol(TelnetProtocol):
    """
    is done with encryption set up by the portal at start time.

    """

        self.protocol_key = "telnet/ssl"

def verify_or_create_SSL_key_and_cert(keyfile, certfile):
    """
    Verify or create new key/certificate files.

    Args:
        keyfile (str): Path to ssl.key file.
        certfile (str): Parth to ssl.cert file.

    Notes:
        If files don't already exist, they are created.

    """

        try:
            # generate the keypair
            keypair = crypto.PKey()
            keypair.generate_key(crypto.TYPE_RSA, _PRIVATE_KEY_LENGTH)

            with open(_PRIVATE_KEY_FILE, "wt") as pfile:
                pfile.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, keypair).decode("utf-8"))
                print("Created SSL private key in '{}'.".format(_PRIVATE_KEY_FILE))

            with open(_PUBLIC_KEY_FILE, "wt") as pfile:
                pfile.write(crypto.dump_publickey(crypto.FILETYPE_PEM, keypair).decode("utf-8"))
                print("Created SSL public key in '{}'.".format(_PUBLIC_KEY_FILE))

            print(NO_AUTOGEN.format(err=err))

        else:
            try:
                # create certificate
                cert = crypto.X509()
                subj = cert.get_subject()
                for key, value in _CERTIFICATE_ISSUER.items():
                    setattr(subj, key, value)
                cert.set_issuer(subj)

                cert.set_serial_number(1000)
                cert.gmtime_adj_notBefore(0)
                cert.gmtime_adj_notAfter(_CERTIFICATE_EXPIRE)
                cert.set_pubkey(keypair)
                cert.sign(keypair, "sha1")

                with open(_CERTIFICATE_FILE, "wt") as cfile:
                    cfile.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8"))
                    print("Created SSL certificate in '{}'.".format(_CERTIFICATE_FILE))

                print(NO_AUTOCERT.format(err=err))

    """

    Returns:
            existing previously or created on the fly.

    """

    if verify_or_create_SSL_key_and_cert(_PRIVATE_KEY_FILE, _CERTIFICATE_FILE):
        return twisted_ssl.DefaultOpenSSLContextFactory(_PRIVATE_KEY_FILE, _CERTIFICATE_FILE)
    else:

try:
    from django.utils.unittest import TestCase
    from django.test import TestCase

try:
    from django.utils import unittest
    import unittest

import sys

from autobahn.twisted.websocket import WebSocketServerFactory
from mock import MagicMock, Mock
from twisted.conch.telnet import DO, DONT, IAC, NAWS, SB, SE, WILL
from twisted.test import proto_helpers

from evennia.server.portal import irc
from evennia.server.portal.portalsessionhandler import PortalSessionHandler
from evennia.server.portal.service import EvenniaPortalService

from .amp import (
    AMP_MAXLEN,
    AMPMultiConnectionProtocol,
    MsgPortal2Server,
    MsgServer2Portal,
)
from .amp_server import AMPServerFactory
from .mccp import MCCP
from .mssp import MSSP
from .mxp import MXP
from .naws import DEFAULT_HEIGHT, DEFAULT_WIDTH
from .suppress_ga import SUPPRESS_GA
from .telnet import TelnetProtocol, TelnetServerFactory
from .telnet_oob import MSDP, MSDP_VAL, MSDP_VAR
from .ttype import IS, TTYPE
from .webclient import WebSocketClient

class TestAMPServer(TwistedTestCase):
    """
    Test AMP communication
    """

        portal = Mock()
        factory = AMPServerFactory(portal)
        self.proto = factory.buildProtocol(("localhost", 0))
        self.transport = MagicMock()  # proto_helpers.StringTransport()
        self.transport.client = ["localhost"]
        self.transport.write = MagicMock()

    def test_amp_out(self):
        self.proto.makeConnection(self.transport)

        self.proto.data_to_server(MsgServer2Portal, 1, test=2)

        if pickle.HIGHEST_PROTOCOL == 5:
            # Python 3.8+
            byte_out = (
                b"\x00\x04_ask\x00\x011\x00\x08_command\x00\x10MsgServer2Portal\x00\x0b"
                b"packed_data\x00 x\xdak`\x9d*\xc8\x00\x01\xde\x8c\xb5SzXJR"
                b"\x8bK\xa6x3\x15\xb7M\xd1\x03\x00VU\x07u\x00\x00"
            )
        elif pickle.HIGHEST_PROTOCOL == 4:
            # Python 3.7
                b"packed_data\x00 x\xdak`\x99*\xc8\x00\x01\xde\x8c\xb5SzXJR"
                b"\x8bK\xa6x3\x15\xb7M\xd1\x03\x00V:\x07t\x00\x00"
            )
        self.transport.write.assert_called_with(byte_out)
        with mock.patch("evennia.server.portal.amp.amp.AMP.dataReceived") as mocked_amprecv:
            self.proto.dataReceived(byte_out)
            mocked_amprecv.assert_called_with(byte_out)

    def test_amp_in(self):

        self.proto.data_to_server(MsgPortal2Server, 1, test=2)
                b"\x00\x04_ask\x00\x011\x00\x08_command\x00\x10MsgPortal2Server\x00\x0b"
            )
            )

    def test_large_msg(self):
        """
        Send message larger than AMP_MAXLEN - should be split into several
        """
        outstr = "test" * AMP_MAXLEN
        self.proto.data_to_server(MsgServer2Portal, 1, test=outstr)

            self.transport.write.assert_called_with(
                b"\x00\x04_ask\x00\x011\x00\x08_command\x00\x10MsgServer2Portal\x00\x0bpacked_data"
                b"\x00wx\xda\xed\xc6\xc1\t\x80 \x00@Q#=5Z\x0b\xb8\x80\x13\xe85h\x80\x8e\xbam`Dc\xf4><\xf8g"
                b"\x1a[\xf8\xda\x97\xa3_\xb1\x95\xdaz\xbe\xe7\x1a\xde\x03\x00\x00\x00\x00\x00\x00\x00"
                b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
                b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x1f\x1eP\x1d\x02\r\x00\rpacked_data.2"
                b"\x00Zx\xda\xed\xc3\x01\r\x00\x00\x08\xc0\xa0\xb4&\xf0\xfdg\x10a\xa3"
                b"\xd9RUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\xf5\xfb\x03m\xe0\x06"
                b"\x1d\x00\rpacked_data.3\x00Zx\xda\xed\xc3\x01\r\x00\x00\x08\xc0\xa0\xb4&\xf0\xfdg\x10a"
                b"\xa3fSUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\xf5\xfb\x03n\x1c"
                b"\x06\x1e\x00\rpacked_data.4\x00Zx\xda\xed\xc3\x01\t\x00\x00\x0c\x03\xa0\xb4O\xb0\xf5gA"
                b"\xae`\xda\x8b\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"
                b"\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"
                b"\xaa\xaa\xaa\xdf\x0fnI\x06,\x00\rpacked_data.5\x00\x18x\xdaK-.)I\xc5\x8e\xa7\xb22@\xc0"
                b"\x94\xe2\xb6)z\x00Z\x1e\x0e\xb6\x00\x00"
            )
                b"\x00wx\xda\xed\xc6\xc1\t\x80 \x00@Q#o\x8e\xd6\x02-\xe0\x04z\r\x1a\xa0\xa3m+$\xd2"
                b"\x18\xbe\x0f\x0f\xfe\x1d\xdf\x14\xfe\x8e\xedjO\xac\xb9\xd4v\xf6o\x0f\xf3\x00\x00"
                b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
                b"\x00X\xc3\x00P\x10\x02\x0c\x00\rpacked_data.2\x00Zx\xda\xed\xc3\x01\r\x00\x00\x08"
                b"\xc0\xa0\xb4&\xf0\xfdg\x10a\xa3\xd9RUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
                b"\xf5\xfb\x03m\xe0\x06\x1d\x00\rpacked_data.3\x00Zx\xda\xed\xc3\x01\r\x00\x00\x08"
                b"\xc0\xa0\xb4&\xf0\xfdg\x10a\xa3fSUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
                b"\xf5\xfb\x03n\x1c\x06\x1e\x00\rpacked_data.4\x00Zx\xda\xed\xc3\x01\t\x00\x00\x0c"
                b"\x03\xa0\xb4O\xb0\xf5gA\xae`\xda\x8b\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"
                b"\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"
                b"\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xdf\x0fnI\x06,\x00\rpacked_data.5"
                b"\x00\x18x\xdaK-.)I\xc5\x8e\xa7\xb22@\xc0\x94\xe2\xb6)z\x00Z\x1e\x0e\xb6\x00\x00"
            )

class TestIRC(TestCase):
    def test_plain_ansi(self):
        """
        Test that printable characters do not get mangled.
        """
        irc_ansi = irc.parse_ansi_to_irc(string.printable)
        ansi_irc = irc.parse_irc_to_ansi(string.printable)
        self.assertEqual(irc_ansi, string.printable)
        self.assertEqual(ansi_irc, string.printable)

    def test_bold(self):
        s_irc = "\x02thisisatest"
        s_eve = r"|hthisisatest"
        self.assertEqual(irc.parse_ansi_to_irc(s_eve), s_irc)
        self.assertEqual(s_eve, irc.parse_irc_to_ansi(s_irc))

    def test_italic(self):

    def test_colors(self):
        color_map = (
            ("\0030", r"|w"),
            ("\0031", r"|X"),
            ("\0032", r"|B"),
            ("\0033", r"|G"),
            ("\0034", r"|r"),
            ("\0035", r"|R"),
            ("\0036", r"|M"),
            ("\0037", r"|Y"),
            ("\0038", r"|y"),
            ("\0039", r"|g"),
            ("\00310", r"|C"),
            ("\00311", r"|c"),
            ("\00312", r"|b"),
            ("\00313", r"|m"),
            ("\00314", r"|x"),
            ("\00315", r"|W"),
            ("\00399,5", r"|[r"),
            ("\00399,3", r"|[g"),
            ("\00399,7", r"|[y"),
            ("\00399,2", r"|[b"),
            ("\00399,6", r"|[m"),
            ("\00399,10", r"|[c"),
            ("\00399,15", r"|[w"),
            ("\00399,1", r"|[x"),
        )

        for m in color_map:
            self.assertEqual(irc.parse_irc_to_ansi(m[0]), m[1])
            self.assertEqual(m[0], irc.parse_ansi_to_irc(m[1]))

    def test_identity(self):
        """
        Test that the composition of the function and
        its inverse gives the correct string.
        """

        s = r"|wthis|Xis|gis|Ma|C|complex|*string"

        self.assertEqual(irc.parse_irc_to_ansi(irc.parse_ansi_to_irc(s)), s)

class TestTelnet(TwistedTestCase):
        self.portal = EvenniaPortalService()
        evennia.EVENNIA_PORTAL_SERVICE = self.portal
        self.amp_server_factory = AMPServerFactory(self.portal)
        self.amp_server = self.amp_server_factory.buildProtocol("127.0.0.1")
        factory = TelnetServerFactory()
        factory.protocol = TelnetProtocol
        evennia.PORTAL_SESSION_HANDLER = PortalSessionHandler()
        factory.sessionhandler = evennia.PORTAL_SESSION_HANDLER
        factory.sessionhandler.portal = Mock()
        self.transport = proto_helpers.StringTransport()
        self.addCleanup(factory.sessionhandler.disconnect_all)

    @mock.patch("evennia.server.portal.portalsessionhandler.reactor", new=MagicMock())
    def test_mudlet_ttype(self):
        self.transport.setTcpKeepAlive = Mock()
        d = self.proto.makeConnection(self.transport)
        # test suppress_ga
        self.assertTrue(self.proto.protocol_flags["NOGOAHEAD"])
        self.proto.dataReceived(IAC + DONT + SUPPRESS_GA)
        self.assertFalse(self.proto.protocol_flags["NOGOAHEAD"])
        self.assertEqual(self.proto.handshakes, 7)
        # test naws
        self.assertEqual(self.proto.protocol_flags["SCREENWIDTH"], {0: DEFAULT_WIDTH})
        self.assertEqual(self.proto.protocol_flags["SCREENHEIGHT"], {0: DEFAULT_HEIGHT})
        self.proto.dataReceived(IAC + WILL + NAWS)
        self.proto.dataReceived(b"".join([IAC, SB, NAWS, b"", b"x", b"", b"d", IAC, SE]))
        self.assertEqual(self.proto.protocol_flags["SCREENWIDTH"][0], 78)
        self.assertEqual(self.proto.protocol_flags["SCREENHEIGHT"][0], 45)
        self.assertEqual(self.proto.handshakes, 6)
        # test ttype
        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.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)
        # test mccp
        self.proto.dataReceived(IAC + DONT + MCCP)
        self.assertFalse(self.proto.protocol_flags["MCCP"])
        self.assertEqual(self.proto.handshakes, 4)
        # test mssp
        self.proto.dataReceived(IAC + DONT + MSSP)
        self.assertEqual(self.proto.handshakes, 3)
        # test oob
        self.proto.dataReceived(IAC + DO + MSDP)
        self.proto.dataReceived(
            b"".join([IAC, SB, MSDP, MSDP_VAR, b"LIST", MSDP_VAL, b"COMMANDS", IAC, SE])
        )
        self.assertTrue(self.proto.protocol_flags["OOB"])
        self.assertEqual(self.proto.handshakes, 2)
        # test mxp
        self.proto.dataReceived(IAC + DONT + MXP)
        self.assertFalse(self.proto.protocol_flags["MXP"])
        self.assertEqual(self.proto.handshakes, 1)
        # clean up to prevent Unclean reactor
        self.proto.nop_keep_alive.stop()
        self.proto._handshake_delay.cancel()
        return d

class TestWebSocket(BaseEvenniaTest):
        self.proto = WebSocketClient()
        self.proto.factory = WebSocketServerFactory()
        self.proto.factory.sessionhandler = evennia.PORTAL_SESSION_HANDLER
        self.proto.sessionhandler = evennia.PORTAL_SESSION_HANDLER
        self.proto.sessionhandler.portal = Mock()
        self.proto.transport = proto_helpers.StringTransport()
        # self.proto.transport = proto_helpers.FakeDatagramTransport()
        self.proto.transport.client = ["localhost"]
        self.proto.transport.setTcpKeepAlive = Mock()
        self.proto.state = MagicMock()
        self.addCleanup(self.proto.factory.sessionhandler.disconnect_all)
        DelayedCall.debug = True

    def test_data_in(self):
        self.proto.sessionhandler.data_in = MagicMock()
        self.proto.onOpen()
        msg = json.dumps(["logged_in", (), {}]).encode()
        self.proto.onMessage(msg, isBinary=False)
        self.proto.sessionhandler.data_in.assert_called_with(self.proto, logged_in=[[], {}])
        sendStr = "You can get anything you want at Alice's Restaurant."
        msg = json.dumps(["text", (sendStr,), {}]).encode()
        self.proto.sessionhandler.data_in.assert_called_with(self.proto, text=[[sendStr], {}])

    def test_data_out(self):
        self.proto.sendLine = MagicMock()
        msg = json.dumps(["logged_in", (), {}])
        self.proto.sessionhandler.data_out(self.proto, text=[["Excepting Alice"], {}])
        self.proto.sendLine.assert_called_with(json.dumps(["text", ["Excepting Alice"], {}]))

"""
TTYPE (MTTS) - Mud Terminal Type Standard

This module implements the TTYPE telnet protocol as per
http://tintin.sourceforge.net/mtts/. It allows the server to ask the
client about its capabilities. If the client also supports TTYPE, it
will return with information such as its name, if it supports colour
etc. If the client does not support TTYPE, this will be ignored.

All data will be stored on the protocol's protocol_flags dictionary,
under the 'TTYPE' key.

"""

# telnet option codes
TTYPE = bytes([24])  # b"\x18"
SEND = bytes([1])  # b"\x01"

# terminal capabilities and their codes
MTTS = [
    (2048, "SSL"),
    (1024, "MSLP"),
    (512, "MNES"),
    (256, "TRUECOLOR"),
    (128, "PROXY"),
    (64, "SCREENREADER"),
    (32, "OSC_COLOR_PALETTE"),
    (16, "MOUSE_TRACKING"),
    (8, "XTERM256"),
    (4, "UTF-8"),
    (2, "VT100"),
    (1, "ANSI"),
]

class Ttype:
    """
    Handles ttype negotiations. Called and initiated by the
    telnet protocol.

    """

        """
        Initialize ttype by storing protocol on ourselves and calling
        the client to see if it supporst ttype.

        Args:
            protocol (Protocol): The protocol instance.

        Notes:
            The `self.ttype_step` indicates how far in the data
            retrieval we've gotten.

        """
        self.ttype_step = 0
        # we set FORCEDENDLINE for clients not supporting ttype
        self.protocol.protocol_flags["FORCEDENDLINE"] = True
        self.protocol.protocol_flags["TTYPE"] = False
        # is it a safe bet to assume ANSI is always supported?
        self.protocol.protocol_flags["ANSI"] = True
        # setup protocol to handle ttype initialization and negotiation
        self.protocol.negotiationMap[TTYPE] = self.will_ttype
        # ask if client will ttype, connect callback if it does.
        self.protocol.do(TTYPE).addCallbacks(self.will_ttype, self.wont_ttype)

    def wont_ttype(self, option):
        """
        Callback if ttype is not supported by client.

        Args:

        """

    def will_ttype(self, option):
        """
        Handles negotiation of the ttype protocol once the client has
        confirmed that it will respond with the ttype protocol.

        Args:

        Notes:
            The negotiation proceeds in several steps, each returning a
            certain piece of information about the client. All data is
            stored on protocol.protocol_flags under the TTYPE key.

        """
        options = self.protocol.protocol_flags

        if options and options.get("TTYPE", False) or self.ttype_step > 3:
            return

        try:
            option = b"".join(option).lstrip(IS).decode()
            # option is not on a suitable form for joining
            pass

        if self.ttype_step == 0:
            # just start the request chain
            self.protocol.requestNegotiation(TTYPE, SEND)

        elif self.ttype_step == 1:
            # this is supposed to be the name of the client/terminal.
            # For clients not supporting the extended TTYPE
            # definition, subsequent calls will just repeat-return this.
            try:
                clientname = option.upper()
                # malformed option (not a string)
                clientname = "UNKNOWN"

            # use name to identify support for xterm256. Many of these
            # only support after a certain version, but all support
            # it since at least 4 years. We assume recent client here for now.
            xterm256 = False
            if clientname.startswith("MUDLET"):
                # supports xterm256 stably since 1.1 (2010?)
                xterm256 = clientname.split("MUDLET", 1)[1].strip() >= "1.1"
                # Mudlet likes GA's on a prompt line for the prompt trigger to
                # match, if it's not wanting NOGOAHEAD.
                if not self.protocol.protocol_flags["NOGOAHEAD"]:
                    self.protocol.protocol_flags["NOGOAHEAD"] = True
                    self.protocol.protocol_flags["NOPROMPTGOAHEAD"] = False

            if (
                clientname.startswith("XTERM")
                or clientname.endswith("-256COLOR")
                or clientname
                in (
                    "ATLANTIS",  # > 0.9.9.0 (aug 2009)
                    "CMUD",  # > 3.04 (mar 2009)
                    "KILDCLIENT",  # > 2.2.0 (sep 2005)
                    "MUDLET",  # > beta 15 (sep 2009)
                    "MUSHCLIENT",  # > 4.02 (apr 2007)
                    "PUTTY",  # > 0.58 (apr 2005)
                    "BEIP",  # > 2.00.206 (late 2009) (BeipMu)
                    "POTATO",  # > 2.00 (maybe earlier)
                    "TINYFUGUE",  # > 4.x (maybe earlier)
                )
            ):
                xterm256 = True

            # all clients supporting TTYPE at all seem to support ANSI
            self.protocol.protocol_flags["ANSI"] = True
            self.protocol.protocol_flags["XTERM256"] = xterm256
            self.protocol.protocol_flags["CLIENTNAME"] = clientname

        elif self.ttype_step == 2:
            # this is a term capabilities flag
            term = option
            tupper = term.upper()
            # identify xterm256 based on flag
            xterm256 = (
                tupper.endswith("-256COLOR")
                or tupper.endswith("XTERM")  # Apple Terminal, old Tintin
                and not tupper.endswith("-COLOR")  # old Tintin, Putty
            )
            if xterm256:
                self.protocol.protocol_flags["ANSI"] = True
                self.protocol.protocol_flags["XTERM256"] = xterm256
            self.protocol.protocol_flags["TERM"] = term
            # request next information

        elif self.ttype_step == 3:
            # the MTTS bitstring identifying term capabilities
            if option.startswith("MTTS"):
                option = option[4:].strip()
                if option.isdigit():
                    # a number - determine the actual capabilities
                    option = int(option)
                    support = dict(
                        (capability, True) for bitval, capability in MTTS if option & bitval > 0
                    )
                    self.protocol.protocol_flags.update(support)
                else:
                    # some clients send erroneous MTTS as a string. Add directly.
                    self.protocol.protocol_flags[option.upper()] = True

            self.protocol.protocol_flags["TTYPE"] = True
            # we must sync ttype once it'd done
            self.protocol.handshake_done()
        self.ttype_step += 1

"""
Webclient based on websockets.

This implements a webclient with WebSockets (http://en.wikipedia.org/wiki/WebSocket)
by use of the autobahn-python package's implementation (https://github.com/crossbario/autobahn-python).
It is used together with evennia/web/media/javascript/evennia_websocket_webclient.js.

All data coming into the webclient is in the form of valid JSON on the form

`["inputfunc_name", [args], {kwarg}]`

which represents an "inputfunc" to be called on the Evennia side with *args, **kwargs.
The most common inputfunc is "text", which takes just the text input
from the command line and interprets it as an Evennia Command: `["text", ["look"], {}]`

"""
import html
import re

from autobahn.exception import Disconnected
from autobahn.twisted.websocket import WebSocketServerProtocol

from evennia.utils.ansi import parse_ansi
from evennia.utils.text2html import parse_html
from evennia.utils.utils import class_from_module, mod_import

)
_CLIENT_SESSIONS = mod_import(settings.SESSION_ENGINE).SessionStore
_UPSTREAM_IPS = settings.UPSTREAM_IPS

# Status Code 1000: Normal Closure
#   called when the connection was closed through JavaScript
CLOSE_NORMAL = WebSocketServerProtocol.CLOSE_STATUS_CODE_NORMAL

# Status Code 1001: Going Away
#   called when the browser is navigating away from the page
GOING_AWAY = WebSocketServerProtocol.CLOSE_STATUS_CODE_GOING_AWAY

class WebSocketClient(WebSocketServerProtocol, _BASE_SESSION_CLASS):
    """
    Implements the server-side of the Websocket connection.

    """

    # nonce value, used to prevent the webclient from erasing the
    # webclient_authenticated_uid value of csession on disconnect
    nonce = 0

        self.protocol_key = "webclient/websocket"
        self.browserstr = ""

    def get_client_session(self):
        """
        Get the Client browser session (used for auto-login based on browser session)

        Returns:
            csession (ClientSession): This is a django-specific internal representation
                of the browser session.

        """
        try:
            # client will connect with wsurl?csessid&browserid
            webarg = self.http_request_uri.split("?", 1)[1]
            # this may happen for custom webclients not caring for the
            # browser session.
            self.csessid = None

            logger.log_trace(str(self))

        self.csessid, *browserstr = webarg.split("&", 1)
        if browserstr:
            self.browserstr = str(browserstr[0])

        if self.csessid:
            return _CLIENT_SESSIONS(session_key=self.csessid)

        """
        This is called when the WebSocket connection is fully established.

        """

        if client_address in _UPSTREAM_IPS and "x-forwarded-for" in self.http_headers:
            addresses = [x.strip() for x in self.http_headers["x-forwarded-for"].split(",")]
            addresses.reverse()

            for addr in addresses:
                if addr not in _UPSTREAM_IPS:
                    client_address = addr
                    break

        self.init_session("websocket", client_address, self.factory.sessionhandler)

        csession = self.get_client_session()  # this sets self.csessid
        csessid = self.csessid
        uid = csession and csession.get("webclient_authenticated_uid", None)
        nonce = csession and csession.get("webclient_authenticated_nonce", 0)
        if uid:
            # the client session is already logged in.
            self.uid = uid
            self.nonce = nonce

            for old_session in self.sessionhandler.sessions_from_csessid(csessid):
                if (
                    hasattr(old_session, "websocket_close_code")
                    and old_session.websocket_close_code != CLOSE_NORMAL
                ):
                    # if we have old sessions with the same csession, they are remnants
                    self.sessid = old_session.sessid
                    self.sessionhandler.disconnect(old_session)

        browserstr = f":{self.browserstr}" if self.browserstr else ""
        self.protocol_flags["CLIENTNAME"] = f"Evennia Webclient (websocket{browserstr})"
        self.protocol_flags["UTF-8"] = True
        self.protocol_flags["OOB"] = True
        self.protocol_flags["TRUECOLOR"] = True
        self.protocol_flags["XTERM256"] = True
        self.protocol_flags["ANSI"] = True

        # watch for dead links
        # actually do the connection

        """

        Args:

        """
        csession = self.get_client_session()

        if csession:
            # if the nonce is different, webclient_authenticated_uid has been
            # set *before* this disconnect (disconnect called after a new client
            # connects, which occurs in some 'fast' browsers like Google Chrome
            # and Mobile Safari)
            if csession.get("webclient_authenticated_nonce", 0) == self.nonce:
                csession["webclient_authenticated_uid"] = None
                csession["webclient_authenticated_nonce"] = 0
                csession.save()
            self.logged_in = False

        # autobahn-python:
        # 1000 for a normal close, 1001 if the browser window is closed,
        # 3000-4999 for app. specific,
        #

        """
        method.

        Args:

        """
        if code == CLOSE_NORMAL or code == GOING_AWAY:
            self.disconnect(reason)
        else:
            self.websocket_close_code = code

        """

        Args:

        """
        cmdarray = json.loads(str(payload, "utf-8"))
        if cmdarray:
            self.data_in(**{cmdarray[0]: [cmdarray[1], cmdarray[2]]})

        """
        Send data to client.

        Args:
            line (str): Text to send.

        """
        try:
            return self.sendMessage(line.encode())
        except Disconnected:
            # this can happen on an unclean close of certain browsers.
            # it means this link is actually already closed.
            self.disconnect(reason="Browser already closed.")

            csession["webclient_authenticated_uid"] = self.uid
            csession.save()

        """
        Data User > Evennia.

        Args:
            text (str): Incoming text.

        Notes:
            At initilization, the client will send the special
            'csessid' command to identify its browser session hash
            with the Evennia side.

            The websocket client will also pass 'websocket_close' command
            to report that the client has been closed and that the
            session should be disconnected.

            Both those commands are parsed and extracted already at
            this point.

        """
        if "websocket_close" in kwargs:
            self.disconnect()
            return

        """
        color-replacement, conversion to html etc.

        Args:

                - raw (bool): No parsing at all (leave ansi-to-html markers unparsed).
                - screenreader (bool): Use Screenreader mode.
                - send_prompt (bool): Send a prompt with parsed html

        """
        if args:
                return
        else:
            return

        client_raw = options.get("client_raw", False)

            text = parse_ansi(text, strip_ansi=True, xterm256=False, mxp=False)
        if raw:
            if client_raw:
                args[0] = text
            else:
                args[0] = html.escape(text)  # escape html!
        else:
            args[0] = parse_html(text, strip_ansi=nocolor)

        """
        Data Evennia -> User.

        Args:
            cmdname (str): The first argument will always be the oob cmd name.
            *args (any): Remaining args will be arguments for `cmd`.

            options (dict): These are ignored for oob commands. Use command
                arguments (which can hold dicts) to send instructions to the
                client instead.

        """
            self.sendLine(json.dumps([cmdname, args, kwargs]))

"""
AJAX/COMET fallback webclient

The AJAX/COMET web client consists of two components running on
twisted and django. They are both a part of the Evennia website url
tree (so the testing website might be located on
http://localhost:4001/, whereas the webclient can be found on
http://localhost:4001/webclient.)

/webclient - this url is handled through django's template
             system and serves the html page for the client
             itself along with its javascript chat program.
/webclientdata - this url is called by the ajax chat using
                 POST requests (long-polling when necessary)
                 The WebClient resource in this module will
                 handle these requests and act as a gateway
                 to sessions connected over the webclient.

"""
import re

from django.utils.functional import Promise
from twisted.web import resource, server

from evennia.server import session
from evennia.utils.utils import to_bytes, ip_from_request

_CLIENT_SESSIONS = utils.mod_import(settings.SESSION_ENGINE).SessionStore
)
_SERVERNAME = settings.SERVERNAME
_KEEPALIVE = 30  # how often to check keepalive

# defining a simple json encoder for returning
# django data to the client. Might need to
# extend this if one wants to send more
# complex database objects too.

class LazyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Promise):
            return str(obj)
        return super().default(obj)

def jsonify(obj):
    return to_bytes(json.dumps(obj, ensure_ascii=False, cls=LazyEncoder))

#
# AjaxWebClient resource - this is called by the ajax client
# using POST requests to /webclientdata.
#

class AjaxWebClient(resource.Resource):
    """
    An ajax/comet long-polling transport

    """

    isLeaf = True
    allowedMethods = ("POST",)

        self.requests = {}
        self.databuffer = {}

        self.last_alive = {}
        self.keep_alive = None

    def _responseFailed(self, failure, csessid, request):
        "callback if a request is lost/timed out"
        try:
            del self.requests[csessid]
            # nothing left to delete
            pass

    def _keepalive(self):
        """
        Callback for checking the connection is still alive.
        """
        to_remove = []
        keep_alives = (
            (csessid, remove)
            for csessid, (t, remove) in self.last_alive.items()
            if now - t > _KEEPALIVE
        )
        for csessid, remove in keep_alives:
            if remove:
                # keepalive timeout. Line is dead.
                to_remove.append(csessid)
            else:
                # normal timeout - send keepalive
                self.last_alive[csessid] = (now, True)
                self.lineSend(csessid, ["ajax_keepalive", [], {}])
        # remove timed-out sessions
        for csessid in to_remove:
            sessions = self.sessionhandler.sessions_from_csessid(csessid)
                sess.disconnect()
            self.last_alive.pop(csessid, None)
            if not self.last_alive:
                # no more ajax clients. Stop the keepalive
                self.keep_alive.stop()
                self.keep_alive = None

    def get_client_sessid(self, request):
        """
        Helper to get the client session id out of the request.

        Args:
            request (Request): Incoming request object.
        Returns:
            csessid (int): The client-session id.

        """
        return html.escape(request.args[b"csessid"][0].decode("utf-8"))

    def get_browserstr(self, request):
        """
        Get browser-string out of the request.

        Args:
        Returns:
            str: The browser name.

        """
        return html.escape(request.args[b"browserstr"][0].decode("utf-8"))

        """
        """
        pass

    def lineSend(self, csessid, data):
        """
        This adds the data to the buffer and/or sends it to the client
        as soon as possible.

        Args:
            data (list): A send structure [cmdname, [args], {kwargs}].

        """
        request = self.requests.get(csessid)
        if request:
            # we have a request waiting. Return immediately.
            request.write(jsonify(data))
            request.finish()
        else:
            # no waiting request. Store data in buffer
            dataentries = self.databuffer.get(csessid, [])
            dataentries.append(jsonify(data))
            self.databuffer[csessid] = dataentries

    def client_disconnect(self, csessid):
        """
        Disconnect session with given csessid.

        Args:

        """
        if csessid in self.requests:
            self.requests[csessid].finish()
        if csessid in self.databuffer:
            del self.databuffer[csessid]

    def mode_init(self, request):
        """
        This is called by render_POST when the client requests an init
        mode operation (at startup)

        Args:
            request (Request): Incoming request.

        """
        csessid = self.get_client_sessid(request)
        browserstr = self.get_browserstr(request)

        remote_addr = ip_from_request(request)

        host_string = "%s (%s:%s)" % (
            _SERVERNAME,
            request.getRequestHostname(),
            request.getHost().port,
        )

        sess = AjaxWebClientSession()
        sess.client = self
        sess.init_session("ajax/comet", remote_addr, self.sessionhandler)

        sess.csessid = csessid
        sess.browserstr = browserstr
        csession = _CLIENT_SESSIONS(session_key=sess.csessid)
        uid = csession and csession.get("webclient_authenticated_uid", False)
        if uid:
            # the client session is already logged in
            sess.uid = uid
            sess.logged_in = True

        self.last_alive[csessid] = (time.time(), False)
        if not self.keep_alive:
            # the keepalive is not running; start it.
            self.keep_alive = LoopingCall(self._keepalive)
            self.keep_alive.start(_KEEPALIVE, now=False)

        browserstr = f":{browserstr}" if browserstr else ""
        sess.protocol_flags["CLIENTNAME"] = f"Evennia Webclient (ajax{browserstr})"
        sess.protocol_flags["UTF-8"] = True
        sess.protocol_flags["OOB"] = True

        sess.sessionhandler.connect(sess)

        return jsonify({"msg": host_string, "csessid": csessid})

    def mode_keepalive(self, request):
        """
        This is called by render_POST when the
        client is replying to the keepalive.

        Args:

        """
        return b'""'

    def mode_input(self, request):
        """
        This is called by render_POST when the client
        is sending data to the server.

        Args:

        """
        cmdarray = json.loads(request.args.get(b"data")[0])
        for sess in self.sessionhandler.sessions_from_csessid(csessid):
            sess.data_in(**{cmdarray[0]: [cmdarray[1], cmdarray[2]]})

    def mode_receive(self, request):
        """
        This is called by render_POST when the client is telling us
        that it is ready to receive data as soon as it is available.
        This is the basis of a long-polling (comet) mechanism: the
        server will wait to reply until data is available.

        Args:

        """
        csessid = html.escape(request.args[b"csessid"][0].decode("utf-8"))

        dataentries = self.databuffer.get(csessid)
        if dataentries:
            # we have data that could not be sent earlier (because client was not
            # ready to receive it). Return this buffered data immediately
            return dataentries.pop(0)
        else:
            # we have no data to send. End the old request and start
            # a new long-polling one
            request.notifyFinish().addErrback(self._responseFailed, csessid, request)
            if csessid in self.requests:
                self.requests[csessid].finish()  # Clear any stale request.
            self.requests[csessid] = request
            return server.NOT_DONE_YET

    def mode_close(self, request):
        """
        This is called by render_POST when the client is signalling
        that it is about to be closed.

        Args:

        """
        try:
            sess = self.sessionhandler.sessions_from_csessid(csessid)[0]
            sess.sessionhandler.disconnect(sess)
            self.client_disconnect(csessid)

    def render_POST(self, request):
        """
        This function is what Twisted calls with POST requests coming
        in from the ajax client. The requests should be tagged with
        different modes depending on what needs to be done, such as
        initializing or sending/receving data through the request. It
        uses a long-polling mechanism to avoid sending data unless
        there is actual data available.

        Args:

        """
        dmode = request.args.get(b"mode", [b"None"])[0].decode("utf-8")

        if dmode == "init":
            # startup. Setup the server.
            return self.mode_init(request)
        elif dmode == "input":
            # input from the client to the server
            return self.mode_input(request)
        elif dmode == "receive":
            # the client is waiting to receive data.
            return self.mode_receive(request)
        elif dmode == "close":
            # the client is closing
            return self.mode_close(request)
        elif dmode == "keepalive":
            # A reply to our keepalive request - all is well
            return self.mode_keepalive(request)
        else:
            # This should not happen if client sends valid data.
            return b'""'

#
# A session type handling communication over the
# web client interface.
#

class AjaxWebClientSession(session.Session):
    """
    This represents a session running in an AjaxWebclient.
    """

        self.protocol_key = "webclient/ajax"

        """

        Returns:

        """

    def disconnect(self, reason="Server disconnected."):
        """

        Args:
        """

            csession["webclient_authenticated_uid"] = None
        self.client.lineSend(self.csessid, ["connection_close", [reason], {}])
        self.client.client_disconnect(self.csessid)

        """

            kwargs (any): Incoming data.

        """

        """

        """

        """

        Args:

                - nocolor (bool): Remove all color.

        """
        if args:
                return
        else:
            return

        text = utils.to_str(text)

        if raw:
            args[0] = text
        else:

        self.client.lineSend(self.csessid, [cmd, args, kwargs])

        """

        Args:

        """
            self.client.lineSend(self.csessid, [cmdname, args, kwargs])

"""
Dummy client runner

This module implements a stand-alone launcher for stress-testing
an Evennia game. It will launch any number of fake clients. These
clients will log into the server and start doing random operations.
Customizing and weighing these operations differently depends on
which type of game is tested. The module contains a testing module
for plain Evennia.

Please note that you shouldn't run this on a production server!
Launch the program without any arguments or options to see a
full step-by-step setup help.

Basically (for testing default Evennia):

 - Use an empty/testing database.
 - set PERMISSION_ACCOUNT_DEFAULT = "Builder"
 - start server, eventually with profiling active
 - launch this client runner

If you want to customize the runner's client actions
(because you changed the cmdset or needs to better
match your use cases or add more actions), you can
change which actions by adding a path to

   DUMMYRUNNER_ACTIONS_MODULE = <path.to.your.module>

in your settings. See utils.dummyrunner_actions.py
for instructions on how to define this module.

"""

import sys

from twisted.conch import telnet

django.setup()

from django.conf import settings  # noqa

from evennia.commands.cmdset import CmdSet  # noqa
from evennia.commands.command import Command  # noqa
from evennia.utils import mod_import, time_format  # noqa
from evennia.utils.ansi import strip_ansi  # noqa

# Load the dummyrunner settings module

DUMMYRUNNER_SETTINGS = mod_import(settings.DUMMYRUNNER_SETTINGS_MODULE)
if not DUMMYRUNNER_SETTINGS:
    raise IOError(
        "Error: Dummyrunner could not find settings file at %s"
        % settings.DUMMYRUNNER_SETTINGS_MODULE
    )
IDMAPPER_CACHE_MAXSIZE = settings.IDMAPPER_CACHE_MAXSIZE

DATESTRING = "%Y%m%d%H%M%S"
CLIENTS = []

# Settings

# number of clients to launch if no input is given on command line
NCLIENTS = 1
# time between each 'tick', in seconds, if not set on command
# line. All launched clients will be called upon to possibly do an
# action with this frequency.
TIMESTEP = DUMMYRUNNER_SETTINGS.TIMESTEP
# chance of a client performing an action, per timestep. This helps to
# spread out usage randomly, like it would be in reality.
CHANCE_OF_ACTION = DUMMYRUNNER_SETTINGS.CHANCE_OF_ACTION
# spread out the login action separately, having many accounts create accounts
# and connect simultaneously is generally unlikely.
CHANCE_OF_LOGIN = DUMMYRUNNER_SETTINGS.CHANCE_OF_LOGIN
# Port to use, if not specified on command line
TELNET_PORT = DUMMYRUNNER_SETTINGS.TELNET_PORT or settings.TELNET_PORTS[0]
#
NCONNECTED = 0  # client has received a connection
NLOGIN_SCREEN = 0  # client has seen the login screen (server responded)
NLOGGING_IN = 0  # client starting login procedure
NLOGGED_IN = 0  # client has authenticated and logged in

# time when all clients have logged_in
TIME_ALL_LOGIN = 0
# actions since all logged in
TOTAL_ACTIONS = 0
TOTAL_LAG_MEASURES = 0
# lag per 30s for all logged in
TOTAL_LAG = 0
TOTAL_LAG_IN = 0
TOTAL_LAG_OUT = 0

INFO_STARTING = """
    Dummyrunner starting using {nclients} dummy account(s). If you don't see
    any connection messages, make sure that the Evennia server is
    running.

    TELNET_PORT = {port}
    IDMAPPER_CACHE_MAXSIZE = {idmapper_cache_size} MB
    TIMESTEP = {timestep} (rate {rate}/s)
    CHANCE_OF_LOGIN = {chance_of_login}% per time step
    CHANCE_OF_ACTION = {chance_of_action}% per time step
    -> avg rate (per client, after login): {avg_rate} cmds/s
    -> total avg rate (after login): {avg_rate_total} cmds/s

    Use Ctrl-C (or Cmd-C) to stop/disconnect all clients.

    """

ERROR_NO_MIXIN = """
    Error: Evennia is not set up for dummyrunner. Before starting the
    server, make sure to include the following at *the end* of your
    settings file (remove when not using dummyrunner!):

        from evennia.server.profiling.settings_mixin import *

    This will change the settings in the following way:
        - change PERMISSION_ACCOUNT_DEFAULT to 'Developer' to allow clients
          to test all commands
        - change PASSWORD_HASHERS to use a faster (but less safe) algorithm
          when creating large numbers of accounts at the same time
        - set LOGIN_THROTTLE/CREATION_THROTTLE=None to disable it

    If you don't want to use the custom settings of the mixin for some
    reason, you can change their values manually after the import, or
    add DUMMYRUNNER_MIXIN=True to your settings file to avoid this
    error completely.

    Warning: Don't run dummyrunner on a production database! It will
    create a lot of spammy objects and accounts!
    """

ERROR_FEW_ACTIONS = """
    Dummyrunner settings error: The ACTIONS tuple is too short: it must
    contain at least login- and logout functions.
    """

HELPTEXT = """
DO NOT RUN THIS ON A PRODUCTION SERVER! USE A CLEAN/TESTING DATABASE!

This stand-alone program launches dummy telnet clients against a
running Evennia server. The idea is to mimic real accounts logging in
and repeatedly doing resource-heavy commands so as to stress test the
game. It uses the default command set to log in and issue commands, so
if that was customized, some of the functionality will not be tested
(it will not fail, the commands will just not be recognized).  The
running clients will create new objects and rooms all over the place
as part of their running, so using a clean/testing database is
strongly recommended.

Setup:
  1) setup a fresh/clean database (if using sqlite, just safe-copy
     away your real evennia.db3 file and create a new one with
     `evennia migrate`)
  2) in server/conf/settings.py, add

        PERMISSION_ACCOUNT_DEFAULT="Builder"

     This is so that the dummy accounts can test building operations.
     You can also customize the dummyrunner by modifying a setting
     file specified by DUMMYRUNNER_SETTINGS_MODULE

  3) Start Evennia like normal, optionally with profiling (--profile)
  4) Run this dummy runner via the evennia launcher:

        evennia --dummyrunner <nr_of_clients>

  5) Log on and determine if game remains responsive despite the
     heavier load. Note that if you activated profiling, there is a
     considerate additional overhead from the profiler too so you
     should usually not consider game responsivity when using the
     profiler at the same time.
  6) If you use profiling, let the game run long enough to gather
     data, then stop the server cleanly using evennia stop or @shutdown.
     @shutdown. The profile appears as
     server/logs/server.prof/portal.prof (see Python's manual on
     cProfiler).

Notes:

The dummyrunner tends to create a lot of accounts all at once, which is
a very heavy operation. This is not a realistic use-case - what you want
to test is performance during run. A large
number of clients here may lock up the client until all have been
created. It may be better to connect multiple dummyrunners instead of
starting one single one with a lot of accounts. Exactly what this number
is depends on your computer power. So start with 10-20 clients and increase
until you see the initial login slows things too much.

"""

class CmdDummyRunnerEchoResponse(Command):
    """
    Dummyrunner command measuring the round-about response time
    from sending to receiving a result.

    Usage:
        dummyrunner_echo_response <timestamp>

    Responds with
        dummyrunner_echo_response:<timestamp>,<current_time>

    The dummyrunner will send this and then compare the send time
    with the receive time on both ends.

    """

    key = "dummyrunner_echo_response"

        # returns (dummy_client_timestamp,current_time)
        self.msg(f"dummyrunner_echo_response:{self.args},{time.time()}")
            print(f"cmddummyrunner lag in: {time.time() - float(self.args)}s")

class DummyRunnerCmdSet(CmdSet):
    """
    Dummyrunner injected cmdset.

    """

        self.add(CmdDummyRunnerEchoResponse())

ICOUNT = 0

def idcounter():
    """
    Makes unique ids.

    Returns:
        str: A globally unique id.

    """
    global ICOUNT
    ICOUNT += 1
    return str("{:03d}".format(ICOUNT))

GCOUNT = 0

def gidcounter():
    """
    Makes globally unique ids.

    Returns:
        count (int); A globally unique counter.

    """
    global GCOUNT
    GCOUNT += 1
    return "%s_%s" % (time.strftime(DATESTRING), GCOUNT)

def makeiter(obj):
    """
    Makes everything iterable.

    Args:
        obj (any): Object to turn iterable.

    Returns:
        iterable (iterable): An iterable object.
    """
    return obj if hasattr(obj, "__iter__") else [obj]

# Client classes

class DummyClient(telnet.StatefulTelnetProtocol):
    """
    Handles connection to a running Evennia server,
    mimicking a real account by sending commands on
    a timer.

    """

    def report(self, text, clientkey):
        pad = " " * (25 - len(text))
        tim = round(time.time() - self.connection_timestamp)
        print(
            f"{text} {clientkey}{pad}\t"
            f"conn: {NCONNECTED} -> "
            f"welcome screen: {NLOGIN_SCREEN} -> "
            f"authing: {NLOGGING_IN} -> "
            f"loggedin/tot: {NLOGGED_IN}/{NCLIENTS} (after {tim}s)"
        )

        """
        Called when connection is first established.

        """
        global NCONNECTED
        # public properties
        self.cid = idcounter()
        self.key = f"Dummy-{self.cid}"
        self.gid = f"{time.strftime(DATESTRING)}_{self.cid}"
        self.istep = 0
        self.exits = []  # exit names created
        self.objs = []  # obj names created
        self.connection_timestamp = time.time()
        self.connection_attempt = 0
        self.action_started = 0

        self._connected = False
        self._loggedin = False
        self._logging_out = False
        self._ready = False
        self._report = ""
        self._cmdlist = []  # already stepping in a cmd definition
        self._login = self.factory.actions[0]
        self._logout = self.factory.actions[1]
        self._actions = self.factory.actions[2:]

        reactor.addSystemEventTrigger("before", "shutdown", self.logout)

        NCONNECTED += 1
        self.report("-> connected", self.key)

        reactor.callLater(30, self._retry_welcome_screen)

    def _retry_welcome_screen(self):
        if not self._connected and not self._ready:
            # we have connected but not received anything for 30s.
            # (unclear why this would be - overload?)
            # try sending a look to get something to start with
            self.report("?? retrying welcome screen", self.key)
            self.sendLine(bytes("look", "utf-8"))
            # make sure to check again later
            reactor.callLater(30, self._retry_welcome_screen)

    def _print_statistics(self):
        global TIME_ALL_LOGIN, TOTAL_ACTIONS
        global TOTAL_LAG, TOTAL_LAG_MEASURES, TOTAL_LAG_IN, TOTAL_LAG_OUT

        tim = time.time() - TIME_ALL_LOGIN
        avgrate = round(TOTAL_ACTIONS / tim)
        lag = TOTAL_LAG / (TOTAL_LAG_MEASURES or 1)
        lag_in = TOTAL_LAG_IN / (TOTAL_LAG_MEASURES or 1)
        lag_out = TOTAL_LAG_OUT / (TOTAL_LAG_MEASURES or 1)

        TOTAL_ACTIONS = 0
        TOTAL_LAG = 0
        TOTAL_LAG_IN = 0
        TOTAL_LAG_OUT = 0
        TOTAL_LAG_MEASURES = 0
        TIME_ALL_LOGIN = time.time()

        print(
            f".. running 30s average: ~{avgrate} actions/s "
            f"lag: {lag:.2}s (in: {lag_in:.2}s, out: {lag_out:.2}s)"
        )

        reactor.callLater(30, self._print_statistics)

        """
        Called when data comes in over the protocol. We wait to start
        stepping until the server actually responds

        Args:

        """
        global NLOGIN_SCREEN, NLOGGED_IN, NLOGGING_IN, NCONNECTED
        global TOTAL_ACTIONS, TIME_ALL_LOGIN

        if not data.startswith(b"\xff"):
            # regular text, not a telnet command

            if NCLIENTS == 1:
                print("dummy-client sees:", str(data, "utf-8"))

            if not self._connected:
                # waiting for connection
                # wait until we actually get text back (not just telnet
                # negotiation)
                # start client tick
                d = LoopingCall(self.step)
                df = max(abs(TIMESTEP * 0.001), min(TIMESTEP / 10, 0.5))
                # dither next attempt with random time
                timestep = TIMESTEP + (-df + (random.random() * df))
                d.start(timestep, now=True).addErrback(self.error)
                self.connection_attempt += 1

                self._connected = True
                NLOGIN_SCREEN += 1
                NCONNECTED -= 1
                self.report("<- server sent login screen", self.key)

            elif self._loggedin:
                if not self._ready:
                    # logged in, ready to run
                    NLOGGED_IN += 1
                    NLOGGING_IN -= 1
                    self._ready = True
                    self.report("== logged in", self.key)
                    if NLOGGED_IN == NCLIENTS and not TIME_ALL_LOGIN:
                        # all are logged in! We can start collecting statistics
                        print(".. All clients connected and logged in!")
                        TIME_ALL_LOGIN = time.time()
                        reactor.callLater(30, self._print_statistics)

                elif TIME_ALL_LOGIN:
                    TOTAL_ACTIONS += 1

                    try:
                        data = strip_ansi(str(data, "utf-8").strip())
                        if data.startswith("dummyrunner_echo_response:"):
                            # handle special lag-measuring command. This returns
                            # dummyrunner_echo_response:<starttime>,<midpointtime>
                            now = time.time()
                            _, data = data.split(":", 1)
                            start_time, mid_time = (float(part) for part in data.split(",", 1))
                            lag_in = mid_time - start_time
                            lag_out = now - mid_time
                            total_lag = now - start_time  # full round-about time

                            TOTAL_LAG += total_lag
                            TOTAL_LAG_IN += lag_in
                            TOTAL_LAG_OUT += lag_out
                            TOTAL_LAG_MEASURES += 1
                        pass

        """
        Called when loosing the connection.

        Args:
            reason (str): Reason for loosing connection.

        """
        if not self._logging_out:
            self.report("XX lost connection", self.key)

    def error(self, err):
        """

        Args:
            err (Failure): Error instance.
        """

    def counter(self):
        """
        Produces a unique id, also between clients.

        Returns:
            counter (int): A unique counter.

        """
        return gidcounter()

    def logout(self):
        """
        Causes the client to log out of the server. Triggered by ctrl-c signal.

        """
        self._logging_out = True
        cmd = self._logout(self)[0]
        self.report(f"-> logout/disconnect ({self.istep} actions)", self.key)
        self.sendLine(bytes(cmd, "utf-8"))

    def step(self):
        """
        Perform a step. This is called repeatedly by the runner and
        causes the client to issue commands to the server.  This holds
        all "intelligence" of the dummy client.

        """
        global NLOGGING_IN, NLOGIN_SCREEN

        if not self._cmdlist:
            # no commands ready. Load some.

            if not self._loggedin:
                if rand < CHANCE_OF_LOGIN or NLOGGING_IN < 10:
                    # lower rate of logins, but not below 1 / s
                    # get the login commands
                    self._cmdlist = list(makeiter(self._login(self)))
                    NLOGGING_IN += 1  # this is for book-keeping
                    NLOGIN_SCREEN -= 1
                    self.report("-> create/login", self.key)
                    self._loggedin = True
                else:
                    # no login yet, so cmdlist not yet set
                    return
            else:
                # we always pick a cumulatively random function
                crand = random.random()
                cfunc = [func for (cprob, func) in self._actions if cprob >= crand][0]
                self._cmdlist = list(makeiter(cfunc(self)))

        # at this point we always have a list of commands
        if rand < CHANCE_OF_ACTION:
            # send to the game
            cmd = str(self._cmdlist.pop(0))

            if cmd.startswith("dummyrunner_echo_response"):
                # we need to set the timer element as close to
                # the send as possible
                cmd = cmd.format(timestamp=time.time())

            self.sendLine(bytes(cmd, "utf-8"))
            self.action_started = time.time()
            self.istep += 1

                print(f"dummy-client sent: {cmd}")

class DummyFactory(protocol.ReconnectingClientFactory):
    protocol = DummyClient

    def __init__(self, actions):
        "Setup the factory base (shared by all clients)"
        self.actions = actions

# Access method:
# Starts clients and connects them to a running server.

def start_all_dummy_clients(nclients):
    """
    Initialize all clients, connect them and start to step them

    Args:
        nclients (int): Number of dummy clients to connect.

    """
    global NCLIENTS
    NCLIENTS = int(nclients)
    actions = DUMMYRUNNER_SETTINGS.ACTIONS

    if len(actions) < 2:
        print(ERROR_FEW_ACTIONS)
        return

    # make sure the probabilities add up to 1
    pratio = 1.0 / sum(tup[0] for tup in actions[2:])
    flogin, flogout, probs, cfuncs = (
        actions[0],
        actions[1],
        [tup[0] * pratio for tup in actions[2:]],
        [tup[1] for tup in actions[2:]],
    )
    # create cumulative probabilies for the random actions
    cprobs = [sum(v for i, v in enumerate(probs) if i <= k) for k in range(len(probs))]
    # rebuild a new, optimized action structure
    actions = (flogin, flogout) + tuple(zip(cprobs, cfuncs))

    # setting up all clients (they are automatically started)
    factory = DummyFactory(actions)
    for i in range(NCLIENTS):
        reactor.connectTCP("127.0.0.1", TELNET_PORT, factory)
    # start reactor
    reactor.run()

# Command line interface

    try:
        settings.DUMMYRUNNER_MIXIN
        print(ERROR_NO_MIXIN)

    # parsing command line with default vals
    parser = ArgumentParser(description=HELPTEXT)
        "-N", nargs=1, default=1, dest="nclients", help="Number of clients to start"
    )

    args = parser.parse_args()
    nclients = int(args.nclients[0])

    print(
        INFO_STARTING.format(
            nclients=nclients,
            port=TELNET_PORT,
            idmapper_cache_size=IDMAPPER_CACHE_MAXSIZE,
            timestep=TIMESTEP,
            rate=1 / TIMESTEP,
            chance_of_login=CHANCE_OF_LOGIN * 100,
            chance_of_action=CHANCE_OF_ACTION * 100,
            avg_rate=(1 / TIMESTEP) * CHANCE_OF_ACTION,
            avg_rate_total=(1 / TIMESTEP) * CHANCE_OF_ACTION * nclients,
        )
    )

    # run the dummyrunner
    TIME_START = t0 = time.time()
    start_all_dummy_clients(nclients=nclients)
    ttot = time.time() - t0

    # output runtime
    print("... dummy client runner stopped after %s." % time_format(ttot, style=3))

"""
Settings and actions for the dummyrunner

This module defines dummyrunner settings and sets up
the actions available to dummy accounts.

The settings are global variables:

- TIMESTEP - time in seconds between each 'tick'. 1 is a good start.
- CHANCE_OF_ACTION - chance 0-1 of action happening. Default is 0.5.
- CHANCE_OF_LOGIN - chance 0-1 of login happening. 0.01 is a good number.
- TELNET_PORT - port to use, defaults to settings.TELNET_PORT
- ACTIONS - see below

ACTIONS is a tuple

```python
(login_func, logout_func, (0.3, func1), (0.1, func2) ... )

```

where the first entry is the function to call on first connect, with a
chance of occurring given by CHANCE_OF_LOGIN. This function is usually
responsible for logging in the account. The second entry is always
called when the dummyrunner disconnects from the server and should
thus issue a logout command. The other entries are tuples (chance,
func). They are picked randomly, their commonality based on the
cumulative chance given (the chance is normalized between all options
so if will still work also if the given chances don't add up to 1).

The PROFILE variable define pre-made ACTION tuples for convenience.

Each function should return an iterable of one or more command-call
strings (like "look here"), so each can group multiple command operations.

An action-function is called with a "client" argument which is a
reference to the dummy client currently performing the action.

The client object has the following relevant properties and methods:

- key - an optional client key. This is only used for dummyrunner output.
  Default is "Dummy-<cid>"
- cid - client id
- gid - globally unique id, hashed with time stamp
- istep - the current step
- exits - an empty list. Can be used to store exit names
- objs - an empty list. Can be used to store object names
- counter() - returns a unique increasing id, hashed with time stamp
  to make it unique also between dummyrunner instances.

The return should either be a single command string or a tuple of
command strings. This list of commands will always be executed every
TIMESTEP with a chance given by CHANCE_OF_ACTION by in the order given
(no randomness) and allows for setting up a more complex chain of
commands (such as creating an account and logging in).

----

"""

# Dummy runner settings

# Time between each dummyrunner "tick", in seconds. Each dummy
# will be called with this frequency.
TIMESTEP = 1
# TIMESTEP = 0.025  # 40/s

# Chance of a dummy actually performing an action on a given tick.
# This spreads out usage randomly, like it would be in reality.
CHANCE_OF_ACTION = 0.5

# Chance of a currently unlogged-in dummy performing its login
# action every tick. This emulates not all accounts logging in
# at exactly the same time.
CHANCE_OF_LOGIN = 0.01

# Which telnet port to connect to. If set to None, uses the first
# default telnet port of the running server.
TELNET_PORT = None

# Setup actions tuple

# some convenient templates

DUMMY_NAME = "Dummy_{gid}"
DUMMY_PWD = (
    "".join(random.choice(string.ascii_letters + string.digits) for _ in range(20)) + "-{gid}"
)
START_ROOM = "testing_room_start_{gid}"
ROOM_TEMPLATE = "testing_room_%s"
EXIT_TEMPLATE = "exit_%s"
OBJ_TEMPLATE = "testing_obj_%s"
TOBJ_TEMPLATE = "testing_button_%s"
TOBJ_TYPECLASS = "contrib.tutorial_examples.red_button.RedButton"

# action function definitions (pick and choose from
# these to build a client "usage profile"

# login/logout

def c_login(client):
    "logins to the game"
    # we always use a new client name
    cname = DUMMY_NAME.format(gid=client.gid)
    cpwd = DUMMY_PWD.format(gid=client.gid)
    room_name = START_ROOM.format(gid=client.gid)

    # we assign the dummyrunner cmdsert to ourselves so # we can use special commands
    add_cmdset = (
        "py from evennia.server.profiling.dummyrunner import DummyRunnerCmdSet;"
        "self.cmdset.add(DummyRunnerCmdSet, persistent=False)"
    )

    # create character, log in, then immediately dig a new location and
    # teleport it (to keep the login room clean)
    cmds = (
        f"create {cname} {cpwd}",
        f"yes",  # to confirm creation
        f"connect {cname} {cpwd}",
        f"dig {room_name}",
        f"teleport {room_name}",
        add_cmdset,
    )
    return cmds

def c_login_nodig(client):
    "logins, don't dig its own room"
    cmds = (f"create {cname} {cpwd}", f"connect {cname} {cpwd}")

def c_logout(client):
    "logouts of the game"
    return ("quit",)

# random commands

def c_looks(client):
    "looks at various objects"
    cmds = ["look %s" % obj for obj in client.objs]
    if not cmds:
        cmds = ["look %s" % exi for exi in client.exits]
        if not cmds:
            cmds = ("look",)

def c_examines(client):
    "examines various objects"
    cmds = ["examine %s" % obj for obj in client.objs]
        cmds = ["examine %s" % exi for exi in client.exits]
        cmds = ("examine me",)

def c_idles(client):
    "idles"
    cmds = ("idle", "idle")

def c_help(client):
    "reads help files"
    cmds = (
        "help",
        "dummyrunner_echo_response",
    )

def c_digs(client):
    "digs a new room, storing exit names on client"
    roomname = ROOM_TEMPLATE % client.counter()
    exitname1 = EXIT_TEMPLATE % client.counter()
    exitname2 = EXIT_TEMPLATE % client.counter()
    client.exits.extend([exitname1, exitname2])
    return ("dig/tel %s = %s, %s" % (roomname, exitname1, exitname2),)

def c_creates_obj(client):
    "creates normal objects, storing their name on client"
    objname = OBJ_TEMPLATE % client.counter()
    client.objs.append(objname)
    cmds = (
        "create %s" % objname,
        'desc %s = "this is a test object' % objname,
        "set %s/testattr = this is a test attribute value." % objname,
        "set %s/testattr2 = this is a second test attribute." % objname,
    )

def c_creates_button(client):
    "creates example button, storing name on client"
    objname = TOBJ_TEMPLATE % client.counter()
    cmds = ("create %s:%s" % (objname, TOBJ_TYPECLASS), "desc %s = test red button!" % objname)

def c_socialize(client):
    "socializechats on channel"
    cmds = (
        "pub Hello!",
        "say Yo!",
        "emote stands looking around.",
    )

def c_moves(client):
    "moves to a previously created room, using the stored exits"
    cmds = client.exits  # try all exits - finally one will work
    return ("look",) if not cmds else cmds

def c_moves_n(client):
    "move through north exit if available"
    return ("north",)

def c_moves_s(client):
    "move through south exit if available"
    return ("south",)

def c_measure_lag(client):
    """
    Special dummyrunner command, injected in c_login. It  measures
    response time. Including this in the ACTION tuple will give more
    dummyrunner output about just how fast commands are being processed.

    The dummyrunner will treat this special and inject the
    {timestamp} just before sending.

    """
    return ("dummyrunner_echo_response {timestamp}",)

# Action profile (required)

# Some pre-made profiles to test. To make your own, just assign a tuple to ACTIONS.
#
# idler - does nothing after logging in
# looker - just looks around
# normal_player - moves around, reads help, looks around (digs rarely) (spammy)
# normal_builder -  digs now and then, examines, creates objects, moves
# heavy_builder - digs and creates a lot, moves and examines
# socializing_builder - builds a lot, creates help entries, moves, chat (spammy)
# only_digger - extreme builder that only digs room after room

PROFILE = "looker"

if PROFILE == "idler":
    ACTIONS = (
        c_login,
        c_logout,
        (0.9, c_idles),
        (0.1, c_measure_lag),
    )
elif PROFILE == "looker":
    ACTIONS = (c_login, c_logout, (0.8, c_looks), (0.2, c_measure_lag))
elif PROFILE == "normal_player":
    ACTIONS = (
        c_login,
        c_logout,
        (0.01, c_digs),
        (0.29, c_looks),
        (0.2, c_help),
        (0.3, c_moves),
        (0.05, c_socialize),
    )
elif PROFILE == "normal_builder":
    ACTIONS = (
        c_login,
        c_logout,
        (0.5, c_looks),
        (0.08, c_examines),
        (0.1, c_help),
        (0.01, c_creates_obj),
        (0.2, c_moves),
    )
elif PROFILE == "heavy_builder":
    ACTIONS = (
        c_login,
        c_logout,
        (0.1, c_looks),
        (0.1, c_examines),
        (0.1, c_digs),
        (0.1, c_creates_obj),
    )
elif PROFILE == "socializing_builder":
    ACTIONS = (
        c_login,
        c_logout,
        (0.1, c_socialize),
        (0.2, c_digs),
    )
elif PROFILE == "only_digger":
    ACTIONS = (c_login, c_logout, (0.9, c_digs), (0.1, c_measure_lag))

else:
    print("No dummyrunner ACTION profile defined.")
    import sys

"""
Script that saves memory and idmapper data over time.

Data will be saved to game/logs/memoryusage.log. Note that
the script will append to this file if it already exists.

Call this module directly to plot the log (requires matplotlib and numpy).
"""

import os
import sys

# TODO!
# sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
# os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings'
from evennia.utils.idmapper import models as _idmapper

LOGFILE = "logs/memoryusage.log"
INTERVAL = 30  # log every 30 seconds

class Memplot(evennia.DefaultScript):
    """
    Describes a memory plotting action.

    """

        "Called at script creation"
        self.key = "memplot"
        self.desc = "Save server memory stats to file"
        self.start_delay = False
        self.interval = INTERVAL
        self.db.starttime = time.time()

        "Regularly save memory statistics."
        rmem = (
            float(os.popen("ps -p %d -o %s | tail -1" % (pid, "rss")).read()) / 1000.0
        )  # resident memory
        vmem = (
            float(os.popen("ps -p %d -o %s | tail -1" % (pid, "vsz")).read()) / 1000.0
        )  # virtual memory
        total_num, cachedict = _idmapper.cache_size()
        t0 = (time.time() - self.db.starttime) / 60.0  # save in minutes

        with open(LOGFILE, "a") as f:
            f.write("%s, %s, %s, %s\n" % (t0, rmem, vmem, int(total_num)))

    # plot output from the file

    import numpy
    from matplotlib import pyplot as pp

    data = numpy.genfromtxt("../../../game/" + LOGFILE, delimiter=",")
    secs = data[:, 0]
    rmem = data[:, 1]
    vmem = data[:, 2]
    nobj = data[:, 3]

    # calculate derivative of obj creation
    # oderiv = (0.5*(nobj[2:] - nobj[:-2]) / (secs[2:] - secs[:-2])).copy()
    # oderiv = (0.5*(rmem[2:] - rmem[:-2]) / (secs[2:] - secs[:-2])).copy()

    fig = pp.figure()
    ax1 = fig.add_subplot(111)
    ax1.set_title("1000 bots (normal accounts with light building)")
    ax1.set_xlabel("Time (mins)")
    ax1.set_ylabel("Memory usage (MB)")
    ax1.plot(secs, rmem, "r", label="RMEM", lw=2)
    ax1.plot(secs, vmem, "b", label="VMEM", lw=2)
    ax1.legend(loc="upper left")

    ax2 = ax1.twinx()
    ax2.plot(secs, nobj, "g--", label="objs in cache", lw=2)
    # ax2.plot(secs[:-2], oderiv/60.0, "g--", label="Objs/second", lw=2)
    # ax2.plot(secs[:-2], oderiv, "g--", label="Objs/second", lw=2)
    ax2.set_ylabel("Number of objects")
    ax2.legend(loc="lower right")
    ax2.annotate("First 500 bots\nconnecting", xy=(10, 4000))
    ax2.annotate("Next 500 bots\nconnecting", xy=(350, 10000))
    # ax2.annotate("@reload", xy=(185,600))

    #    # plot mem vs cachesize
    #    nobj, rmem, vmem = nobj[:262].copy(), rmem[:262].copy(), vmem[:262].copy()
    #
    #    fig = pp.figure()
    #    ax1 = fig.add_subplot(111)
    #    ax1.set_title("Memory usage per cache size")
    #    ax1.set_xlabel("Cache size (number of objects)")
    #    ax1.set_ylabel("Memory usage (MB)")
    #    ax1.plot(nobj, rmem, "r", label="RMEM", lw=2)
    #    ax1.plot(nobj, vmem, "b", label="VMEM", lw=2)
    #

    # empirical estimate of memory usage: rmem = 35.0 + 0.0157 * Ncache
    # Ncache = int((rmem - 35.0) / 0.0157)  (rmem in MB)
    #
    #    rderiv_aver = 0.0157
    #    ax1.set_title("Relation between memory and cache size")
    #    ax1.set_xlabel("Memory usage (MB)")
    #    ax1.set_ylabel("Idmapper Cache Size (number of objects)")
    #    rmem = numpy.linspace(35, 2000, 2000)
    #    nobjs = numpy.array([int((mem - 35.0) / 0.0157) for mem in rmem])
    #    ax1.plot(rmem, nobjs, "r", lw=2)

    pp.show()

"""
This is a little routine for viewing the sql queries that are executed by a given
query as well as count them for optimization testing.

"""

import os
import sys

# os.environ["DJANGO_SETTINGS_MODULE"] = "game.settings"

def count_queries(exec_string, setup_string):
    """
    Display queries done by exec_string. Use setup_string
    to setup the environment to test.
    """

    exec(setup_string)

    num_queries_old = len(connection.queries)
    exec(exec_string)
    nqueries = len(connection.queries) - num_queries_old

    for query in connection.queries[-nqueries if nqueries else 1 :]:
        print(query["time"], query["sql"])
    print("Number of queries: %s" % nqueries)

    # setup tests here

    setup_string = """
g = ObjectDB.objects.get(db_key="Griatch")
"""
    exec_string = """
g.tags.all()
"""
    count_queries(exec_string, setup_string)

from mock import Mock, mock_open, patch

from .dummyrunner_settings import (
    c_creates_button,
    c_creates_obj,
    c_digs,
    c_examines,
    c_help,
    c_idles,
    c_login,
    c_login_nodig,
    c_logout,
    c_looks,
    c_moves,
    c_moves_n,
    c_moves_s,
    c_socialize,
)

try:
    import memplot
    memplot = Mock()

class TestDummyrunnerSettings(TestCase):
        self.client = Mock()
        self.client.cid = 1
        self.client.counter = Mock(return_value=1)
        self.client.gid = "20171025161153-1"
        self.client.name = "Dummy_%s" % self.client.gid
        self.client.password = (Something,)
        self.client.start_room = "testing_room_start_%s" % self.client.gid
        self.client.objs = []
        self.client.exits = []

    def clear_client_lists(self):

    def test_c_login(self):
            c_login(self.client),
            (
                Something,  # create
                "yes",  # confirm creation
                Something,  # connect
                "dig %s" % self.client.start_room,
                "teleport %s" % self.client.start_room,
                "py from evennia.server.profiling.dummyrunner import DummyRunnerCmdSet;"
                "self.cmdset.add(DummyRunnerCmdSet, persistent=False)",
            ),
        )

    def test_c_login_no_dig(self):
        cmd1, cmd2 = c_login_nodig(self.client)
        self.assertTrue(cmd1.startswith("create " + self.client.name + " "))
        self.assertTrue(cmd2.startswith("connect " + self.client.name + " "))

    def test_c_logout(self):
        self.assertEqual(c_logout(self.client), ("quit",))

    def perception_method_tests(self, func, verb, alone_suffix=""):
        self.assertEqual(func(self.client), ("%s%s" % (verb, alone_suffix),))
        self.client.exits = ["exit1", "exit2"]
        self.assertEqual(func(self.client), ["%s exit1" % verb, "%s exit2" % verb])
        self.client.objs = ["foo", "bar"]
        self.assertEqual(func(self.client), ["%s foo" % verb, "%s bar" % verb])
        self.clear_client_lists()

    def test_c_looks(self):
        self.perception_method_tests(c_looks, "look")

    def test_c_examines(self):
        self.perception_method_tests(c_examines, "examine", " me")

    def test_idles(self):
        self.assertEqual(c_idles(self.client), ("idle", "idle"))

    def test_c_help(self):
            c_help(self.client),
            ("help", "dummyrunner_echo_response"),
        )

    def test_c_digs(self):
        self.assertEqual(c_digs(self.client), ("dig/tel testing_room_1 = exit_1, exit_1",))
        self.assertEqual(self.client.exits, ["exit_1", "exit_1"])

    def test_c_creates_obj(self):
        objname = "testing_obj_1"
            c_creates_obj(self.client),
            (
                "create %s" % objname,
                'desc %s = "this is a test object' % objname,
                "set %s/testattr = this is a test attribute value." % objname,
                "set %s/testattr2 = this is a second test attribute." % objname,
            ),
        )
        self.assertEqual(self.client.objs, [objname])

    def test_c_creates_button(self):
        objname = "testing_button_1"
        typeclass_name = "contrib.tutorial_examples.red_button.RedButton"
            c_creates_button(self.client),
            ("create %s:%s" % (objname, typeclass_name), "desc %s = test red button!" % objname),
        )

    def test_c_socialize(self):
            c_socialize(self.client),
            (
                "pub Hello!",
                "say Yo!",
                "emote stands looking around.",
            ),
        )

    def test_c_moves(self):
        self.assertEqual(c_moves(self.client), ("look",))
        self.client.exits = ["south", "north"]
        self.assertEqual(c_moves(self.client), ["south", "north"])

    def test_c_move_n(self):
        self.assertEqual(c_moves_n(self.client), ("north",))

    def test_c_move_s(self):
        self.assertEqual(c_moves_s(self.client), ("south",))

class TestMemPlot(TestCase):
    @patch.object(memplot, "_idmapper")
    @patch.object(memplot, "os")
    @patch.object(memplot, "open", new_callable=mock_open, create=True)
    @patch.object(memplot, "time")
    @patch("evennia.utils.idmapper.models.SharedMemoryModel.flush_from_cache", new=Mock())
    def test_memplot(self, mock_time, mocked_open, mocked_os, mocked_idmapper):
        if isinstance(memplot, Mock):
            return
        from evennia.utils.create import create_script

        mocked_idmapper.cache_size.return_value = (9, 5000)
        mock_time.time = Mock(return_value=6000.0)
        script = create_script(memplot.Memplot)
        script.db.starttime = 0.0
        mocked_os.popen.read.return_value = 5000.0
        script.at_repeat()
        handle = mocked_open()
        handle.write.assert_called_with("100.0, 0.001, 0.001, 9\n")
        script.stop()

"""
Trace a message through the messaging system
"""

def timetrace(message, idstring, tracemessage="TEST_MESSAGE", final=False):
    """
    Trace a message with time stamps.

    Args:
        message (str): The actual message coming through
        idstring (str): An identifier string specifying where this trace is happening.
        tracemessage (str): The start of the message to tag.
            This message will get attached time stamp.
        final (bool): This is the final leg in the path - include total time in message

    """
    if message.startswith(tracemessage):
        # the message is on the form TEST_MESSAGE tlast t0
        # where t0 is the initial starting time and last is the time
        # saved at the last stop.
        try:
            prefix, tlast, t0 = message.split(None, 2)
            tlast, t0 = float(tlast), float(t0)
        except (IndexError, ValueError):
            tlast = t0
            t1 = t0
        else:
        # print to log (important!)
        print("** timetrace (%s): dT=%fs, total=%fs." % (idstring, t1 - tlast, t1 - t0))

        if final:
            message = " ****  %s (total %f) **** " % (tracemessage, t1 - t0)
        else:
            message = "%s %f %f" % (tracemessage, t1, t0)
    return message

"""
This defines a the Server's generic session object. This object represents
a connection to the outside world but don't know any details about how the
connection actually happens (so it's the same for telnet, web, ssh etc).

It is stored on the Server side (as opposed to protocol-specific sessions which
are stored on the Portal side)
"""

from evennia.scripts.monitorhandler import MONITOR_HANDLER
from evennia.typeclasses.attributes import (
    AttributeHandler,
    DbHolder,
    InMemoryAttributeBackend,
)
from evennia.utils.utils import class_from_module, lazy_property, make_iter

_ANSI = None

# Server Session

class ServerSession(_BASE_SESSION_CLASS):
    """
    This class represents an account's session and is a template for
    individual protocols to communicate with Evennia.

    Each account gets a session assigned to them whenever they connect
    to the game server. All communication between game and account goes
    through their session.

    """

        """
        Initiate to avoid AttributeErrors down the line

        """
        self.puppet = None
        self.cmdset_storage_string = ""
        self.cmdset = CmdSetHandler(self, True)

        return [path.strip() for path in self.cmdset_storage_string.split(",")]

        self.cmdset_storage_string = ",".join(str(val).strip() for val in make_iter(value))

    cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set)

    @property
    def id(self):
        return self.sessid

    def at_sync(self):
        """
        This is called whenever a session has been resynced with the
        portal.  At this point all relevant attributes have already
        been set and self.account been assigned (if applicable).

        Since this is often called after a server restart we need to
        set up the session as it was.

        """
        global _ObjectDB

        super().at_sync()
        if not self.logged_in:
            # assign the unloggedin-command set.
            self.cmdset_storage = settings.CMDSET_UNLOGGEDIN

        self.cmdset.update(init_mode=True)

        if self.puid:
            # reconnect puppet (puid is only set if we are coming
            # back from a server reload). This does all the steps
            # done in the default @ic command but without any
            # hooks, echoes or access checks.
            obj = _ObjectDB.objects.get(id=self.puid)
            obj.sessions.add(self)
            obj.account = self.account
            self.puid = obj.id
            self.puppet = obj
            # obj.scripts.validate()
            obj.locks.cache_lock_bypass(obj)

    def at_login(self, account):
        """
        Hook called by sessionhandler when the session becomes authenticated.

        Args:
            account (Account): The account associated with the session.

        """
        self.uid = self.account.id
        self.uname = self.account.username
        self.conn_time = time.time()
        self.puid = None
        self.cmdset_storage = settings.CMDSET_SESSION

        # Update account's last login time.
        self.account.last_login = timezone.now()
        self.account.save()

        # add the session-level cmdset

    def at_disconnect(self, reason=None):
        """
        Hook called by sessionhandler when disconnecting this session.

        """
        if self.logged_in:
            account = self.account
            if self.puppet:
                account.unpuppet_object(self)
            uaccount = account
            uaccount.last_login = timezone.now()
            uaccount.save()
            # calling account hook
            account.at_disconnect(reason)
            if not self.sessionhandler.sessions_from_account(account):
                # no more sessions connected to this account
                account.is_connected = False
            # this may be used to e.g. delete account after disconnection etc
            account.at_post_disconnect()
            # remove any webclient settings monitors associated with this
            # session
            MONITOR_HANDLER.remove(account, "_saved_webclient_options", self.sessid)

    def get_account(self):
        """
        Get the account associated with this session

        Returns:
            account (Account or None): The associated Account.

        """
        return self.account if self.logged_in else None

    def get_puppet(self):
        """
        Get the in-game character associated with this session.

        Returns:
            puppet (Object or None): The puppeted object, if any.

        """
        return self.puppet if self.logged_in else None

    get_character = get_puppet

    def get_puppet_or_account(self):
        """
        Get puppet or account.

        Returns:
            controller (Object or Account): The puppet if one exists,
                otherwise return the account.

        """
            return self.puppet if self.puppet else self.account

    def log(self, message, channel=True):
        """
        Emits session info to the appropriate outputs and info channels.

        Args:
            message (str): The message to log.
            channel (bool, optional): Log to the CHANNEL_CONNECTINFO channel
                in addition to the server log.

        """
        cchan = channel and settings.CHANNEL_CONNECTINFO
        if cchan:
            try:
                cchan = ChannelDB.objects.get_channel(cchan["key"])
                cchan.msg("[%s]: %s" % (cchan.key, message))
        logger.log_info(message)

    def get_client_size(self):
        """
        Return eventual eventual width and height reported by the
        client. Note that this currently only deals with a single
        client window (windowID==0) as in a traditional telnet session.

        """
        # print("session flags:", flags)
        width = flags.get("SCREENWIDTH", {}).get(0, settings.CLIENT_DEFAULT_WIDTH)
        height = flags.get("SCREENHEIGHT", {}).get(0, settings.CLIENT_DEFAULT_HEIGHT)
        return width, height

    def update_session_counters(self, idle=False):
        """
        Hit this when the user enters a command in order to update
        idle timers and command counters.

        """
        # Idle time used for timeout calcs.
        self.cmd_last = time.time()

        # Store the timestamp of the user's last command.
        if not idle:
            # Increment the user's command counter.
            self.cmd_total += 1
            # Account-visible idle time, not used in idle timeout calcs.
            self.cmd_last_visible = self.cmd_last

    def update_flags(self, **kwargs):
        """
        Update the protocol_flags and sync them with Portal.

            protocol_flag (any): A key and value to set in the
                protocol_flags dictionary.

        Notes:
            Since protocols can vary, no checking is done
            as to the existene of the flag or not. The input
            data should have been validated before this call.

        """
        if kwargs:
            self.protocol_flags.update(kwargs)
            self.sessionhandler.session_portal_sync(self)

        """
        Sending data from Evennia->Client

            text (str or tuple)
            any (str or tuple): Send-commands identified
                by their keys. Or "options", carrying options
                for the protocol(s).

        """

        """
        Receiving data from the client, sending it off to
        the respective inputfuncs.

            kwargs (any): Incoming data from protocol on
                the form `{"commandname": ((args), {kwargs}),...}`
        Notes:
            This method is here in order to give the user
            a single place to catch and possibly process all incoming data from
            the client. It should usually always end by sending
            this data off to `self.sessionhandler.call_inputfuncs(self, **kwargs)`.
        """
        self.sessionhandler.call_inputfuncs(self, **kwargs)

        """
        Wrapper to mimic msg() functionality of Objects and Accounts.

        Args:
            text (str): String input.

        """
        # this can happen if this is triggered e.g. a command.msg
        # that auto-adds the session, we'd get a kwarg collision.
        kwargs.pop("session", None)
        kwargs.pop("from_obj", None)
            self.data_out(text=text, **kwargs)
        else:
            self.data_out(**kwargs)

        """
        Do something as this object. This method is normally never
        called directly, instead incoming command instructions are
        sent to the appropriate inputfunc already at the sessionhandler
        level. This method allows Python code to inject commands into
        this stream, and will lead to the text inputfunc be called.

        Args:
            session (Session): This is here to make API consistent with
                Account/Object.execute_cmd. If given, data is passed to
                that Session, otherwise use self.

        """
        # inject instruction into input stream
        kwargs["text"] = ((raw_string,), {})
        self.sessionhandler.data_in(session or self, **kwargs)

        """
        Handle session comparisons

        """
        try:
            return self.address == other.address

        """

        """
        return hash(self.address)

        try:
            return self.address != other.address

        """
        String representation of the user session class. We use
        this a lot in the server logs.

        """
        symbol = ""
        if self.logged_in and hasattr(self, "account") and self.account:
            symbol = "(#%s)" % self.account.id
        try:
            if hasattr(self.address, "__iter__"):
                address = ":".join([str(part) for part in self.address])
            else:
                address = self.address
            address = self.address
        return "%s%s@%s" % (self.uname, symbol, address)

        return "%s" % str(self)

    # Dummy API hooks for use during non-loggedin operation

        """
        A dummy hook all objects with cmdsets need to have

        """

        pass

    # Mock db/ndb properties for allowing easy storage on the session
    # (note that no databse is involved at all here. session.db.attr =
    # value just saves a normal property in memory, just like ndb).

        return AttributeHandler(self, InMemoryAttributeBackend)

        return self.nattributes

    def ndb_get(self):
        """
        A non-persistent store (ndb: NonDataBase). Everything stored
        to this is guaranteed to be cleared when a server is shutdown.
        Syntax is same as for the _get_db_holder() method and
        property, e.g. obj.ndb.attr = value etc.

        """
        try:
            return self._ndb_holder
            self._ndb_holder = DbHolder(self, "nattrhandler", manager_name="nattributes")

    # @ndb.setter
    def ndb_set(self, value):
        """
        Stop accidentally replacing the db object

        Args:
            value (any): A value to store in the ndb.

        """
        string = "Cannot assign directly to ndb object! "
        string += "Use ndb.attr=value instead."
        raise Exception(string)

    # @ndb.deleter
    def ndb_del(self):
        """
        Stop accidental deletion.

        """
        raise Exception("Cannot delete the ndb object!")

    ndb = property(ndb_get, ndb_set, ndb_del)
    db = property(ndb_get, ndb_set, ndb_del)

    # Mock access method for the session (there is no lock info
    # at this stage, so we just present a uniform API)
    def access(self, *args, **kwargs):
        """
        Dummy method to mimic the logged-in API.

        """

    def get_display_name(self, *args, **kwargs):
        if self.puppet:
            return self.puppet.get_display_name(*args, **kwargs)
        elif self.account:
            return self.account.get_display_name(*args, **kwargs)
        else:
            return f"{self.protocol_key}({self.address})"

"""
This module contains the main EvenniaService class, which is the very core of the
Evennia server. It is instantiated by the evennia/server/server.py module.
"""

from django.db.utils import OperationalError
from evennia.utils.utils import get_evennia_version, make_iter, mod_import
from twisted.internet import defer, reactor
from twisted.internet.defer import Deferred

class EvenniaServerService(MultiService):
    def _wrap_sigint_handler(self, *args):
        if hasattr(self, "web_root"):
            d = self.web_root.empty_threadpool()
            d.addCallback(lambda _: self.shutdown("reload", _reactor_stopping=True))
        else:
            d = Deferred(lambda _: self.shutdown("reload", _reactor_stopping=True))
        d.addCallback(lambda _: reactor.stop())
        reactor.callLater(1, d.callback, None)

        self.maintenance_count = 0
        self.amp_protocol = None  # set by amp factory
        self.amp_service = None
            "amp": "",
            "webserver": "",
            "irc_rss": "",
        }
        self._flush_cache = None
        self._last_server_time_snapshot = 0

        # Database-specific startup optimizations.
        self.sqlite3_prep()

        # 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.
        # (see https://github.com/evennia/evennia/issues/1128)

        reactor.sigInt = self._wrap_sigint_handler

        self.start_stop_modules = [
            mod_import(mod)
            for mod in make_iter(settings.AT_SERVER_STARTSTOP_MODULE)
            if isinstance(mod, str)
        ]

    def server_maintenance(self):
        """
        This maintenance function handles repeated checks and updates that
        the server needs to do. It is called every minute.
        """
        if not self._flush_cache:
            from evennia.utils.idmapper.models import conditional_flush as _FLUSH_CACHE

            self._flush_cache = _FLUSH_CACHE

        self.maintenance_count += 1

        if self.maintenance_count == 1:
            # first call after a reload
            evennia.gametime.SERVER_START_TIME = now
            evennia.gametime.SERVER_RUNTIME = evennia.ServerConfig.objects.conf(
                "runtime", default=0.0
            )
            _LAST_SERVER_TIME_SNAPSHOT = now
        else:
            # adjust the runtime not with 60s but with the actual elapsed time
            # in case this may varies slightly from 60s.
            evennia.gametime.SERVER_RUNTIME += now - self._last_server_time_snapshot
        self._last_server_time_snapshot = now

        # update game time and save it across reloads
        evennia.gametime.SERVER_RUNTIME_LAST_UPDATED = now
        evennia.ServerConfig.objects.conf("runtime", evennia.gametime.SERVER_RUNTIME)

        if self.maintenance_count % 5 == 0:
            # check cache size every 5 minutes
            self._flush_cache(settings.IDMAPPER_CACHE_MAXSIZE)
        if self.maintenance_count % (60 * 7) == 0:

        self.process_idle_timeouts()

        # run unpuppet hooks for objects that are marked as being puppeted,
        # but which lacks an account (indicates a broken unpuppet operation
        # such as a server crash)
        if self.maintenance_count > 1:
            unpuppet_count = 0
            for obj in evennia.ObjectDB.objects.get_by_tag(key="puppeted", category="account"):
                if not obj.has_account:
                    obj.at_pre_unpuppet()
                    obj.at_post_unpuppet(None, reason=_(" (connection lost)"))
                    obj.tags.remove("puppeted", category="account")
                    unpuppet_count += 1
            if unpuppet_count:
                logger.log_msg(f"Ran unpuppet-hooks for {unpuppet_count} link-dead puppets.")

    def process_idle_timeouts(self):
        # handle idle timeouts
        if settings.IDLE_TIMEOUT > 0:
            reason = _("idle timeout exceeded")
            to_disconnect = []
            for session in (
                sess
                for sess in evennia.SESSION_HANDLER.values()
                if (now - sess.cmd_last) > settings.IDLE_TIMEOUT
            ):
                if not session.account or not session.account.access(
                    session.account, "noidletimeout", default=False
                ):
                    to_disconnect.append(session)

            for session in to_disconnect:
                evennia.SESSION_HANDLER.disconnect(session, reason=reason)

    # Server startup methods

        # Tell the system the server is starting up; some things are not available yet
        try:
            evennia.ServerConfig.objects.conf("server_starting_mode", True)
        except OperationalError:
            print("Server server_starting_mode couldn't be set - database not set up.")

        if settings.AMP_ENABLED:

        ENABLED = []
        if settings.IRC_ENABLED:
            # IRC channel connections
            ENABLED.append("irc")

        if settings.RSS_ENABLED:
            # RSS feed channel connections
            ENABLED.append("rss")

        if settings.GRAPEVINE_ENABLED:
            # Grapevine channel connections
            ENABLED.append("grapevine")

        if settings.GAME_INDEX_ENABLED:
            from evennia.server.game_index_client.service import EvenniaGameIndexService

            egi_service = EvenniaGameIndexService()
            egi_service.setServiceParent(self)

        if ENABLED:
            self.info_dict["irc_rss"] = ", ".join(ENABLED) + " enabled."

        # clear server startup mode
        try:
            evennia.ServerConfig.objects.conf("server_starting_mode", delete=True)
            print("Server server_starting_mode couldn't unset - db not set up.")

        SERVER_SERVICES_PLUGIN_MODULES = make_iter(settings.SERVER_SERVICES_PLUGIN_MODULES)
        for plugin_module in SERVER_SERVICES_PLUGIN_MODULES:
            # external plugin protocols - load here
            plugin_module = mod_import(plugin_module)
            else:
                print(f"Could not load plugin module {plugin_module}")

        ifacestr = ""
        if settings.AMP_INTERFACE != "127.0.0.1":
            ifacestr = "-%s" % settings.AMP_INTERFACE

        self.info_dict["amp"] = "amp %s: %s" % (ifacestr, settings.AMP_PORT)

        from evennia.server import amp_client

        factory = amp_client.AMPClientFactory(self)
        self.amp_service = internet.TCPClient(settings.AMP_HOST, settings.AMP_PORT, factory)
        self.amp_service.setName("ServerAMPClient")
        self.amp_service.setServiceParent(self)

        # Start a django-compatible webserver.

        from evennia.server.webserver import (
            DjangoWebRoot,
            LockableThreadPool,
            PrivateStaticRoot,
            Website,
            WSGIWebServer,
        )

        # start a thread pool and define the root url (/) as a wsgi resource
        # recognized by Django
        threads = LockableThreadPool(
            minthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[0]),
            maxthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[1]),
        )

        web_root = DjangoWebRoot(threads)
        # point our media resources to url /media
        web_root.putChild(b"media", PrivateStaticRoot(settings.MEDIA_ROOT))
        # point our static resources to url /static
        web_root.putChild(b"static", PrivateStaticRoot(settings.STATIC_ROOT))
        self.web_root = web_root

        try:
            WEB_PLUGINS_MODULE = mod_import(settings.WEB_PLUGINS_MODULE)
            WEB_PLUGINS_MODULE = None
            self.info_dict["errors"] = (
                "WARNING: settings.WEB_PLUGINS_MODULE not found - "
                "copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf."
            )

        if WEB_PLUGINS_MODULE:
            # custom overloads
            web_root = WEB_PLUGINS_MODULE.at_webserver_root_creation(web_root)

        web_site = Website(web_root, logPath=settings.HTTP_LOG_FILE)
        web_site.is_portal = False

        self.info_dict["webserver"] = ""
        for proxyport, serverport in settings.WEBSERVER_PORTS:
            # create the webserver (we only need the port for this)
            webserver = WSGIWebServer(threads, serverport, web_site, interface="127.0.0.1")
            webserver.setName("EvenniaWebServer%s" % serverport)
            webserver.setServiceParent(self)

            self.info_dict["webserver"] += "webserver: %s" % serverport

    def sqlite3_prep(self):
        """
        Optimize some SQLite stuff at startup since we
        can't save it to the database.
        """
        if (
            ".".join(str(i) for i in django.VERSION) < "1.2"
            and settings.DATABASES.get("default", {}).get("ENGINE") == "sqlite3"
        ) or (
            hasattr(settings, "DATABASES")
            and settings.DATABASES.get("default", {}).get("ENGINE", None)
            == "django.db.backends.sqlite3"
        ):
            cursor = connection.cursor()
            cursor.execute("PRAGMA cache_size=10000")
            cursor.execute("PRAGMA synchronous=OFF")
            cursor.execute("PRAGMA count_changes=OFF")
            cursor.execute("PRAGMA temp_store=2")

    def update_defaults(self):
        """
        We make sure to store the most important object defaults here, so
        we can catch if they change and update them on-objects automatically.
        This allows for changing default cmdset locations and default
        typeclasses in the settings file and have them auto-update all
        already existing objects.

        """

        # setting names
        settings_names = (
            "CMDSET_CHARACTER",
            "CMDSET_ACCOUNT",
            "BASE_ACCOUNT_TYPECLASS",
            "BASE_OBJECT_TYPECLASS",
            "BASE_CHARACTER_TYPECLASS",
            "BASE_ROOM_TYPECLASS",
            "BASE_EXIT_TYPECLASS",
            "BASE_SCRIPT_TYPECLASS",
            "BASE_CHANNEL_TYPECLASS",
        )
        # get previous and current settings so they can be compared
        settings_compare = list(
            zip(
                [evennia.ServerConfig.objects.conf(name) for name in settings_names],
                [settings.__getattr__(name) for name in settings_names],
            )
        )
        mismatches = [
            i for i, tup in enumerate(settings_compare) if tup[0] and tup[1] and tup[0] != tup[1]
        ]
        if len(
            mismatches
        ):  # can't use any() since mismatches may be [0] which reads as False for any()
            # we have a changed default. Import relevant objects and
            # run the update

            # from evennia.accounts.models import AccountDB
            for i, prev, curr in (
                (i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches
            ):
                # update the database
                self.info_dict[
                    "info"
                ] = " %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..." % (
                    settings_names[i],
                    prev,
                    curr,
                )
                if i == 0:
                    evennia.ObjectDB.objects.filter(db_cmdset_storage__exact=prev).update(
                        db_cmdset_storage=curr
                    )
                if i == 1:
                    evennia.AccountDB.objects.filter(db_cmdset_storage__exact=prev).update(
                    )
                if i == 2:
                    evennia.AccountDB.objects.filter(db_typeclass_path__exact=prev).update(
                        db_typeclass_path=curr
                    )
                if i in (3, 4, 5, 6):
                    evennia.ObjectDB.objects.filter(db_typeclass_path__exact=prev).update(
                    )
                if i == 7:
                    evennia.ScriptDB.objects.filter(db_typeclass_path__exact=prev).update(
                    )
                if i == 8:
                    evennia.ChannelDB.objects.filter(db_typeclass_path__exact=prev).update(
                    )
                # store the new default and clean caches
                evennia.ServerConfig.objects.conf(settings_names[i], curr)
                evennia.ObjectDB.flush_instance_cache()
                evennia.AccountDB.flush_instance_cache()
                evennia.ScriptDB.flush_instance_cache()
                evennia.ChannelDB.flush_instance_cache()
        # if this is the first start we might not have a "previous"
        # setup saved. Store it now.
        [
            evennia.ServerConfig.objects.conf(settings_names[i], tup[1])
            for i, tup in enumerate(settings_compare)
            if not tup[0]
        ]

    def run_initial_setup(self):
        """
        This is triggered by the amp protocol when the connection
        to the portal has been established.
        This attempts to run the initial_setup script of the server.
        It returns if this is not the first time the server starts.
        Once finished the last_initial_setup_step is set to 'done'

        """

        initial_setup = importlib.import_module(settings.INITIAL_SETUP_MODULE)
        last_initial_setup_step = evennia.ServerConfig.objects.conf("last_initial_setup_step")
        try:
            if not last_initial_setup_step:
                # None is only returned if the config does not exist,
                # i.e. this is an empty DB that needs populating.
                self.info_dict["info"] = " Server started for the first time. Setting defaults."
                initial_setup.handle_setup()
            elif last_initial_setup_step not in ("done", -1):
                # last step crashed, so we weill resume from this step.
                # modules and setup will resume from this step, retrying
                # the last failed module. When all are finished, the step
                # is set to 'done' to show it does not need to be run again.
                self.info_dict["info"] = " Resuming initial setup from step '{last}'.".format(
                    last=last_initial_setup_step
                )
                initial_setup.handle_setup(last_initial_setup_step)
            # stop server if this happens.
            print(traceback.format_exc())
            if not settings.TEST_ENVIRONMENT or not evennia.SESSION_HANDLER:
                print("Error in initial setup. Stopping Server + Portal.")
                evennia.SESSION_HANDLER.portal_shutdown()

    def create_default_channels(self):
        """
        check so default channels exist on every restart, create if not.

        """

        from evennia import AccountDB, ChannelDB
        from evennia.utils.create import create_channel

        # mudinfo
        mudinfo_chan = settings.CHANNEL_MUDINFO
        if mudinfo_chan and not ChannelDB.objects.filter(db_key__iexact=mudinfo_chan["key"]):
            channel = create_channel(**mudinfo_chan)
            channel.connect(superuser)
        # connectinfo
        connectinfo_chan = settings.CHANNEL_CONNECTINFO
        if connectinfo_chan and not ChannelDB.objects.filter(
            db_key__iexact=connectinfo_chan["key"]
        ):
            channel = create_channel(**connectinfo_chan)
        # default channels
        for chan_info in settings.DEFAULT_CHANNELS:
            if not ChannelDB.objects.filter(db_key__iexact=chan_info["key"]):
                channel = create_channel(**chan_info)
                channel.connect(superuser)

    def run_init_hooks(self, mode):
        """
        Called by the amp client once receiving sync back from Portal

        Args:
            mode (str): One of shutdown, reload or reset

        """
        from evennia.typeclasses.models import TypedObject

        # start server time and maintenance task
        self.maintenance_task = LoopingCall(self.server_maintenance)

        # update eventual changed defaults
        self.update_defaults()

        # run at_init() on all cached entities on reconnect
        [
            [entity.at_init() for entity in typeclass_db.get_all_cached_instances()]
            for typeclass_db in TypedObject.__subclasses__()
        ]

        self.at_server_init()

        # call correct server hook based on start file value
            logger.log_msg("Server successfully reloaded.")
            self.at_server_reload_start()
            # only run hook, don't purge sessions
            self.at_server_cold_start()
            logger.log_msg("Evennia Server successfully restarted in 'reset' mode.")
            from evennia.objects.models import ObjectDB

            # clear eventual lingering session storages
            ObjectDB.objects.clear_all_sessids()
            logger.log_msg("Evennia Server successfully started.")

        # always call this regardless of start type

    @defer.inlineCallbacks
    def shutdown(self, mode="reload", _reactor_stopping=False):
        """

        mode - sets the server restart mode.
           - 'reload' - server restarts, no "persistent" scripts
             are stopped, at_reload hooks called.
           - 'reset' - server restarts, non-persistent scripts stopped,
             at_shutdown hooks called but sessions will not
             be disconnected.
           - 'shutdown' - like reset, but server will not auto-restart.
        _reactor_stopping - this is set if server is stopped by a kill
           command OR this method was already called
           once - in both cases the reactor is
           dead/stopping already.
        """
            # this means we have already passed through this method
            # once; we don't need to run the shutdown procedure again.
            defer.returnValue(None)

            # call restart hooks
            evennia.ServerConfig.objects.conf("server_restart_mode", "reload")
            yield [o.at_server_reload() for o in evennia.ObjectDB.get_all_cached_instances()]
            yield [p.at_server_reload() for p in evennia.AccountDB.get_all_cached_instances()]
            yield [
                (s._pause_task(auto_pause=True) if s.is_active else None, s.at_server_reload())
                for s in evennia.ScriptDB.get_all_cached_instances()
                if s.id
            ]
            yield evennia.SESSION_HANDLER.all_sessions_portal_sync()
            self.at_server_reload_stop()
            # only save monitor state on reload, not on shutdown/reset
            from evennia.scripts.monitorhandler import MONITOR_HANDLER

            MONITOR_HANDLER.save()
        else:
            if mode == "reset":
                # like shutdown but don't unset the is_connected flag and don't disconnect sessions
                yield [o.at_server_shutdown() for o in evennia.ObjectDB.get_all_cached_instances()]
                yield [p.at_server_shutdown() for p in evennia.AccountDB.get_all_cached_instances()]
                if self.amp_protocol:
                    yield evennia.SESSION_HANDLER.all_sessions_portal_sync()
            else:  # shutdown
                yield [
                    _SA(p, "is_connected", False)
                    for p in evennia.AccountDB.get_all_cached_instances()
                ]
                yield [
                    (p.unpuppet_all(), p.at_server_shutdown())
                ]
                yield evennia.ObjectDB.objects.clear_all_sessids()
            yield [
                (s._pause_task(auto_pause=True), s.at_server_shutdown())
                if s.id and s.is_active
            ]
            evennia.ServerConfig.objects.conf("server_restart_mode", "reset")
            self.at_server_cold_stop()

        # tickerhandler state should always be saved.
        from evennia.scripts.tickerhandler import TICKER_HANDLER

        TICKER_HANDLER.save()

        # always called, also for a reload
        self.at_server_stop()

        if hasattr(self, "web_root"):  # not set very first start
            yield self.web_root.empty_threadpool()

            # kill the server
            reactor.callLater(1, reactor.stop)

        # we make sure the proper gametime is saved as late as possible
        evennia.ServerConfig.objects.conf("runtime", evennia.gametime.runtime())

        """
        Return the server info, for display.

        """

    # server start/stop hooks

    def _call_start_stop(self, hookname):
        """
        Helper method for calling hooks on all modules.

        Args:
            hookname (str): Name of hook to call.

        """
        for mod in self.start_stop_modules:
            if hook := getattr(mod, hookname, None):
                hook()

    def at_server_init(self):
        """
        This is called first when the server is starting, before any other hooks, regardless of how it's starting.
        """
        self._call_start_stop("at_server_init")

        """
        This is called every time the server starts up, regardless of
        how it was shut down.

        """
        self._call_start_stop("at_server_start")

    def at_server_stop(self):
        """
        This is called just before a server is shut down, regardless
        of it is fore a reload, reset or shutdown.

        """
        self._call_start_stop("at_server_stop")

    def at_server_reload_start(self):
        """
        This is called only when server starts back up after a reload.

        """
        self._call_start_stop("at_server_reload_start")

    def at_post_portal_sync(self, mode):
        """
        This is called just after the portal has finished syncing back data to the server
        after reconnecting.

        Args:
            mode (str): One of 'reload', 'reset' or 'shutdown'.

        """

        MONITOR_HANDLER.restore(mode == "reload")

        TICKER_HANDLER.restore(mode == "reload")

        # Un-pause all scripts, stop non-persistent timers
        evennia.ScriptDB.objects.update_scripts_after_server_start()

        # start the task handler
        from evennia.scripts.taskhandler import TASK_HANDLER

        TASK_HANDLER.load()
        TASK_HANDLER.create_delays()

        # create/update channels
        self.create_default_channels()

        # delete the temporary setting
        evennia.ServerConfig.objects.conf("server_restart_mode", delete=True)

    def at_server_reload_stop(self):
        """
        This is called only time the server stops before a reload.

        """
        self._call_start_stop("at_server_reload_stop")

    def at_server_cold_start(self):
        """
        This is called only when the server starts "cold", i.e. after a
        shutdown or a reset.

        """
        # We need to do this just in case the server was killed in a way where
        # the normal cleanup operations did not have time to run.
        from evennia.objects.models import ObjectDB

        ObjectDB.objects.clear_all_sessids()

        # Remove non-persistent scripts

        for script in ScriptDB.objects.filter(db_persistent=False):

        if settings.GUEST_ENABLED:
            for guest in evennia.AccountDB.objects.all().filter(
                db_typeclass_path=settings.BASE_GUEST_TYPECLASS
            ):
                for character in guest.db._playable_characters:
                    if character:
                        character.delete()
                guest.delete()
        self._call_start_stop("at_server_cold_start")

    def at_server_cold_stop(self):
        """
        This is called only when the server goes down due to a shutdown or reset.

        """
        self._call_start_stop("at_server_cold_stop")

"""
This module defines a generic session class. All connection instances
(both on Portal and Server side) should inherit from this class.

"""

class Session:
    """
    This class represents a player's session and is a template for
    both portal- and server-side sessions.

    Each connection will see two session instances created:

     1. A Portal session. This is customized for the respective connection
        protocols that Evennia supports, like Telnet, SSH etc. The Portal
        session must call init_session() as part of its initialization. The
        respective hook methods should be connected to the methods unique
        for the respective protocol so that there is a unified interface
        to Evennia.
     2. A Server session. This is the same for all connected accounts,
        regardless of how they connect.

    The Portal and Server have their own respective sessionhandlers. These
    are synced whenever new connections happen or the Server restarts etc,
    which means much of the same information must be stored in both places
    e.g. the portal can re-sync with the server when the server reboots.

    """

    def init_session(self, protocol_key, address, sessionhandler):
        """
        Initialize the Session. This should be called by the protocol when
        a new session is established.

        Args:
            protocol_key (str): By default, one of 'telnet', 'telnet/ssl', 'ssh',
                'webclient/websocket' or 'webclient/ajax'.
            address (str): Client address.
            sessionhandler (SessionHandler): Reference to the
                main sessionhandler instance.

        """
        # This is currently 'telnet', 'ssh', 'ssl' or 'web'
        self.protocol_key = protocol_key
        # Protocol address tied to this session
        self.address = address

        # suid is used by some protocols, it's a hex key.
        self.suid = None

        # unique id for this session
        self.sessid = 0  # no sessid yet
        # client session id, if given by the client
        self.csessid = None
        # database id for the user connected to this session
        self.uid = None
        # user name, for easier tracking of sessions
        self.uname = None
        # if user has authenticated already or not
        self.logged_in = False

        # database id of puppeted object (if any)

        # session time statistics
        self.cmd_last_visible = self.conn_time
        self.cmd_last = self.conn_time
        self.cmd_total = 0

        self.protocol_flags = {
            "ENCODING": "utf-8",
            "SCREENREADER": False,
            "INPUTDEBUG": False,
            "RAW": False,
            "NOCOLOR": False,
            "LOCALECHO": False,
        }
        self.server_data = {}

        # map of input data to session methods
        self.datamap = {}

        # a back-reference to the relevant sessionhandler this
        # session is stored in.

    def get_sync_data(self):
        """
        Get all data relevant to sync the session.

        Args:
            syncdata (dict): All syncdata values, based on
                the keys given by self._attrs_to_sync.

        """
        return {
            attr: getattr(self, attr) for attr in settings.SESSION_SYNC_ATTRS if hasattr(self, attr)
        }

    def load_sync_data(self, sessdata):
        """
        Takes a session dictionary, as created by get_sync_data, and
        loads it into the correct properties of the session.

        Args:
            sessdata (dict): Session data dictionary.

        """
        for propname, value in sessdata.items():
            if (
                propname == "protocol_flags"
                and isinstance(value, dict)
                and hasattr(self, "protocol_flags")
                and isinstance(self.protocol_flags, dict)
            ):
                # special handling to allow partial update of protocol flags
                self.protocol_flags.update(value)
            else:
                setattr(self, propname, value)

        """
        Called after a session has been fully synced (including
        secondary operations such as setting self.account based
        on uid etc).

        """
            self.protocol_flags.update(
                self.account.attributes.get("_saved_protocol_flags", None) or {}
            )

    # access hooks

        """
        generic hook called from the outside to disconnect this session
        should be connected to the protocols actual disconnect mechanism.

        Args:
            reason (str): Eventual text motivating the disconnect.

        """
        pass

        """
        Generic hook for sending data out through the protocol. Server
        protocols can use this right away. Portal sessions
        should overload this to format/handle the outgoing data as needed.

        """
        pass

        """

        """
        pass

"""
This module defines handlers for storing sessions when handles
sessions of users connecting to the server.

There are two similar but separate stores of sessions:

- ServerSessionHandler - this stores generic game sessions
     for the game. These sessions has no knowledge about
     how they are connected to the world.
- PortalSessionHandler - this stores sessions created by
     twisted protocols. These are dumb connectors that
     handle network communication but holds no game info.

"""
from codecs import decode as codecs_decode

    SIGNAL_ACCOUNT_POST_FIRST_LOGIN,
    SIGNAL_ACCOUNT_POST_LAST_LOGOUT,
    SIGNAL_ACCOUNT_POST_LOGIN,
    SIGNAL_ACCOUNT_POST_LOGOUT,
)
from evennia.utils.utils import callables_from_module, class_from_module, delay, is_iter, make_iter

_FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED

_ServerSession = None
_ServerConfig = None
_OOB_HANDLER = None

_ERR_BAD_UTF8 = _("Your client sent an incorrect UTF-8 sequence.")

class DummySession(object):
    sessid = 0

DUMMYSESSION = DummySession()

_DELAY_CMD_LOGINSTART = settings.DELAY_CMD_LOGINSTART
_MAX_SERVER_COMMANDS_PER_SECOND = 100.0
_MAX_SESSION_COMMANDS_PER_SECOND = 5.0
_MODEL_MAP = None

# input handlers

_INPUT_FUNCS = {}
for modname in make_iter(settings.INPUT_FUNC_MODULES):
    _INPUT_FUNCS.update(callables_from_module(modname))

def delayed_import():
    """
    Helper method for delayed import of all needed entities.

    """
    global _ServerSession, _AccountDB, _ServerConfig, _ScriptDB
    if not _ServerSession:
        # we allow optional arbitrary serversession class for overloading
        _ServerSession = class_from_module(settings.SERVER_SESSION_CLASS)
    if not _AccountDB:
        from evennia.accounts.models import AccountDB as _AccountDB
    if not _ServerConfig:
        from evennia.server.models import ServerConfig as _ServerConfig
    if not _ScriptDB:
        from evennia.scripts.models import ScriptDB as _ScriptDB
    # including once to avoid warnings in Python syntax checkers
    assert _ServerSession, "ServerSession class could not load"
    assert _AccountDB, "AccountDB class could not load"
    assert _ServerConfig, "ServerConfig class could not load"
    assert _ScriptDB, "ScriptDB class c ould not load"

# -----------------------------------------------------------
# SessionHandler base class

class SessionHandler(dict):
    """
    This handler holds a stack of sessions.

    """

        """
        Clean out None-sessions automatically.

        """
        if None in self:
            del self[None]
        return super().__getitem__(key)

    def get(self, key, default=None):
        """

        """
        return super().get(key, default)

        """
        Don't assign None sessions"

        """
        if key is not None:
            super().__setitem__(key, value)

    def __contains__(self, key):
        """
        None-keys are not accepted.

        """
        return False if key is None else super().__contains__(key)

    def get_sessions(self, include_unloggedin=False):
        """
        Returns the connected session objects.

        Args:
            include_unloggedin (bool, optional): Also list Sessions
                that have not yet authenticated.

        Returns:
            sessions (list): A list of `Session` objects.

        """
        if include_unloggedin:
            return list(self.values())
        else:
            return [session for session in self.values() if session.logged_in]

    def get_all_sync_data(self):
        """
        Create a dictionary of sessdata dicts representing all
        sessions in store.

        Returns:
            syncdata (dict): A dict of sync data.

        """
        return dict((sessid, sess.get_sync_data()) for sessid, sess in self.items())

    def clean_senddata(self, session, kwargs):
        """
        Clean up data for sending across the AMP wire. Also apply the
        FuncParser using callables from `settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES`.

        Args:
            session (Session): The relevant session instance.
            kwargs (dict) Each keyword represents a send-instruction, with the keyword itself being
                the name of the instruction (like "text"). Suitable values for each keyword are:
                - arg                ->  [[arg], {}]
                - [args]             ->  [[args], {}]
                - {kwargs}           ->  [[], {kwargs}]
                - [args, {kwargs}]   ->  [[arg], {kwargs}]
                - [[args], {kwargs}] ->  [[args], {kwargs}]

        Returns:
            kwargs (dict): A cleaned dictionary of cmdname:[[args],{kwargs}] pairs,
            where the keys, args and kwargs have all been converted to
            send-safe entities (strings or numbers), and funcparser parsing has been
            applied.

        """
            from evennia.utils.funcparser import FuncParser

            _FUNCPARSER = FuncParser(
                settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES, raise_errors=True
            )

        options = kwargs.pop("options", None) or {}
        raw = options.get("raw", False)
        strip_inlinefunc = options.get("strip_inlinefunc", False)

        def _utf8(data):
            if isinstance(data, bytes):
                try:
                    data = codecs_decode(data, session.protocol_flags["ENCODING"])
                except LookupError:
                    # wrong encoding set on the session. Set it to a safe one
                    session.protocol_flags["ENCODING"] = "utf-8"
                    data = codecs_decode(data, "utf-8")
                except UnicodeDecodeError:
                    # incorrect unicode sequence
                    session.sendLine(_ERR_BAD_UTF8)
                    data = ""

            return data

        def _validate(data):
            """
            Helper function to convert data to AMP-safe (picketable) values"

            """
            if isinstance(data, dict):
                newdict = {}
                for key, part in data.items():
                    newdict[key] = _validate(part)
                return newdict
            elif is_iter(data):
                return [_validate(part) for part in data]
            elif isinstance(data, (str, bytes)):
                data = _utf8(data)

                if (
                    _FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED
                    and not raw
                    and isinstance(self, ServerSessionHandler)
                ):
                    # only apply funcparser on the outgoing path (sessionhandler->)
                    # data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
                    data = _FUNCPARSER.parse(data, strip=strip_inlinefunc, session=session)

                return str(data)
            elif (
                hasattr(data, "id")
                and hasattr(data, "db_date_created")
                and hasattr(data, "__dbclass__")
            ):
                # convert database-object to their string representation.
                return _validate(str(data))
            else:
                return data

        rkwargs = {}
        for key, data in kwargs.items():
            key = _validate(key)
            if not data:
                if key == "text":
                    # we don't allow sending text = None, this must mean
                    # that the text command is not to be used.
                    continue
                rkwargs[key] = [[], {}]
            elif isinstance(data, dict):
                rkwargs[key] = [[], _validate(data)]
                data = tuple(data)
                if isinstance(data[-1], dict):
                    if len(data) == 2:
                        if is_iter(data[0]):
                            rkwargs[key] = [_validate(data[0]), _validate(data[1])]
                        else:
                            rkwargs[key] = [[_validate(data[0])], _validate(data[1])]
                    else:
                        rkwargs[key] = [_validate(data[:-1]), _validate(data[-1])]
                else:
                    rkwargs[key] = [_validate(data), {}]
            else:
                rkwargs[key] = [[_validate(data)], {}]
            rkwargs[key][1]["options"] = dict(options)
        # make sure that any "prompt" message will be processed last
        # by moving it to the end
        if "prompt" in rkwargs:
            prompt = rkwargs.pop("prompt")
            rkwargs["prompt"] = prompt
        return rkwargs

# Server-SessionHandler class

class ServerSessionHandler(SessionHandler):
    """
    This object holds the stack of sessions active in the game at any time.

    A session register with the handler in two steps, first by registering itself with the connect()
    method. This indicates an non-authenticated session. Whenever the session is authenticated the
    session together with the related account is sent to the login() method.

    """

    # AMP communication methods

        """
        Init the handler.

        """
        evennia.server_data = {"servername": _SERVERNAME}
        # will be set on psync
        self.portal_start_time = 0.0

    def _run_cmd_login(self, session):
        """
        Launch the CMD_LOGINSTART command. This is wrapped
        for delays.

        """
            self.data_in(session, text=[[CMD_LOGINSTART], {}])

    def portal_connect(self, portalsessiondata):
        """
        Called by Portal when a new session has connected.
        Creates a new, unlogged-in game session.

        Args:
            portalsessiondata (dict): a dictionary of all property:value
                keys defining the session and which is marked to be
                synced.

        """
        delayed_import()
        global _ServerSession, _AccountDB, _ScriptDB

        sess = _ServerSession()
        sess.sessionhandler = self
        sess.load_sync_data(portalsessiondata)
        sess.at_sync()
        # validate all scripts
        # _ScriptDB.objects.validate()
        self[sess.sessid] = sess

        if sess.logged_in and sess.uid:
            # Session is already logged in. This can happen in the
            # case of auto-authenticating protocols like SSH or
            # webclient's session sharing
            account = _AccountDB.objects.get_account_from_uid(sess.uid)
                # this will set account.is_connected too
                self.login(sess, account, force=True)
                return
            else:
                sess.logged_in = False
                sess.uid = None

        # show the first login command, may delay slightly to allow
        # the handshakes to finish.
        delay(_DELAY_CMD_LOGINSTART, self._run_cmd_login, sess)

    def portal_session_sync(self, portalsessiondata):
        """
        Called by Portal when it wants to update a single session (e.g.
        because of all negotiation protocols have finally replied)

        Args:
                synced.

        """
        sessid = portalsessiondata.get("sessid")
        session = self.get(sessid)
            # since some of the session properties may have had
            # a chance to change already before the portal gets here
            # the portal doesn't send all sessiondata but only
            # ones which should only be changed from portal (like
            # protocol_flags etc)
            session.load_sync_data(portalsessiondata)

    def portal_sessions_sync(self, portalsessionsdata):
        """
        Syncing all session ids of the portal with the ones of the
        server. This is instantiated by the portal when reconnecting.

        Args:
            portalsessionsdata (dict): A dictionary
              `{sessid: {property:value},...}` defining each session and
              the properties in it which should be synced.

        """
        global _ServerSession, _AccountDB, _ServerConfig, _ScriptDB

        for sess in list(self.values()):
            # we delete the old session to make sure to catch eventual
            # lingering references.
            del sess

        for sessid, sessdict in portalsessionsdata.items():
            sess = _ServerSession()
            sess.sessionhandler = self
            sess.load_sync_data(sessdict)
            if sess.uid:
                sess.account = _AccountDB.objects.get_account_from_uid(sess.uid)
            self[sessid] = sess
            sess.at_sync()

        mode = "reload"

        # tell the server hook we synced
        evennia.EVENNIA_SERVER_SERVICE.at_post_portal_sync(mode)
        # announce the reconnection
            self.announce_all(_(" ... Server restarted."))

    def portal_disconnect(self, session):
        """
        Called from Portal when Portal session closed from the portal
        side. There is no message to report in this case.

        Args:
            session (Session): The Session to disconnect

        """
        # disconnect us without calling Portal since
        # Portal already knows.
        self.disconnect(session, reason="", sync_portal=False)

    def portal_disconnect_all(self):
        """
        Called from Portal when Portal is closing down. All
        Sessions should die. The Portal should not be informed.

        """
        # set a watchdog to avoid self.disconnect from deleting
        # the session while we are looping over them
        self._disconnect_all = True
            session.disconnect()
        del self._disconnect_all

    # server-side access methods

    def start_bot_session(self, protocol_path, configdict):
        """
        This method allows the server-side to force the Portal to
        create a new bot session.

        Args:
            protocol_path (str): The  full python path to the bot's
                class.
            configdict (dict): This dict will be used to configure
                the bot (this depends on the bot protocol).

        Examples:
            start_bot_session("evennia.server.portal.irc.IRCClient",
                              {"uid":1,  "botname":"evbot", "channel":"#evennia",
                               "network:"irc.freenode.net", "port": 6667})

        Notes:
            The new session will use the supplied account-bot uid to
            initiate an already logged-in connection. The Portal will
            treat this as a normal connection and henceforth so will
            the Server.

        """
        evennia.EVENNIA_SERVER_SERVICE.amp_protocol.send_AdminServer2Portal(
            DUMMYSESSION, operation=amp.SCONN, protocol_path=protocol_path, config=configdict
        )

    def portal_restart_server(self):
        """
        Called by server when reloading. We tell the portal to start a new server instance.

        """
            DUMMYSESSION, operation=amp.SRELOAD
        )

    def portal_reset_server(self):
        """

        """
            DUMMYSESSION, operation=amp.SRESET
        )

    def portal_shutdown(self):
        """
        Called by server when it's time to shut down (the portal will shut us down and then shut
        itself down)

        """
            DUMMYSESSION, operation=amp.PSHUTD
        )

    def login(self, session, account, force=False, testmode=False):
        """
        Log in the previously unloggedin session and the account we by now should know is connected
        to it. After this point we assume the session to be logged in one way or another.

        Args:
            session (Session): The Session to authenticate.
            account (Account): The Account identified as associated with this Session.
            force (bool): Login also if the session thinks it's already logged in
                (this can happen for auto-authenticating protocols)
            testmode (bool, optional): This is used by unittesting for
                faking login without any AMP being actually active.

        """
        if session.logged_in and not force:
            # don't log in a session that is already logged in.
            return

        account.is_connected = True

        # sets up and assigns all properties on the session
        session.at_login(account)

        # account init
        account.at_init()

        # Check if this is the first time the *account* logs in
        if account.db.FIRST_LOGIN:
            account.at_first_login()
            del account.db.FIRST_LOGIN

        account.at_pre_login()

        if _MULTISESSION_MODE == 0:
            # disconnect all previous sessions.
            self.disconnect_duplicate_sessions(session)

        nsess = len(self.sessions_from_account(account))
        string = "Logged in: {account} {address} ({nsessions} session(s) total)"
        string = string.format(account=account, address=session.address, nsessions=nsess)
        session.log(string)
        session.logged_in = True
        # sync the portal to the session
        if not testmode:
            evennia.EVENNIA_SERVER_SERVICE.amp_protocol.send_AdminServer2Portal(
                session, operation=amp.SLOGIN, sessiondata={"logged_in": True, "uid": session.uid}
            )
        account.at_post_login(session=session)
        if nsess < 2:
            SIGNAL_ACCOUNT_POST_FIRST_LOGIN.send(sender=account, session=session)
        SIGNAL_ACCOUNT_POST_LOGIN.send(sender=account, session=session)

    def disconnect(self, session, reason="", sync_portal=True):
        """
        Called from server side to remove session and inform portal
        of this fact.

        Args:
            session (Session): The Session to disconnect.
            reason (str, optional): A motivation for the disconnect.
            sync_portal (bool, optional): Sync the disconnect to
                Portal side. This should be done unless this was
                called by self.portal_disconnect().

        """
        session = self.get(session.sessid)
            return

        if hasattr(session, "account") and session.account:
            # only log accounts logging off
            nsess = len(self.sessions_from_account(session.account)) - 1
            sreason = " ({})".format(reason) if reason else ""
            string = "Logged out: {account} {address} ({nsessions} sessions(s) remaining){reason}"
            string = string.format(
                reason=sreason, account=session.account, address=session.address, nsessions=nsess
            )
            session.log(string)

            if nsess == 0:
                SIGNAL_ACCOUNT_POST_LAST_LOGOUT.send(sender=session.account, session=session)

        session.at_disconnect(reason)
        SIGNAL_ACCOUNT_POST_LOGOUT.send(sender=session.account, session=session)
        sessid = session.sessid
        if sessid in self and not hasattr(self, "_disconnect_all"):
            del self[sessid]
        if sync_portal:
            # inform portal that session should be closed.
                session, operation=amp.SDISCONN, reason=reason
            )

    def all_sessions_portal_sync(self):
        """
        This is called by the server when it reboots. It syncs all session data
        to the portal. Returns a deferred!

        """
        sessdata = self.get_all_sync_data()
        return evennia.EVENNIA_SERVER_SERVICE.amp_protocol.send_AdminServer2Portal(
            DUMMYSESSION, operation=amp.SSYNC, sessiondata=sessdata
        )

    def session_portal_sync(self, session):
        """
        This is called by the server when it wants to sync a single session
        with the Portal for whatever reason. Returns a deferred!

        """
        sessdata = {session.sessid: session.get_sync_data()}
            DUMMYSESSION, operation=amp.SSYNC, sessiondata=sessdata, clean=False
        )

    def session_portal_partial_sync(self, session_data):
        """
        Call to make a partial update of the session, such as only a particular property.

        Args:
            session_data (dict): Store `{sessid: {property:value}, ...}` defining one or
                more sessions in detail.

        """
            DUMMYSESSION, operation=amp.SSYNC, sessiondata=session_data, clean=False
        )

    def disconnect_all_sessions(self, reason="You have been disconnected."):
        """
        Cleanly disconnect all of the connected sessions.

        Args:
            reason (str, optional): The reason for the disconnection.

        """

        for session in self:
        # tell portal to disconnect all sessions
            DUMMYSESSION, operation=amp.SDISCONNALL, reason=reason
        )

    def disconnect_duplicate_sessions(
        self, curr_session, reason=_("Logged in from elsewhere. Disconnecting.")
    ):
        """
        Disconnects any existing sessions with the same user.

        args:
            curr_session (Session): Disconnect all Sessions matching this one.
            reason (str, optional): A motivation for disconnecting.

        """
        uid = curr_session.uid
        # we can't compare sessions directly since this will compare addresses and
        # mean connecting from the same host would not catch duplicates
        sid = id(curr_session)
        doublet_sessions = [
            sess for sess in self.values() if sess.logged_in and sess.uid == uid and id(sess) != sid
        ]

        for session in doublet_sessions:
            self.disconnect(session, reason)

    def validate_sessions(self):
        """
        Check all currently connected sessions (logged in and not) and
        see if any are dead or idle.

        """
        tcurr = time.time()
        reason = _("Idle timeout exceeded, disconnecting.")
        for session in (
            session
            for session in self.values()
            if session.logged_in
            and _IDLE_TIMEOUT > 0
            and (tcurr - session.cmd_last) > _IDLE_TIMEOUT
        ):
            self.disconnect(session, reason=reason)

    def account_count(self):
        """
        Get the number of connected accounts (not sessions since a
        account may have more than one session depending on settings).
        Only logged-in accounts are counted here.

        Returns:
            naccount (int): Number of connected accounts

        """
        return len(set(session.uid for session in self.values() if session.logged_in))

    def all_connected_accounts(self):
        """
        Get a unique list of connected and logged-in Accounts.

        Returns:
            accounts (list): All connected Accounts (which may be fewer than the
                amount of Sessions due to multi-playing).

        """
        return list(
            set(
                session.account
                for session in self.values()
                if session.logged_in and session.account
            )
        )

    def session_from_sessid(self, sessid):
        """
        Get session based on sessid, or None if not found

        Args:
            sessid (int or list): Session id(s).

        Return:
            sessions (Session or list): Session(s) found. This
                is a list if input was a list.

        """
        if is_iter(sessid):
            return [self.get(sid) for sid in sessid if sid in self]
        return self.get(sessid)

    def session_from_account(self, account, sessid):
        """
        Given an account and a session id, return the actual session
        object.

        Args:
            account (Account): The Account to get the Session from.

        Returns:
            sessions (Session or list): Session(s) found.

        """
        sessions = [
            self[sid]
            for sid in make_iter(sessid)
            if sid in self and self[sid].logged_in and account.uid == self[sid].uid
        ]
        return sessions[0] if len(sessions) == 1 else sessions

    def sessions_from_account(self, account):
        """
        Given an account, return all matching sessions.

        Args:
            account (Account): Account to get sessions from.

        Returns:
            sessions (list): All Sessions associated with this account.

        """
        uid = account.uid
        return [session for session in self.values() if session.logged_in and session.uid == uid]

    def sessions_from_puppet(self, puppet):
        """
        Given a puppeted object, return all controlling sessions.

        Args:
            puppet (Object): Object puppeted

        Returns.
            sessions (Session or list): Can be more than one of Object is controlled by more than
                one Session (MULTISESSION_MODE > 1).

        """
        sessions = puppet.sessid.get()

    sessions_from_character = sessions_from_puppet

        """
        Given a client identification hash (for session types that offer them)
        return all sessions with a matching hash.

        Args
            csessid (str): The session hash.

        Returns:
            sessions (list): The sessions with matching .csessid, if any.

        """
        if not csessid:
            return []
        return [
            session for session in self.values() if session.csessid and session.csessid == csessid
        ]

        """
        Send message to all connected sessions

        Args:
            message (str): Message to send.

        """
            self.data_out(session, text=message)

        """
        Sending data Server -> Portal

        Args:
            session (Session): Session to relay to.
            text (str, optional): text data to return

        Notes:
            The outdata will be scrubbed for sending across
            the wire here.
        """
        # clean output for sending
        kwargs = self.clean_senddata(session, kwargs)

        # send across AMP
        evennia.EVENNIA_SERVER_SERVICE.amp_protocol.send_MsgServer2Portal(session, **kwargs)

    def get_inputfuncs(self):
        """
        Get all registered inputfuncs (access function)

        Returns:
            inputfuncs (dict): A dict of {key:inputfunc,...}
        """
        return _INPUT_FUNCS

        """
        We let the data take a "detour" to session.data_in
        so the user can override and see it all in one place.
        That method is responsible to in turn always call
        this class' `sessionhandler.call_inputfunc` with the
        (possibly processed) data.

        """
            session.data_in(**kwargs)

    def call_inputfuncs(self, session, **kwargs):
        """
        Split incoming data into its inputfunc counterparts. This should be
        called by the `serversession.data_in` as
        `sessionhandler.call_inputfunc(self, **kwargs)`.

        We also intercept OOB communication here.

        Args:
            sessions (Session): Session.

            any (tuple): Incoming data from protocol, each
                on the form `commandname=((args), {kwargs})`.

        """

        # distribute incoming data to the correct receiving methods.
            input_debug = session.protocol_flags.get("INPUTDEBUG", False)
                cname = cmdname.strip().lower()
                try:
                    cmdkwargs.pop("options", None)
                    if cname in _INPUT_FUNCS:
                        _INPUT_FUNCS[cname](session, *cmdargs, **cmdkwargs)
                    else:
                        _INPUT_FUNCS["default"](session, cname, *cmdargs, **cmdkwargs)
                    if input_debug:
                        session.msg(err)
                    log_trace()

# These are filled in during server boot.
SESSIONS = None  # legacy

from django.core.cache import caches

class Throttle:
    """
    Keeps a running count of failed actions per IP address.

    Available methods indicate whether or not the number of failures exceeds a
    particular threshold.

    This version of the throttle is usable by both the terminal server as well
    as the web server, imposes limits on memory consumption by using deques
    with length limits instead of open-ended lists, and uses native Django
    caches for automatic key eviction and persistence configurability.
    """

    error_msg = _("Too many failed attempts; you must wait a few minutes before trying again.")

        """
        Allows setting of throttle parameters.

            name (str): Name of this throttle.
            limit (int): Max number of failures before imposing limiter. If `None`,
                the throttle is disabled.
            timeout (int): number of timeout seconds after
                max number of tries has been reached.
            cache_size (int): Max number of attempts to record per IP within a
                rolling window; this is NOT the same as the limit after which
                the throttle is imposed!
        """
        try:
            self.storage = caches["throttle"]
            logger.log_trace("Throttle: Errors encountered; using default cache.")
            self.storage = caches["default"]

        self.name = kwargs.get("name", "undefined-throttle")
        self.limit = kwargs.get("limit", 5)
        self.cache_size = kwargs.get("cache_size", self.limit)
        self.timeout = kwargs.get("timeout", 5 * 60)

    def get_cache_key(self, *args, **kwargs):
        """
        Creates a 'prefixed' key containing arbitrary terms to prevent key
        collisions in the same namespace.

        """
        return "-".join((self.name, *args))

    def touch(self, key, *args, **kwargs):
        """
        Refreshes the timeout on a given key and ensures it is recorded in the
        key register.

        Args:
            key(str): Key of entry to renew.

        """
        cache_key = self.get_cache_key(key)
        if self.storage.touch(cache_key, self.timeout):
            self.record_key(key)

    def get(self, ip=None):
        """
        Convenience function that returns the storage table, or part of.

        Args:
            ip (str, optional): IP address of requestor

        Returns:
            storage (dict): When no IP is provided, returns a dict of all
                current IPs being tracked and the timestamps of their recent
                failures.
            timestamps (deque): When an IP is provided, returns a deque of
                timestamps of recent failures only for that IP.

        """
        if ip:
            cache_key = self.get_cache_key(str(ip))
            return self.storage.get(cache_key, deque(maxlen=self.cache_size))
        else:
            keys_key = self.get_cache_key("keys")
            keys = self.storage.get_or_set(keys_key, set(), self.timeout)
            data = self.storage.get_many((self.get_cache_key(x) for x in keys))

            found_keys = set(data.keys())
            if len(keys) != len(found_keys):
                self.storage.set(keys_key, found_keys, self.timeout)

    def update(self, ip, failmsg="Exceeded threshold."):
        """
        Store the time of the latest failure.

        Args:
            ip (str): IP address of requestor
            failmsg (str, optional): Message to display in logs upon activation
                of throttle.

        Returns:
            None

        """
        cache_key = self.get_cache_key(ip)

        # Get current status
        previously_throttled = self.check(ip)

        # Get previous failures, if any
        entries = self.storage.get(cache_key, [])
        entries.append(time.time())

        # Store updated record
        self.storage.set(cache_key, deque(entries, maxlen=self.cache_size), self.timeout)

        # See if this update caused a change in status
        currently_throttled = self.check(ip)

        # If this makes it engage, log a single activation event
        if not previously_throttled and currently_throttled:
                f"Throttle Activated: {failmsg} (IP: {ip}, "
                f"{self.limit} hits in {self.timeout} seconds.)"
            )

        self.record_ip(ip)

    def remove(self, ip, *args, **kwargs):
        """
        Clears data stored for an IP from the throttle.

        Args:
            ip(str): IP to clear.

        """
        exists = self.get(ip)
        if not exists:

        self.storage.delete(cache_key)
        self.unrecord_ip(ip)

        # Return True if NOT exists
        return not bool(self.get(ip))

    def record_ip(self, ip, *args, **kwargs):
        """
        Tracks keys as they are added to the cache (since there is no way to
        get a list of keys after-the-fact).

        Args:
            ip(str): IP being added to cache. This should be the original
                IP, not the cache-prefixed key.

        """
        keys_key = self.get_cache_key("keys")
        keys = self.storage.get(keys_key, set())
        keys.add(ip)
        self.storage.set(keys_key, keys, self.timeout)

    def unrecord_ip(self, ip, *args, **kwargs):
        """
        Forces removal of a key from the key registry.

        Args:
            ip(str): IP to remove from list of keys.

        """
        try:
            keys.remove(ip)
            self.storage.set(keys_key, keys, self.timeout)

    def check(self, ip):
        """
        This will check the session's address against the
        storage dictionary to check they haven't spammed too many
        fails recently.

        Args:

        Returns:
            throttled (bool): True if throttling is active,
                False otherwise.

        """
        if self.limit is None:
            # throttle is disabled

        ip = str(ip)

        # checking mode
        latest_fails = self.storage.get(cache_key)
        if latest_fails and len(latest_fails) >= self.limit:
            # too many fails recently
            if now - latest_fails[-1] < self.timeout:
                # too soon - timeout in play
                self.touch(cache_key)
            else:
                # timeout has passed. clear faillist
                self.remove(ip)
        else:

import re

from django.core.exceptions import ValidationError

class EvenniaUsernameAvailabilityValidator:
    """
    Checks to make sure a given username is not taken or otherwise reserved.
    """

    def __call__(self, username):
        """
        Validates a username to make sure it is not in use or reserved.

        Args:

        Returns:
            None (None): None if password successfully validated,
                raises ValidationError otherwise.

        """
        # Check guest list
        if settings.GUEST_LIST and username.lower() in (
            guest.lower() for guest in settings.GUEST_LIST
        ):
            raise ValidationError(
                _("Sorry, that username is reserved."), code="evennia_username_reserved"
            )

        # Check database
        exists = AccountDB.objects.filter(username__iexact=username).exists()
        if exists:
                _("Sorry, that username is already taken."), code="evennia_username_taken"
            )

class EvenniaPasswordValidator:
        self,
        regex=r"^[\w. @+\-',]+$",
        policy="Password should contain a mix of letters, spaces, digits and @/./+/-/_/'/, only.",
    ):
        """
        Constructs a standard Django password validator.

        Args:
            regex (str): Regex pattern of valid characters to allow.
            policy (str): Brief explanation of what the defined regex permits.

        """
        self.regex = regex
        self.policy = policy

    def validate(self, password, user=None):
        """
        Validates a password string to make sure it meets predefined Evennia
        acceptable character policy.

        Args:
            user (None): Unused argument but required by Django

        Returns:

        """
        # Check complexity
        if not re.findall(self.regex, password):
            raise ValidationError(_(self.policy), code="evennia_password_policy")

    def get_help_text(self):
        """
        Returns a user-facing explanation of the password policy defined
        by this validator.

        Returns:
            text (str): Explanation of password policy.

        """
        return _(
            "{policy} From a terminal client, you can also use a phrase of multiple words if "
            "you enclose the password in double quotes.".format(policy=self.policy)
        )

"""
This implements resources for Twisted webservers using the WSGI
interface of Django. This alleviates the need of running e.g. an
Apache server to serve Evennia's web presence (although you could do
that too if desired).

The actual servers are started inside server.py as part of the Evennia
application.

(Lots of thanks to http://github.com/clemesha/twisted-wsgi-django for
a great example/aid on how to do this.)

"""
from urllib.parse import quote as urlquote

from django.core.wsgi import get_wsgi_application
from twisted.python import threadpool
from twisted.web import http, resource, server, static
from twisted.web.proxy import ReverseProxyResource
from twisted.web.server import NOT_DONE_YET
from twisted.web.wsgi import WSGIResource

_DEBUG = settings.DEBUG

class LockableThreadPool(threadpool.ThreadPool):
    """
    Threadpool that can be locked from accepting new requests.
    """

        self._accept_new = True
        threadpool.ThreadPool.__init__(self, *args, **kwargs)

    def lock(self):
        self._accept_new = False

    def callInThread(self, func, *args, **kwargs):
        """
        called in the main reactor thread. Makes sure the pool
        is not locked before continuing.
        """
        if self._accept_new:
            threadpool.ThreadPool.callInThread(self, func, *args, **kwargs)

#
# X-Forwarded-For Handler
#

class HTTPChannelWithXForwardedFor(http.HTTPChannel):
    """
    HTTP xforward class

    """

    def allHeadersReceived(self):
        """
        Check to see if this is a reverse proxied connection.

        """
        if self.requests:
            CLIENT = 0
            http.HTTPChannel.allHeadersReceived(self)
            req = self.requests[-1]
            client_ip, port = self.transport.client
            proxy_chain = req.getHeader("X-FORWARDED-FOR")
            if proxy_chain and client_ip in _UPSTREAM_IPS:
                forwarded = proxy_chain.split(", ", 1)[CLIENT]
                self.transport.client = (forwarded, port)

# Monkey-patch Twisted to handle X-Forwarded-For.

http.HTTPFactory.protocol = HTTPChannelWithXForwardedFor

class EvenniaReverseProxyResource(ReverseProxyResource):
    def getChild(self, path, request):
        """
        Create and return a proxy resource with the same proxy configuration
        as this one, except that its path also contains the segment given by
        path at the end.

        Args:
            path (str): Url path.
            request (Request object): Incoming request.

        Return:
            resource (EvenniaReverseProxyResource): A proxy resource.

        """
        request.notifyFinish().addErrback(
            lambda f: 0
            # lambda f: logger.log_trace("%s\nCaught errback in webserver.py" % f)
        )
        return EvenniaReverseProxyResource(
            self.host, self.port, self.path + "/" + urlquote(path, safe=""), self.reactor
        )

    def render(self, request):
        """
        Render a request by forwarding it to the proxied server.

        Args:

        Returns:
            not_done (char): Indicator to note request not yet finished.

        """
        # RFC 2616 tells us that we can omit the port if it's the default port,
        # but we have to provide it otherwise
        request.content.seek(0, 0)
        qs = urllib.parse.urlparse(request.uri)[4]
        if qs:
            rest = self.path + "?" + qs.decode()
        else:
            rest = self.path
        rest = rest.encode()
        clientFactory = self.proxyClientFactoryClass(
            request.method,
            rest,
            request.clientproto,
            request.getAllHeaders(),
            request.content.read(),
            request,
        )
        clientFactory.noisy = False
        self.reactor.connectTCP(self.host, self.port, clientFactory)
        # don't trigger traceback if connection is lost before request finish.
        request.notifyFinish().addErrback(lambda f: 0)
        # request.notifyFinish().addErrback(
        #   lambda f:logger.log_trace("Caught errback in webserver.py: %s" % f)
        return NOT_DONE_YET

#
# Website server resource
#

class DjangoWebRoot(resource.Resource):
    """
    This creates a web root (/) that Django
    understands by tweaking the way
    child instances are recognized.
    """

    def __init__(self, pool):
        """
        Setup the django+twisted resource.

        Args:
            pool (ThreadPool): The twisted threadpool.

        """
        self.pool = pool
        self._echo_log = True
        self._pending_requests = {}
        self.wsgi_resource = WSGIResource(reactor, pool, get_wsgi_application())

    def empty_threadpool(self):
        """
        Converts our _pending_requests list of deferreds into a DeferredList

        Returns:
            deflist (DeferredList): Contains all deferreds of pending requests.

        """
        self.pool.lock()
        if self._pending_requests and self._echo_log:
            self._echo_log = False  # just to avoid multiple echoes
            msg = "Webserver waiting for %i requests ... "
            logger.log_info(msg % len(self._pending_requests))
        return defer.DeferredList(self._pending_requests, consumeErrors=True)

    def _decrement_requests(self, *args, **kwargs):
        self._pending_requests.pop(kwargs.get("deferred", None), None)

        """
        To make things work we nudge the url tree to make this the
        root.

        Args:

        Notes:
            We make sure to save the request queue so
            that we can safely kill the threadpool
            on a server reload.

        """
        path0 = request.prepath.pop(0)
        request.postpath.insert(0, path0)

            lambda f: 0
            # lambda f: logger.log_trace("%s\nCaught errback in webserver.py:" % f)
        )

        deferred = request.notifyFinish()
        self._pending_requests[deferred] = deferred
        deferred.addBoth(self._decrement_requests, deferred=deferred)

        return self.wsgi_resource

#
# Site with deactivateable logging
#

class Website(server.Site):
    """
    This class will only log http requests if settings.DEBUG is True.
    """

        "How to be named in logs"
        if hasattr(self, "is_portal") and self.is_portal:
            return "Webserver-proxy"
        return "Webserver"

    def log(self, request):
        """Conditional logging"""
        if _DEBUG:
            server.Site.log(self, request)

#
# Threaded Webserver
#

class WSGIWebServer(internet.TCPServer):
    """
    This is a WSGI webserver. It makes sure to start
    the threadpool after the service itself started,
    so as to register correctly with the twisted daemon.

    call with WSGIWebServer(threadpool, port, wsgi_resource)

    """

    def __init__(self, pool, *args, **kwargs):
        """
        This just stores the threadpool.

        Args:
            args, kwargs (any): Passed on to the TCPServer.

        """

        """
        Start the pool after the service starts.

        """
        try:
            super().startService()
            self.pool.start()
            logger.log_trace("Webserver did not start correctly. Disabling.")
            self.stopService()

        """
        Safely stop the pool after the service stops.

        """
        try:
            super().stopService()
            logger.log_trace("Webserver stopService error.")
        finally:
            if self.pool.started:
                self.pool.stop()

class PrivateStaticRoot(static.File):
    """
    This overrides the default static file resource so as to not make the
    directory listings public (that is, if you go to /media or /static you
    won't see an index of all static/media files on the server).

    """

    def directoryListing(self):
        return resource.ForbiddenResource()

"""
Attributes are arbitrary data stored on objects. Attributes supports
both pure-string values and pickled arbitrary data.

Attributes are also used to implement Nicks. This module also contains
the Attribute- and NickHandlers as well as the `NAttributeHandler`,
which is a non-db version of Attributes.

"""
import fnmatch
import re

from evennia.utils.picklefield import PickledObjectField
from evennia.utils.utils import is_iter, lazy_property, make_iter, to_str

_TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE

#
#   Attributes
#

class IAttribute:
    """
    Attributes are things that are specific to different types of objects. For
    example, a drink container needs to store its fill level, whereas an exit
    needs to store its open/closed/locked/unlocked state. These are done via
    attributes, rather than making different classes for each object type and
    storing them directly. The added benefit is that we can add/remove
    attributes on the fly as we like.

    The Attribute class defines the following properties:
     - key (str): Primary identifier.
     - lock_storage (str): Perm strings.
     - model (str): A string defining the model this is connected to. This
        is a natural_key, like "objects.objectdb"
     - date_created (datetime): When the attribute was created.
     - value (any): The data stored in the attribute, in pickled form
        using wrappers to be able to store/retrieve models.
     - strvalue (str): String-only data. This data is not pickled and
        is thus faster to search for in the database.
     - category (str): Optional character string for grouping the
        Attribute.

    This class is an API/Interface/Abstract base class; do not instantiate it directly.
    """

    key = property(lambda self: self.db_key)
    strvalue = property(lambda self: self.db_strvalue)
    category = property(lambda self: self.db_category)
    model = property(lambda self: self.db_model)
    attrtype = property(lambda self: self.db_attrtype)
    date_created = property(lambda self: self.db_date_created)

    def __lock_storage_get(self):
        return self.db_lock_storage

    def __lock_storage_set(self, value):
        self.db_lock_storage = value

    def __lock_storage_del(self):
        self.db_lock_storage = ""

    lock_storage = property(__lock_storage_get, __lock_storage_set, __lock_storage_del)

    def access(self, accessing_obj, access_type="read", default=False, **kwargs):
        """

        Args:
            accessing_obj (object): Entity trying to access this one.
            access_type (str, optional): Type of access sought, see
                the lock documentation.
            default (bool, optional): What result to return if no lock
                of access_type was found. The default, `False`, means a lockdown
                policy, only allowing explicit access.
            kwargs (any, optional): Not used; here to make the API consistent with
                other access calls.

        Returns:
            result (bool): If the lock was passed or not.

        """
        result = self.locks.check(accessing_obj, access_type=access_type, default=default)

    #
    #
    # Attribute methods
    #
    #

        return smart_str("%s(%s)" % (self.db_key, self.id))

        return "%s(%s)" % (self.db_key, self.id)

class InMemoryAttribute(IAttribute):
    """
    This Attribute is used purely for NAttributes/NAttributeHandler. It has no database backend.

    """

    # Primary Key has no meaning for an InMemoryAttribute. This merely serves to satisfy other code.

    def __init__(self, pk, **kwargs):
        """
        Create an Attribute that exists only in Memory.

        Args:
            pk (int): This is a fake 'primary key' / id-field. It doesn't actually have to be
                unique, but is fed an incrementing number from the InMemoryBackend by default. This
                is needed only so Attributes can be sorted. Some parts of the API also see the lack
                of a .pk field as a sign that the Attribute was deleted.
            **kwargs: Other keyword arguments are used to construct the actual Attribute.

        """
        self.id = pk
        self.pk = pk

        # Copy all kwargs to local properties. We use db_ for compatability here.
            # Value and locks are special. We must call the wrappers.
            if key == "value":
                self.value = value
            elif key == "lock_storage":
                self.lock_storage = value
            else:
                setattr(self, f"db_{key}", value)

        return self.db_value

    def __value_set(self, new_value):
        self.db_value = new_value

        pass

class AttributeProperty:
    """
    AttributeProperty.

    """

    attrhandler_name = "attributes"

    def __init__(self, default=None, category=None, strattr=False, lockstring="", autocreate=True):
        """
        Allows for specifying Attributes as Django-like 'fields' on the class level. Note that while
        one can set a lock on the Attribute, there is no way to *check* said lock when accessing via
        the property - use the full `AttributeHandler` if you need to do access checks. Note however
        that if you use the full `AttributeHandler` to access this Attribute, the `at_get/at_set`
        methods on this class will _not_ fire (because you are bypassing the `AttributeProperty`
        entirely in that case).

        Initialize an Attribute as a property descriptor.

            default (any): A default value if the attr is not set. If a callable, this will be
                run without any arguments and is expected to return the default value.
            category (str): The attribute's category. If unset, use class default.
            strattr (bool): If set, this Attribute *must* be a simple string, and will be
                stored more efficiently.
            lockstring (str): This is not itself useful with the property, but only if
                using the full AttributeHandler.get(accessing_obj=...) to access the
                Attribute.
            autocreate (bool): True by default; this means Evennia makes sure to create a new
                copy of the Attribute (with the default value) whenever a new object with this
                property is created. If `False`, no Attribute will be created until the property
                is explicitly assigned a value. This makes it more efficient while it retains
                its default (there's no db access), but without an actual Attribute generated,
                one cannot access it via .db, the AttributeHandler or see it with `examine`.
        Example:
        ::

            class Character(DefaultCharacter):
                foo = AttributeProperty(default="Bar")

        """
        self._category = category
        self._strattr = strattr
        self._lockstring = lockstring
        self._autocreate = autocreate
        self._key = ""

    @property
    def _default(self):
        """
        Tries returning a new instance of default if callable.

        """
        if callable(self.__default):
            return self.__default()

        return self.__default

    @_default.setter
    def _default(self, value):
        self.__default = value

    def __set_name__(self, cls, name):
        """
        Called when descriptor is first assigned to the class. It is called with
        the name of the field.

        """
        self._key = name

        """
        Called when the attrkey is retrieved from the instance.

        """
        value = self._default
        try:
            value = self.at_get(
                getattr(instance, self.attrhandler_name).get(
                    key=self._key,
                    default=self._default,
                    category=self._category,
                    strattr=self._strattr,
                    raise_exception=self._autocreate,
                ),
                instance,
            )
            if self._autocreate:
                # attribute didn't exist and autocreate is set
                self.__set__(instance, self._default)
            else:
                raise

        """
        Called when assigning to the property (and when auto-creating an Attribute).

        """
        (
            getattr(instance, self.attrhandler_name).add(
                self._key,
                self.at_set(value, instance),
                category=self._category,
                lockstring=self._lockstring,
                strattr=self._strattr,
            )
        )

        """
        Called when running `del` on the property. Will remove/clear the Attribute. Note that
        the Attribute will be recreated next retrieval unless the AttributeProperty is also
        removed in code!

        """
        getattr(instance, self.attrhandler_name).remove(key=self._key, category=self._category)

    def at_set(self, value, obj):
        """
        The value to set is passed through the method. It can be used to customize/validate
        the input in a custom child class.

        Args:
            value (any): The value about to the stored in this Attribute.
            obj (object): Object the attribute is attached to

        Returns:
            any: The value to store.

        Raises:
            AttributeError: If the value is invalid to store.

        Notes:
            This is will only fire if you actually set the Attribute via this `AttributeProperty`.
            That is, if you instead set it via the `AttributeHandler` (or via `.db`), you are
            bypassing this `AttributeProperty` entirely and this method is never reached.

        """

        """
        The value returned from the Attribute is passed through this method. It can be used
        to react to the retrieval or modify the result in some way.

        Args:
            value (any): Value returned from the Attribute.

        Returns:
            any: The value to return to the caller.

        Notes:
            This is will only fire if you actually get the Attribute via this `AttributeProperty`.
            That is, if you instead get it via the `AttributeHandler` (or via `.db`), you are

        """

class NAttributeProperty(AttributeProperty):
    """
    NAttribute property descriptor. Allows for specifying NAttributes as Django-like 'fields'
    on the class level.

    Example:
    ::

            foo = NAttributeProperty(default="Bar")

    """

    attrhandler_name = "nattributes"

class Attribute(IAttribute, SharedMemoryModel):
    """
    This attribute is stored via Django. Most Attributes will be using this class.

    """

    #
    # Attribute Database Model setup
    #
    db_key = models.CharField("key", max_length=255, db_index=True)
    db_value = PickledObjectField(
        "value",
            "The data returned when the attribute is accessed. Must be "
            "written as a Python literal if editing through the admin "
            "interface. Attribute values which are not Python literals "
            "cannot be edited through the admin interface."
        ),
    )
    db_strvalue = models.TextField(
        "strvalue", null=True, blank=True, help_text="String-specific storage for quick look-up"
    )
    db_category = models.CharField(
        "category",
        max_length=128,
        help_text="Optional categorization of attribute.",
    )
    # Lock storage
        "locks", blank=True, help_text="Lockstrings for this object are stored here."
    )
    db_model = models.CharField(
        "model",
            "Which model of object this attribute is attached to (A "
            "natural key like 'objects.objectdb'). You should not change "
            "this value unless you know what you are doing."
        ),
    )
    # subclass of Attribute (None or nick)
    db_attrtype = models.CharField(
        "attrtype",
        max_length=16,
        help_text="Subclass of Attribute (None or nick)",
    )
    # time stamp
    db_date_created = models.DateTimeField("date_created", editable=False, auto_now_add=True)

    # objects = managers.AttributeManager()

        verbose_name = "Attribute"

    # lock_storage wrapper. Overloaded for saving to database.

        self.save(update_fields=["db_lock_storage"])

    @property
        """
        Getter. Allows for `value = self.value`.
        We cannot cache here since it makes certain cases (such
        as storing a dbobj which is then deleted elsewhere) out-of-sync.
        The overhead of unpickling seems hard to avoid.
        """

    def value(self, new_value):
        """
        Setter. Allows for self.value = value. We cannot cache here,
        see self.__value_get.
        """
        self.db_value = to_pickle(new_value)
        self.save(update_fields=["db_value"])

    @value.deleter
        """Deleter. Allows for del attr.value. This removes the entire attribute."""

#
# Handlers making use of the Attribute model
#

class IAttributeBackend:
    """
    Abstract interface for the backends used by the Attribute Handler.

    All Backends must implement this base class.
    """

    _attrcreate = "attrcreate"
    _attredit = "attredit"
    _attrread = "attrread"
    _attrclass = None

    def __init__(self, handler, attrtype):
        self.obj = handler.obj
        self._attrtype = attrtype
        self._objid = handler.obj.id
        # store category names fully cached
        self._catcache = {}
        # full cache was run on all attributes
        self._cache_complete = False

    def query_all(self):
        """
        Fetch all Attributes from this object.

        Returns:
            attrlist (list): A list of Attribute objects.
        """
        raise NotImplementedError()

    def query_key(self, key, category):
        """

        Args:
            key (str): The key of the Attribute being searched for.
            category (str or None): The category of the desired Attribute.

        Returns:
            attribute (IAttribute): A single Attribute.
        """

    def query_category(self, category):
        """
        Returns every matching Attribute as a list, given a category.

        This method calls up whatever storage the backend uses.

        Args:
            category (str or None): The category to query.

        Returns:
            attrs (list): The discovered Attributes.
        """

    def _full_cache(self):
        """Cache all attributes of this object"""
        if not _TYPECLASS_AGGRESSIVE_CACHE:
            return
        attrs = self.query_all()
            f"{to_str(attr.key).lower()}-{attr.category.lower() if attr.category else None}": attr
            for attr in attrs
        }
        self._cache_complete = True

    def _get_cache_key(self, key, category):
        """
        Fetch cache key.

        Args:

        Returns:
        """
        cachekey = "%s-%s" % (key, category)
        cachefound = False
        try:
            attr = _TYPECLASS_AGGRESSIVE_CACHE and self._cache[cachekey]
            cachefound = True
            attr = None

        if attr and (not hasattr(attr, "pk") and attr.pk is None):
            # clear out Attributes deleted from elsewhere. We must search this anew.
            attr = None
            cachefound = False
            del self._cache[cachekey]
        if cachefound and _TYPECLASS_AGGRESSIVE_CACHE:
            if attr:
                return [attr]  # return cached entity
            else:
                return []  # no such attribute: return an empty list
        else:
            conn = self.query_key(key, category)
            if conn:
                attr = conn[0].attribute
                if _TYPECLASS_AGGRESSIVE_CACHE:
                    self._cache[cachekey] = attr
                return [attr] if attr.pk else []
            else:
                # There is no such attribute. We will explicitly save that
                # in our cache to avoid firing another query if we try to
                # retrieve that (non-existent) attribute again.
                    self._cache[cachekey] = None
                return []

    def _get_cache_category(self, category):
        """
        Retrieves Attribute list (by category) from cache.

        Args:

        Returns:
        """
        catkey = "-%s" % category
        if _TYPECLASS_AGGRESSIVE_CACHE and catkey in self._catcache:
            return [attr for key, attr in self._cache.items() if key.endswith(catkey) and attr]
        else:
            # we have to query to make this category up-date in the cache
            attrs = self.query_category(category)
            if _TYPECLASS_AGGRESSIVE_CACHE:
                    if attr.pk:
                        cachekey = "%s-%s" % (attr.key, category)
                        self._cache[cachekey] = attr
                # mark category cache as up-to-date
                self._catcache[catkey] = True
            return attrs

    def _get_cache(self, key=None, category=None):
        """
        Retrieve from cache or database (always caches)

        Args:
            key (str, optional): Attribute key to query for
            category (str, optional): Attribiute category

        Returns:
            args (list): Returns a list of zero or more matches
                found from cache or database.
        Notes:
            When given a category only, a search for all objects
            of that cateogory is done and the category *name* is
            stored. This tells the system on subsequent calls that the
            list of cached attributes of this category is up-to-date
            and that the cache can be queried for category matches
            without missing any.
            The TYPECLASS_AGGRESSIVE_CACHE=False setting will turn off
            caching, causing each attribute access to trigger a
            database lookup.

        """
        key = key.strip().lower() if key else None
        category = category.strip().lower() if category is not None else None
        if key:
            return self._get_cache_key(key, category)
        return self._get_cache_category(category)

    def get(self, key=None, category=None):
        """
        Frontend for .get_cache. Retrieves Attribute(s).

        Args:

        Returns:
        """
        return self._get_cache(key, category)

    def _set_cache(self, key, category, attr_obj):
        """
        Update cache.

        Args:
            key (str): A cleaned key string
            category (str or None): A cleaned category name
            attr_obj (IAttribute): The newly saved attribute

        """
            return
        if not key:  # don't allow an empty key in cache
            return
        self._cache[cachekey] = attr_obj
        # mark that the category cache is no longer up-to-date
        self._catcache.pop(catkey, None)

    def _delete_cache(self, key, category):
        """
        Remove attribute from cache

        Args:

        """
        if key:
            cachekey = "%s-%s" % (key, category)
            self._cache.pop(cachekey, None)
        else:
            self._cache = {
                key: attrobj
                for key, attrobj in list(self._cache.items())
                if not key.endswith(catkey)
            }

    def reset_cache(self):
        """
        Reset cache from the outside.
        """

    def do_create_attribute(self, key, category, lockstring, value, strvalue):
        """
        Does the hard work of actually creating Attributes, whatever is needed.

        Args:
            key (str): The Attribute's key.
            category (str or None): The Attribute's category, or None
            lockstring (str): Any locks for the Attribute.
            value (obj): The Value of the Attribute.
            strvalue (bool): Signifies if this is a strvalue Attribute. Value MUST be a string or
                this will lead to Trouble. Ignored for InMemory attributes.

        Returns:
            attr (IAttribute): The new Attribute.
        """

    def create_attribute(self, key, category, lockstring, value, strvalue=False, cache=True):
        """
        Creates Attribute (using the class specified for the backend), (optionally) caches it, and
        returns it.

        This MUST actively save the Attribute to whatever database backend is used, AND
        call self.set_cache(key, category, new_attrobj)

        Args:
            cache (bool): Whether to cache the new Attribute

        Returns:
        """
        attr = self.do_create_attribute(key, category, lockstring, value, strvalue)
        if cache:
            self._set_cache(key, category, attr)
        return attr

    def do_update_attribute(self, attr, value, strvalue):
        """
        Simply sets a new Value to an Attribute.

        Args:
            attr (IAttribute): The Attribute being changed.
            value (obj): The Value for the Attribute.
            strvalue (bool): If True, `value` is expected to be a string.

        """

    def do_batch_update_attribute(self, attr_obj, category, lock_storage, new_value, strvalue):
        """
        Called opnly by batch add. For the database backend, this is a method
        of updating that can alter category and lock-storage.

        Args:
            attr_obj (IAttribute): The Attribute being altered.
            category (str or None): The attribute's (new) category.
            lock_storage (str): The attribute's new locks.
            new_value (obj): The Attribute's new value.
        """

    def do_batch_finish(self, attr_objs):
        """
        Called after batch_add completed. Used for handling database operations
        and/or caching complications.

        Args:
            attr_objs (list of IAttribute): The Attributes created/updated thus far.

        """

    def batch_add(self, *args, **kwargs):
        """
        Batch-version of `.add()`. This is more efficient than repeat-calling
        `.add` when having many Attributes to add.

        Args:
             *args (tuple): Tuples of varying length representing the
                Attribute to add to this object. Supported tuples are

                - (key, value)
                - (key, value, category)
                - (key, value, category, lockstring)
                - (key, value, category, lockstring, default_access)

        Raises:
            RuntimeError: If trying to pass a non-iterable as argument.

        Notes:
            The indata tuple order matters, so if you want a lockstring but no
            category, set the category to `None`. This method does not have the
            ability to check editing permissions and is mainly used internally.
            It does not use the normal `self.add` but applies the Attributes
            directly to the database.

        """
        new_attrobjs = []
        strattr = kwargs.get("strattr", False)
        for tup in args:
            if not is_iter(tup) or len(tup) < 2:
                raise RuntimeError("batch_add requires iterables as arguments (got %r)." % tup)
            ntup = len(tup)
            keystr = str(tup[0]).strip().lower()
            new_value = tup[1]
            category = str(tup[2]).strip().lower() if ntup > 2 and tup[2] is not None else None
            lockstring = tup[3] if ntup > 3 else ""

            attr_objs = self._get_cache(keystr, category)

            if attr_objs:
                attr_obj = attr_objs[0]
                # update an existing attribute object
                self.do_batch_update_attribute(attr_obj, category, lockstring, new_value, strattr)
            else:
                new_attr = self.do_create_attribute(
                    keystr, category, lockstring, new_value, strvalue=strattr
                )
                new_attrobjs.append(new_attr)
        if new_attrobjs:
            self.do_batch_finish(new_attrobjs)

    def do_delete_attribute(self, attr):
        """
        Does the hard work of actually deleting things.

        Args:
            attr (IAttribute): The attribute to delete.
        """

    def delete_attribute(self, attr):
        """
        Given an Attribute, deletes it. Also remove it from cache.

        Args:
        """
        if not attr:
            return
        self._delete_cache(attr.key, attr.category)
        self.do_delete_attribute(attr)

    def update_attribute(self, attr, value, strattr=False):
        """
        Simply updates an Attribute.

        Args:
            value (obj): The new value.
            strattr (bool): If set, the `value` is a raw string.
        """
        self.do_update_attribute(attr, value, strattr)

    def do_batch_delete(self, attribute_list):
        """
        Given a list of attributes, deletes them all.
        The default implementation is fine, but this is overridable since some databases may allow
        for a better method.

        Args:
            attribute_list (list of IAttribute):
        """
        for attribute in attribute_list:
            self.delete_attribute(attribute)

    def clear_attributes(self, category, accessing_obj, default_access):
        """
        Remove all Attributes on this object.

        Args:
            category (str, optional): If given, clear only Attributes
                of this category.
            accessing_obj (object, optional): If given, check the
                `attredit` lock on each Attribute before continuing.
            default_access (bool, optional): Use this permission as
                fallback if `access_obj` is given but there is no lock of
                type `attredit` on the Attribute in question.

        """

        if not self._cache_complete:
            self._full_cache()

        if category is not None:
            attrs = [attr for attr in self._cache.values() if attr.category == category]
        else:
            attrs = self._cache.values()

        if accessing_obj:
            self.do_batch_delete(
                [
                    attr
                    for attr in attrs
                    if attr.access(accessing_obj, self._attredit, default=default_access)
                ]
            )
        else:
            # have to cast the results to a list or we'll get a RuntimeError for removing from the
            # dict we're iterating
            self.do_batch_delete(list(attrs))
        self.reset_cache()

    def get_all_attributes(self):
        """
        Simply returns all Attributes of this object, sorted by their IDs.

        Returns:
            attributes (list of IAttribute)
        """
        if _TYPECLASS_AGGRESSIVE_CACHE:
            if not self._cache_complete:
                self._full_cache()
            return sorted([attr for attr in self._cache.values() if attr], key=lambda o: o.id)
        else:
            return sorted([attr for attr in self.query_all() if attr], key=lambda o: o.id)

class InMemoryAttributeBackend(IAttributeBackend):
    """
    This Backend for Attributes stores NOTHING in the database. Everything is kept in memory, and
    normally lost on a crash, reload, shared memory flush, etc. It generates IDs for the Attributes
    it manages, but these are of little importance beyond sorting and satisfying the caching logic
    to know an Attribute hasn't been deleted out from under the cache's nose.

    """

    _attrclass = InMemoryAttribute

        super().__init__(handler, attrtype)
        self._storage = dict()
        self._category_storage = defaultdict(list)
        self._id_counter = 0

    def _next_id(self):
        """
        Increments the internal ID counter and returns the new value.

        Returns:
            next_id (int): A simple integer.
        """
        self._id_counter += 1
        return self._id_counter

        return self._storage.values()

        found = self._storage.get((key, category), None)
        if found:
            return [found]
        return []

        if category is None:
            return self._storage.values()
        return self._category_storage.get(category, [])

        """
        See parent class.

        strvalue has no meaning for InMemory attributes.

        """
        new_attr = self._attrclass(
            pk=self._next_id(), key=key, category=category, lock_storage=lockstring, value=value
        )
        self._storage[(key, category)] = new_attr
        self._category_storage[category].append(new_attr)
        return new_attr

        attr.value = value

        """
        No need to bother saving anything. Just set some values.
        """
        attr_obj.db_category = category
        attr_obj.db_lock_storage = lock_storage if lock_storage else ""
        attr_obj.value = new_value

        """
        Nothing to do here for In-Memory.

        Args:
        """
        pass

        """
        Removes the Attribute from local storage. Once it's out of the cache, garbage collection
        will handle the rest.

        Args:
        """
        del self._storage[(attr.key, attr.category)]
        self._category_storage[attr.category].remove(attr)

class ModelAttributeBackend(IAttributeBackend):
    """
    Uses Django models for storing Attributes.
    """

    _attrclass = Attribute
    _m2m_fieldname = "db_attributes"

        self._model = to_str(handler.obj.__dbclass__.__name__.lower())

        query = {
            "%s__id" % self._model: self._objid,
            "attribute__db_model__iexact": self._model,
            "attribute__db_attrtype": self._attrtype,
        }
        return [
            conn.attribute
            for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)
        ]

        query = {
            "attribute__db_key__iexact": key.lower(),
            "attribute__db_category__iexact": category.lower() if category else None,
        }
        if not self.obj.pk:
            return []
        return getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)

        query = {
        }
        return [
        ]

        kwargs = {
            "db_key": key,
            "db_category": category,
            "db_model": self._model,
            "db_lock_storage": lockstring if lockstring else "",
            "db_attrtype": self._attrtype,
        }
        if strvalue:
            kwargs["db_value"] = None
            kwargs["db_strvalue"] = value
        else:
            kwargs["db_value"] = to_pickle(value)
            kwargs["db_strvalue"] = None
        new_attr = self._attrclass(**kwargs)
        new_attr.save()
        getattr(self.obj, self._m2m_fieldname).add(new_attr)
        self._set_cache(key, category, new_attr)

            attr.value = None
            attr.db_strvalue = value
        else:
            attr.value = value
            attr.db_strvalue = None
        attr.save(update_fields=["db_strvalue", "db_value"])

            # store as a simple string (will not notify OOB handlers)
            attr_obj.db_strvalue = new_value
            attr_obj.value = None
        else:
            # store normally (this will also notify OOB handlers)
            attr_obj.value = new_value
            attr_obj.db_strvalue = None
        attr_obj.save(update_fields=["db_strvalue", "db_value", "db_category", "db_lock_storage"])

        # Add new objects to m2m field all at once
        getattr(self.obj, self._m2m_fieldname).add(*attr_objs)

        try:
            attr.delete()
        except AssertionError:
            # This could happen if the Attribute has already been deleted.
            pass

class AttributeHandler:
    """
    Handler for adding Attributes to the object.
    """

    _attrtype = None

    def __init__(self, obj, backend_class):
        """
        Setup the AttributeHandler.

        Args:
            obj (TypedObject): An Account, Object, Channel, ServerSession (not technically a typed
                object), etc.  backend_class (IAttributeBackend class): The class of the backend to
                use.
        """
        self.backend = backend_class(self, self._attrtype)

    def has(self, key=None, category=None):
        """
        Checks if the given Attribute (or list of Attributes) exists on
        the object.

        Args:
            key (str or iterable): The Attribute key or keys to check for.
                If `None`, search by category.
            category (str or None): Limit the check to Attributes with this
                category (note, that `None` is the default category).

        Returns:
            has_attribute (bool or list): If the Attribute exists on
                this object or not. If `key` was given as an iterable then
                the return is a list of booleans.

        """
        ret = []
        for keystr in make_iter(key):
            keystr = key.strip().lower()
            ret.extend(bool(attr) for attr in self.backend.get(keystr, category))
        return ret[0] if len(ret) == 1 else ret

    def get(
        self,
        key=None,
        category=None,
        return_obj=False,
        strattr=False,
        raise_exception=False,
        accessing_obj=None,
        default_access=True,
        return_list=False,
    ):
        """
        Get the Attribute.

        Args:
            key (str or list, optional): the attribute identifier or
                multiple attributes to get. if a list of keys, the
                method will return a list.
            default (any, optional): The value to return if an
                Attribute was not defined. If set, it will be returned in
                a one-item list.
            category (str, optional): the category within which to
                retrieve attribute(s).
            return_obj (bool, optional): If set, the return is not the value of the
                Attribute but the Attribute object itself.
            strattr (bool, optional): Return the `strvalue` field of
                the Attribute rather than the usual `value`, this is a
                string-only value for quick database searches.
            raise_exception (bool, optional): When an Attribute is not
                found, the return from this is usually `default`. If this
                is set, an exception is raised instead.
            accessing_obj (object, optional): If set, an `attrread`
                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): 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
                categories. Each match will be the value of the found Attribute(s)
                unless `return_obj` is True, at which point it will be the
                attribute object itself or None. If `return_list` is True, this
                will always be a list, regardless of the number of elements.

        Raises:
            AttributeError: If `raise_exception` is set and no matching Attribute
                was found matching `key`.

        """

        ret = []
            # it's okay to send a None key
            attr_objs = self.backend.get(keystr, category)
                ret.extend(attr_objs)
            elif raise_exception:
                raise AttributeError
            elif return_obj:
                ret.append(None)

            # check 'attrread' locks
            ret = [
                attr
                for attr in ret
                if attr.access(accessing_obj, self._attrread, default=default_access)
            ]
        if strattr:
            ret = ret if return_obj else [attr.strvalue for attr in ret if attr]
        else:
            ret = ret if return_obj else [attr.value for attr in ret if attr]

        if return_list:
            return ret if ret else [default] if default is not None else []
        return ret[0] if ret and len(ret) == 1 else ret or default

    def add(
        self,
        key,
        value,
    ):
        """
        Add attribute to object, with optional `lockstring`.

        Args:
            key (str): An Attribute name to add.
            value (any or str): The value of the Attribute. If
                `strattr` keyword is set, this *must* be a string.
            category (str, optional): The category for the Attribute.
                The default `None` is the normal category used.
            lockstring (str, optional): A lock string limiting access
                to the attribute.
            strattr (bool, optional): Make this a string-only Attribute.
                This is only ever useful for optimization purposes.
            accessing_obj (object, optional): An entity to check for
                the `attrcreate` access-type. If not passing, this method
                will be exited.
            default_access (bool, optional): What access to grant if
                `accessing_obj` is given but no lock of the type
                `attrcreate` is defined on the Attribute in question.

        """
        if accessing_obj and not self.obj.access(
            accessing_obj, self._attrcreate, default=default_access
        ):
            # check create access
            return

        if not key:
            return

        keystr = key.strip().lower()
        attr_obj = self.backend.get(key, category)

        if attr_obj:
            # update an existing attribute object
            attr_obj = attr_obj[0]
            self.backend.update_attribute(attr_obj, value, strattr)
        else:
            # create a new Attribute (no OOB handlers can be notified)
            self.backend.create_attribute(keystr, category, lockstring, value, strattr)

        """
        Batch-version of `add()`. This is more efficient than
        repeat-calling add when having many Attributes to add.

        Args:
            *args (tuple): Each argument should be a tuples (can be of varying
                length) representing the Attribute to add to this object.
                Supported tuples are

            strattr (bool): If `True`, value must be a string. This
                will save the value without pickling which is less
                flexible but faster to search (not often used except
                internally).

        Raises:

        Notes:
            The indata tuple order matters, so if you want a lockstring
            but no category, set the category to `None`. This method
            does not have the ability to check editing permissions like
            normal .add does, and is mainly used internally. It does not
            use the normal self.add but apply the Attributes directly
            to the database.

        """
        self.backend.batch_add(*args, **kwargs)

    def remove(
        self,
        key=None,
    ):
        """
        Remove attribute or a list of attributes from object.

        Args:
            key (str or list, optional): An Attribute key to remove or a list of keys. If
                multiple keys, they must all be of the same `category`. If None and
                category is not given, remove all Attributes.
            category (str, optional): The category within which to
                remove the Attribute.
            raise_exception (bool, optional): If set, not finding the
                Attribute to delete will raise an exception instead of
                just quietly failing.
            accessing_obj (object, optional): An object to check
                against the `attredit` lock. If not given, the check will
                be skipped.
            default_access (bool, optional): The fallback access to
                grant if `accessing_obj` is given but there is no
                `attredit` lock set on the Attribute in question.

        Raises:

        Notes:
            If neither key nor category is given, this acts as clear().

        """

        if key is None:
            self.clear(
                category=category, accessing_obj=accessing_obj, default_access=default_access
            )
            return

            keystr = keystr.lower()

            for attr_obj in attr_objs:
                if not (
                    accessing_obj
                    and not attr_obj.access(accessing_obj, self._attredit, default=default_access)
                ):
                    self.backend.delete_attribute(attr_obj)
            if not attr_objs and raise_exception:

    def clear(self, category=None, accessing_obj=None, default_access=True):
        """

        Args:

        """
        self.backend.clear_attributes(category, accessing_obj, default_access)

    def all(self, category=None, accessing_obj=None, default_access=True):
        """
        Return all Attribute objects on this object, regardless of category.

        Args:
            category (str, optional): A given category to limit results to.
            accessing_obj (object, optional): Check the `attrread`
                lock on each attribute before returning them. If not
                given, this check is skipped.
            default_access (bool, optional): Use this permission as a
                fallback if `accessing_obj` is given but one or more
                Attributes has no lock of type `attrread` defined on them.

        Returns:
            Attributes (list): All the Attribute objects (note: Not
                their values!) in the handler.

        """
        attrs = self.backend.get_all_attributes()
            attrs = [attr for attr in attrs if attr.category == category]

            return [
                attr
                for attr in attrs
            ]
        else:

        self.backend.reset_cache()

# DbHolders for .db and .ndb properties on Typeclasses.

class DbHolder:
    "Holder for allowing property access of attributes"

    def __init__(self, obj, name, manager_name="attributes"):
        _SA(self, name, _GA(obj, manager_name))
        _SA(self, "name", name)

    def __getattribute__(self, attrname):
        if attrname == "all":
            # we allow to overload our default .all
            attr = _GA(self, _GA(self, "name")).get("all")
            return attr if attr else _GA(self, "all")
        return _GA(self, _GA(self, "name")).get(attrname)

    def __setattr__(self, attrname, value):
        _GA(self, _GA(self, "name")).add(attrname, value)

    def __delattr__(self, attrname):
        _GA(self, _GA(self, "name")).remove(attrname)

        return _GA(self, _GA(self, "name")).backend.get_all_attributes()

    all = property(get_all)

#
# Nick templating
#

"""
This supports the use of replacement templates in nicks:

This happens in two steps:

1) The user supplies a template that is converted to a regex according
   to the unix-like templating language.
2) This regex is tested against nicks depending on which nick replacement
   strategy is considered (most commonly inputline).
3) If there is a template match and there are templating markers,
   these are replaced with the arguments actually given.

@desc $1 $2 $3

This will be converted to the following regex:

    \@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)

Supported template markers (through fnmatch)
   *       matches anything (non-greedy)     -> .*?
   ?       matches any single character      ->
   [seq]   matches any entry in sequence
   [!seq]  matches entries not in sequence
Custom arg markers
   $N      argument position (1-99)

"""
_RE_OR = re.compile(r"(?<!\\)\|")
_RE_NICK_RE_ARG = re.compile(r"arg([1-9][0-9]?)")
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
_RE_NICK_RAW_ARG = re.compile(r"(\$)([1-9][0-9]?)")
_RE_NICK_SPACE = re.compile(r"\\ ")

class NickTemplateInvalid(ValueError):
    pass

def initialize_nick_templates(pattern, replacement, pattern_is_regex=False):
    """
    Initialize the nick templates for matching and remapping a string.

    Args:
        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, 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 from the pattern
    if pattern_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.

        # 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" for or_part in _RE_OR.split(pattern)
        )

    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) for match in _RE_NICK_RAW_ARG.finditer(pattern)]
        replacement_args = [match.group(1) for match in _RE_NICK_RAW_ARG.finditer(replacement)]
        if set(pattern_args) != set(replacement_args):
            # We don't have the same amount of argN/$N tags in input/output.
            raise NickTemplateInvalid("Nicks: Both in/out-templates must contain the same $N tags.")

        # 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(
            lambda m: "(?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"

    # map the replacement to match the arg1 group-names, to make replacement easy
    replacement_string = _RE_NICK_RAW_ARG.sub(lambda m: "{arg%s}" % m.group(2), replacement)

    return pattern_regex_string, replacement_string

def parse_nick_template(string, template_regex, outtemplate):
    """
    Parse a text using a template and map it to another template

    Args:
        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 template-regex. Un-found $N-markers (possible if
            the regex has optional matching groups) are replaced with empty
            strings.

    """
    match = template_regex.match(string)
    if match:
        matchdict = {
            key: value if value is not None else "" for key, value in match.groupdict().items()
        }
        return True, outtemplate.format_map(matchdict)
    return False, string

class NickHandler(AttributeHandler):
    """
    Handles the addition and removal of Nicks. Nicks are special
    versions of Attributes with an `_attrtype` hardcoded to `nick`.
    They also always use the `strvalue` fields for their data.

    """

    _attrtype = "nick"

        self._regex_cache = {}

    def has(self, key, category="inputline"):
        """
        Args:
            key (str or iterable): The Nick key or keys to check for.
            category (str): Limit the check to Nicks with this

        Returns:
            has_nick (bool or list): If the Nick exists on this object
                or not. If `key` was given as an iterable then the return
                is a list of booleans.

        """
        return super().has(key, category=category)

    def get(self, key=None, category="inputline", return_tuple=False, **kwargs):
        """
        Get the replacement value matching the given key and category

        Args:
                retrieve the nick. The "inputline" means replacing data
                sent by the user.
            return_tuple (bool, optional): return the full nick tuple rather
                than just the replacement. For non-template nicks this is just
                a string.
            kwargs (any, optional): These are passed on to `AttributeHandler.get`.

        Returns:
            str or tuple:  The nick replacement string or nick tuple.

        """
        if return_tuple or "return_obj" in kwargs:
            return super().get(key=key, category=category, **kwargs)
        else:
            retval = super().get(key=key, category=category, **kwargs)
            if retval:
                return (
                    retval[3]
                    if isinstance(retval, tuple)
                    else [tup[3] for tup in make_iter(retval)]
                )

    def add(self, pattern, replacement, category="inputline", pattern_is_regex=False, **kwargs):
        """
        Add a new nick, a mapping pattern -> replacement.

        Args:
            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
            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."`

        """
        nick_regex, nick_template = initialize_nick_templates(
            pattern, replacement, pattern_is_regex=pattern_is_regex
        )
        super().add(
            pattern, (nick_regex, nick_template, pattern, replacement), category=category, **kwargs
        )

    def remove(self, key, category="inputline", **kwargs):
        """
        Remove Nick with matching category.

        Args:
            key (str): A key for the nick to match for.
                removethe nick. The "inputline" means replacing data

        """
        super().remove(key, category=category, **kwargs)

    def nickreplace(self, raw_string, categories=("inputline", "channel"), include_account=True):
        """
        Apply nick replacement of entries in raw_string with nick replacement.

        Args:
            raw_string (str): The string in which to perform nick
                replacement.
            categories (tuple, optional): Replacement categories in
                which to perform the replacement, such as "inputline",
                "channel" etc.
            include_account (bool, optional): Also include replacement
                with nicks stored on the Account level.
            kwargs (any, optional): Not used.

        Returns:
            string (str): A string with matching keys replaced with
                their nick equivalents.

        """
        nicks = {}
        for category in make_iter(categories):
            nicks.update(
                {
                    nick.key: nick
                    for nick in make_iter(self.get(category=category, return_obj=True))
                    if nick and nick.key
                }
            )
        if include_account and self.obj.has_account:
            for category in make_iter(categories):
                nicks.update(
                    {
                        nick.key: nick
                        for nick in make_iter(
                            self.obj.account.nicks.get(category=category, return_obj=True)
                        )
                        if nick and nick.key
                    }
                )
        for key, nick in nicks.items():
            nick_regex, template, _, _ = nick.value
            regex = self._regex_cache.get(nick_regex)
            if not regex:
                try:
                    regex = re.compile(nick_regex, re.I + re.DOTALL + re.U)
                except re.error:
                    from evennia.utils import logger

                    logger.log_trace("Probably nick being created with unvalidated regex mapping.")
                    continue
                self._regex_cache[nick_regex] = regex

            is_match, raw_string = parse_nick_template(raw_string, regex, template)
            if is_match:
                break
        return raw_string

"""
This implements the common managers that are used by the
abstract models in dbobjects.py (and which are thus shared by
all Attributes and TypedObjects).

"""

from django.db.models import Count, ExpressionWrapper, F, FloatField, Q
from django.db.models.functions import Cast

from evennia.typeclasses.tags import Tag
from evennia.utils import idmapper
from evennia.utils.utils import class_from_module, make_iter, variable_from_module

__all__ = ("TypedObjectManager",)
_Tag = None

# Managers

class TypedObjectManager(idmapper.manager.SharedMemoryManager):
    """
    Common ObjectManager for all dbobjects.

    """

    # common methods for all typed managers. These are used
    # in other methods. Returns querysets.

    # Attribute manager methods
    def get_attribute(
        self, key=None, category=None, value=None, strvalue=None, obj=None, attrtype=None, **kwargs
    ):
        """
        Return Attribute objects by key, by category, by value, by strvalue, by
        object (it is stored on) or with a combination of those criteria.

        Args:
            key (str, optional): The attribute's key to search for
            category (str, optional): The category of the attribute(s) to search for.
            value (str, optional): The attribute value to search for.
                Note that this is not a very efficient operation since it
                will query for a pickled entity. Mutually exclusive to
                `strvalue`.
            strvalue (str, optional): The str-value to search for.
                Most Attributes will not have strvalue set. This is
                mutually exclusive to the `value` keyword and will take
                precedence if given.
            obj (Object, optional): On which object the Attribute to
                search for is.
            attrype (str, optional): An attribute-type to search for.
                By default this is either `None` (normal Attributes) or
                `"nick"`.
            **kwargs (any): Currently unused. Reserved for future use.

        Returns:
            list: The matching Attributes.

        """
        dbmodel = self.model.__dbclass__.__name__.lower()
        query = [("attribute__db_attrtype", attrtype), ("attribute__db_model", dbmodel)]
        if obj:
            query.append(("%s__id" % self.model.__dbclass__.__name__.lower(), obj.id))
        if key:
            query.append(("attribute__db_key", key))
            query.append(("attribute__db_category", category))
            query.append(("attribute__db_strvalue", strvalue))
        if value:
            # no reason to make strvalue/value mutually exclusive at this level
            query.append(("attribute__db_value", value))
        return Attribute.objects.filter(
            pk__in=self.model.db_attributes.through.objects.filter(**dict(query)).values_list(
                "attribute_id", flat=True
            )
        )

    def get_nick(self, key=None, category=None, value=None, strvalue=None, obj=None):
        """
        Get a nick, in parallel to `get_attribute`.

        Args:
            key (str, optional): The nicks's key to search for
            category (str, optional): The category of the nicks(s) to search for.
            value (str, optional): The attribute value to search for. Note that this
                is not a very efficient operation since it will query for a pickled
                entity. Mutually exclusive to `strvalue`.
            strvalue (str, optional): The str-value to search for. Most Attributes
                will not have strvalue set. This is mutually exclusive to the `value`
                keyword and will take precedence if given.
            obj (Object, optional): On which object the Attribute to search for is.

        Returns:
            nicks (list): The matching Nicks.

        """
        return self.get_attribute(
            key=key, category=category, value=value, strvalue=strvalue, obj=obj
        )

    def get_by_attribute(
        self, key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs
    ):
        """
        Return objects having attributes with the given key, category,
        value, strvalue or combination of those criteria.

        Args:
            category (str, optional): The category of the attribute
                to search for.
                `"nick"`.
            kwargs (any): Currently unused. Reserved for future use.

        Returns:
            obj (list): Objects having the matching Attributes.

        """
        query = [
            ("db_attributes__db_attrtype", attrtype),
            ("db_attributes__db_model", dbmodel),
        ]
        if key:
            query.append(("db_attributes__db_key", key))
            query.append(("db_attributes__db_category", category))
            query.append(("db_attributes__db_strvalue", strvalue))
            # strvalue and value are mutually exclusive
            query.append(("db_attributes__db_value", value))
        return self.filter(**dict(query))

    def get_by_nick(self, key=None, nick=None, category="inputline"):
        """
        Get object based on its key or nick.

        Args:
            nick (str, optional): The nickname to search for
            category (str, optional): The category of the nick

        Returns:
            obj (list): Objects having the matching Nicks.

        """
        return self.get_by_attribute(key=key, category=category, strvalue=nick, attrtype="nick")

    # Tag manager methods

    def get_tag(self, key=None, category=None, obj=None, tagtype=None, global_search=False):
        """
        Return Tag objects by key, by category, by object (it is
        stored on) or with a combination of those criteria.

        Args:
            key (str, optional): The Tag's key to search for
            category (str, optional): The Tag of the attribute(s)
            obj (Object, optional): On which object the Tag to
            tagtype (str, optional): One of `None` (normal tags),
                "alias" or "permission"
            global_search (bool, optional): Include all possible tags,
                not just tags on this object

        Returns:
            tag (list): The matching Tags.

        """
        global _Tag
        if not _Tag:
            from evennia.typeclasses.models import Tag as _Tag
        if global_search:
            # search all tags using the Tag model
            query = [("db_tagtype", tagtype), ("db_model", dbmodel)]
            if obj:
                query.append(("id", obj.id))
            if key:
                query.append(("db_key", key))
            if category:
                query.append(("db_category", category))
            else:
                query.append(("db_category", None))
            return _Tag.objects.filter(**dict(query))
        else:
            # search only among tags stored on on this model
            query = [("tag__db_tagtype", tagtype), ("tag__db_model", dbmodel)]
            if obj:
                query.append(("%s__id" % self.model.__name__.lower(), obj.id))
            if key:
                query.append(("tag__db_key", key))
                query.append(("tag__db_category", category))
            return Tag.objects.filter(
                pk__in=self.model.db_tags.through.objects.filter(**dict(query)).values_list(
                    "tag_id", flat=True
                )
            )

    def get_permission(self, key=None, category=None, obj=None):
        """
        Get a permission from the database.

        Args:
            key (str, optional): The permission's identifier.
            category (str, optional): The permission's category.
            obj (object, optional): The object on which this Tag is set.

        Returns:
            permission (list): Permission objects.

        """
        return self.get_tag(key=key, category=category, obj=obj, tagtype="permission")

    def get_alias(self, key=None, category=None, obj=None):
        """
        Get an alias from the database.

        Args:

        Returns:
            alias (list): Alias objects.

        """
        return self.get_tag(key=key, category=category, obj=obj, tagtype="alias")

    def get_by_tag(self, key=None, category=None, tagtype=None, **kwargs):
        """
        Return objects having tags with a given key or category or combination of the two.
        Also accepts multiple tags/category/tagtype

        Args:
            key (str or list, optional): Tag key or list of keys. Not case sensitive.
            category (str or list, optional): Tag category. Not case sensitive.
                If `key` is a list, a single category can either apply to all
                keys in that list or this must be a list matching the `key`
                list element by element. If no `key` is given, all objects with
                tags of this category are returned.
            tagtype (str, optional): 'type' of Tag, by default
                this is either `None` (a normal Tag), `alias` or
                `permission`. This always apply to all queried tags.

            match (str): "all" (default) or "any"; determines whether the
                target object must be tagged with ALL of the provided
                tags/categories or ANY single one. ANY will perform a weighted
                sort, so objects with more tag matches will outrank those with
                fewer tag matches.

        Returns:
            objects (list): Objects with matching tag.

        Raises:
            IndexError: If `key` and `category` are both lists and `category` is shorter
                than `key`.

        """
        if not (key or category):
            return []

        anymatch = "any" == kwargs.get("match", "all").lower().strip()

        keys = make_iter(key) if key else []
        categories = make_iter(category) if category else []
        n_keys = len(keys)
        n_categories = len(categories)
        unique_categories = set(categories)
        n_unique_categories = len(unique_categories)

        query = (
            self.filter(db_tags__db_tagtype__iexact=tagtype, db_tags__db_model__iexact=dbmodel)
            .distinct()
        )

        if n_keys > 0:
            # keys and/or categories given
            if n_categories == 0:
                categories = [None for _ in range(n_keys)]
            elif n_categories == 1 and n_keys > 1:
                cat = categories[0]
                categories = [cat for _ in range(n_keys)]
            elif 1 < n_categories < n_keys:
                raise IndexError(
                    "get_by_tag needs a single category or a list of categories "
                    "the same length as the list of tags."
                )
            clauses = Q()
            for ikey, key in enumerate(keys):
                # ANY mode; must match any one of the given tags/categories
                clauses |= Q(db_key__iexact=key, db_category__iexact=categories[ikey])
        else:
            # only one or more categories given
            # ANY mode; must match any one of them
            for category in unique_categories:
                clauses |= Q(db_category__iexact=category)

        tags = _Tag.objects.filter(clauses)
        query = query.filter(db_tags__in=tags).annotate(
            matches=Count("db_tags__pk", filter=Q(db_tags__in=tags), distinct=True)
        )

        if anymatch:
            # ANY: Match any single tag, ordered by weight
            query = query.order_by("-matches")
        else:
            # Default ALL: Match all of the tags and optionally more
            n_req_tags = n_keys if n_keys > 0 else n_unique_categories
            query = query.filter(matches__gte=n_req_tags)

        return query

    def get_by_permission(self, key=None, category=None):
        """
        Return objects having permissions with a given key or category or
        combination of the two.

        Args:
            key (str, optional): Permissions key. Not case sensitive.
            category (str, optional): Permission category. Not case sensitive.
        Returns:
            objects (list): Objects with matching permission.
        """
        return self.get_by_tag(key=key, category=category, tagtype="permission")

    def get_by_alias(self, key=None, category=None):
        """
        Return objects having aliases with a given key or category or

        Args:
            key (str, optional): Alias key. Not case sensitive.
            category (str, optional): Alias category. Not case sensitive.
        Returns:
            objects (list): Objects with matching alias.
        """
        return self.get_by_tag(key=key, category=category, tagtype="alias")

    def create_tag(self, key=None, category=None, data=None, tagtype=None):
        """
        Create a new Tag of the base type associated with this
        object.  This makes sure to create case-insensitive tags.
        If the exact same tag configuration (key+category+tagtype+dbmodel)
        exists on the model, a new tag will not be created, but an old
        one returned.

        Args:
            key (str, optional): Tag key. Not case sensitive.
            category (str, optional): Tag category. Not case sensitive.
            data (str, optional): Extra information about the tag.
            tagtype (str or None, optional): 'type' of Tag, by default
                `permission`.
        Notes:
            The `data` field is not part of the uniqueness of the tag:
            Setting `data` on an existing tag will overwrite the old
            data field. It is intended only as a way to carry
            information about the tag (like a help text), not to carry
            any information about the tagged objects themselves.

        """
        data = str(data) if data is not None else None
        # try to get old tag

        tag = self.get_tag(key=key, category=category, tagtype=tagtype, global_search=True)
        if tag and data is not None:
            # get tag from list returned by get_tag
            tag = tag[0]
            # overload data on tag
            tag.db_data = data
            tag.save()
        elif not tag:
            # create a new tag
            global _Tag
            if not _Tag:
                from evennia.typeclasses.models import Tag as _Tag
            tag = _Tag.objects.create(
                db_key=key.strip().lower() if key is not None else None,
                db_category=category.strip().lower() if category and key is not None else None,
                db_data=data,
                db_model=dbmodel,
                db_tagtype=tagtype.strip().lower() if tagtype is not None else None,
            )
        return make_iter(tag)[0]

    def dbref(self, dbref, reqhash=True):
        """
        Determing if input is a valid dbref.

        Args:
            dbref (str or int): A possible dbref.
            reqhash (bool, optional): If the "#" is required for this
                to be considered a valid hash.

        Returns:
            dbref (int or None): The integer part of the dbref.

        Notes:
            Valid forms of dbref (database reference number) are
            either a string '#N' or an integer N.

        """
        if reqhash and not (isinstance(dbref, str) and dbref.startswith("#")):
        if isinstance(dbref, str):
            dbref = dbref.lstrip("#")
        try:
            if int(dbref) < 0:
        return dbref

    def get_id(self, dbref):
        """
        Find object with given dbref.

        Args:
            dbref (str or int): The id to search for.

        Returns:
            object (TypedObject): The matched object.

        """
        dbref = self.dbref(dbref, reqhash=False)
        try:
            return self.get(id=dbref)
            pass

    def dbref_search(self, dbref):
        """
        Alias to get_id.

        Args:

        Returns:
            Queryset: Queryset with 0 or 1 match.

        """
        if dbref:
        return self.none()

    search_dbref = dbref_search  # alias

    def get_dbref_range(self, min_dbref=None, max_dbref=None):
        """
        Get objects within a certain range of dbrefs.

        Args:
            min_dbref (int): Start of dbref range.
            max_dbref (int): End of dbref range (inclusive)

        Returns:
            objects (list): TypedObjects with dbrefs within
                the given dbref ranges.

        """
        retval = super().all()
        if min_dbref is not None:
            retval = retval.filter(id__gte=self.dbref(min_dbref, reqhash=False))
        if max_dbref is not None:
            retval = retval.filter(id__lte=self.dbref(max_dbref, reqhash=False))

    def get_typeclass_totals(self, *args, **kwargs) -> object:
        """
        Returns a queryset of typeclass composition statistics.

        Returns:
            qs (Queryset): A queryset of dicts containing the typeclass (name),
                the count of objects with that typeclass and a float representing
                the percentage of objects associated with the typeclass.

        """
        return (
            self.values("db_typeclass_path")
            .annotate(
                # Get count of how many objects for each typeclass exist
                count=Count("db_typeclass_path")
            )
                # Rename db_typeclass_path field to something more human
                typeclass=F("db_typeclass_path"),
                # Calculate this class' percentage of total composition
                percent=ExpressionWrapper(
                    ((F("count") / float(self.count())) * 100.0),
                    output_field=FloatField(),
                ),
            )
            .values("typeclass", "count", "percent")
        )

    def object_totals(self):
        """
        Get info about database statistics.

        Returns:
            census (dict): A dictionary `{typeclass_path: number, ...}` with
                all the typeclasses active in-game as well as the number
                of such objects defined (i.e. the number of database
                object having that typeclass set on themselves).

        """
        stats = self.get_typeclass_totals().order_by("typeclass")
        return {x.get("typeclass"): x.get("count") for x in stats}

    def typeclass_search(self, typeclass, include_children=False, include_parents=False):
        """
        Searches through all objects returning those which are of the
        specified typeclass.

        Args:
            typeclass (str or class): A typeclass class or a python path to a typeclass.
            include_children (bool, optional): Return objects with
                given typeclass *and* all children inheriting from this
                typeclass. Mutually exclusive to `include_parents`.
            include_parents (bool, optional): Return objects with
                given typeclass *and* all parents to this typeclass.
                Mutually exclusive to `include_children`.

        Returns:
            objects (list): The objects found with the given typeclasses.

        Raises:
            ImportError: If the provided `typeclass` is not a valid typeclass or the
                path to an existing typeclass.

        """
        if not callable(typeclass):

        if include_children:
            query = typeclass.objects.all_family()
        else:
            query = typeclass.objects.all()

        if include_parents:
            parents = typeclass.__mro__
            if parents:
                parent_queries = []
                for parent in (parent for parent in parents if hasattr(parent, "path")):
                    parent_queries.append(super().filter(db_typeclass_path__exact=parent.path))
                query = query.union(*parent_queries)

class TypeclassManager(TypedObjectManager):
    """
    Manager for the typeclasses. The main purpose of this manager is
    to limit database queries to the given typeclass despite all
    typeclasses technically being defined in the same core database
    model.

    """

    # object-manager methods
    def smart_search(self, query):
        """
        Search by supplying a string with optional extra search criteria to aid the query.

        Args:
            query (str): A search criteria that accepts extra search criteria on the following
            forms:

                [key|alias|#dbref...]
                [tag==<tagstr>[:category]...]
                [attr==<key>:<value>:category...]

            All three can be combined in the same query, separated by spaces.

        Returns:
            matches (queryset): A queryset result matching all queries exactly. If wanting to use
                spaces or ==, != in tags or attributes, enclose them in quotes.

        Example:
            house = smart_search("key=foo alias=bar tag=house:building tag=magic attr=color:red")

        Note:
            The flexibility of this method is limited by the input line format. Tag/attribute
            matching only works for matching primitives.  For even more complex queries, such as
            'in' operations or object field matching, use the full django query language.

        """
        # shlex splits by spaces unless escaped by quotes
        querysplit = shlex.split(query)
        queries, plustags, plusattrs, negtags, negattrs = [], [], [], [], []
        for ipart, part in enumerate(querysplit):
            key, rest = part, ""
            if ":" in part:
                key, rest = part.split(":", 1)
            # tags are on the form tag or tag:category
            if key.startswith("tag=="):
                plustags.append((key[5:], rest))
                continue
            elif key.startswith("tag!="):
                negtags.append((key[5:], rest))
                continue
            # attrs are on the form attr:value or attr:value:category
            elif rest:
                value, category = rest, ""
                if ":" in rest:
                    value, category = rest.split(":", 1)
                if key.startswith("attr=="):
                    plusattrs.append((key[7:], value, category))
                    continue
                elif key.startswith("attr!="):
                    negattrs.append((key[7:], value, category))
                    continue
            # if we get here, we are entering a key search criterion which
            # we assume is one word.
            queries.append(part)
        # build query from components
        query = " ".join(queries)
        # TODO

    def get(self, *args, **kwargs):
        """
        Overload the standard get. This will limit itself to only
        return the current typeclass.

        Args:
            args (any): These are passed on as arguments to the default
                django get method.
            kwargs (any): These are passed on as normal arguments
                to the default django get method
        Returns:
            object (object): The object found.

        Raises:
            ObjectNotFound: The exact name of this exception depends
                on the model base used.

        """
        kwargs.update({"db_typeclass_path": self.model.path})
        return super().get(**kwargs)

    def filter(self, *args, **kwargs):
        """
        Overload of the standard filter function. This filter will
        limit itself to only the current typeclass.

        Args:
                django filter method.
                to the default django filter method.
        Returns:
            objects (queryset): The objects found.

        """
        return super().filter(*args, **kwargs)

        """
        Overload method to return all matches, filtering for typeclass.

        Returns:

        """
        return super().all().filter(db_typeclass_path=self.model.path)

    def first(self):
        """
        Overload method to return first match, filtering for typeclass.

        Returns:

        Raises:

        """
        return super().filter(db_typeclass_path=self.model.path).first()

    def last(self):
        """
        Overload method to return last match, filtering for typeclass.

        Returns:

        Raises:

        """
        return super().filter(db_typeclass_path=self.model.path).last()

        """
        Overload method to return number of matches, filtering for typeclass.

        Returns:
            integer : Number of objects found.

        """
        return super().filter(db_typeclass_path=self.model.path).count()

    def annotate(self, *args, **kwargs):
        """
        Overload annotate method to filter on typeclass before annotating.
        Args:
            *args (any): Positional arguments passed along to queryset annotate method.
            **kwargs (any): Keyword arguments passed along to queryset annotate method.

        Returns:
            Annotated queryset.
        """
        return super().filter(db_typeclass_path=self.model.path).annotate(*args, **kwargs)

    def values(self, *args, **kwargs):
        """
        Overload values method to filter on typeclass first.
        Args:
            *args (any): Positional arguments passed along to values method.
            **kwargs (any): Keyword arguments passed along to values method.

        Returns:
            Queryset of values dictionaries, just filtered by typeclass first.
        """
        return super().filter(db_typeclass_path=self.model.path).values(*args, **kwargs)

    def values_list(self, *args, **kwargs):
        """
        Args:
            *args (any): Positional arguments passed along to values_list method.
            **kwargs (any): Keyword arguments passed along to values_list method.

        Returns:
            Queryset of value_list tuples, just filtered by typeclass first.
        """
        return super().filter(db_typeclass_path=self.model.path).values_list(*args, **kwargs)

    def _get_subclasses(self, cls):
        """
        Recursively get all subclasses to a class.

        Args:
            cls (classoject): A class to get subclasses from.
        """
        all_subclasses = cls.__subclasses__()
        for subclass in all_subclasses:
            all_subclasses.extend(self._get_subclasses(subclass))
        return all_subclasses

    def get_family(self, *args, **kwargs):
        """
        Variation of get that not only returns the current typeclass
        but also all subclasses of that typeclass.

                to the default django get method.
        Returns:
            objects (list): The objects found.

        Raises:

        """
        ]
        kwargs.update({"db_typeclass_path__in": paths})
        return super().get(*args, **kwargs)

    def filter_family(self, *args, **kwargs):
        """
        Variation of filter that allows results both from typeclass
        and from subclasses of typeclass

        Args:
        Returns:

        """
        # query, including all subclasses
        ]

    def all_family(self):
        """
        Return all matches, allowing matches from all subclasses of
        the typeclass.

        Returns:

        """
        ]
        return super().all().filter(db_typeclass_path__in=paths)

"""
This is the *abstract* django models for many of the database objects
in Evennia. A django abstract (obs, not the same as a Python metaclass!) is
a model which is not actually created in the database, but which only exists
for other models to inherit from, to avoid code duplication. Any model can
import and inherit from these classes.

Attributes are database objects stored on other objects. The implementing
class needs to supply a ForeignKey field attr_object pointing to the kind
of object being mapped. Attributes storing iterables actually store special
types of iterables named PackedList/PackedDict respectively. These make
sure to save changes to them to database - this is criticial in order to
allow for obj.db.mylist[2] = data. Also, all dbobjects are saved as
dbrefs but are also aggressively cached.

TypedObjects are objects 'decorated' with a typeclass - that is, the typeclass
(which is a normal Python class implementing some special tricks with its
get/set attribute methods, allows for the creation of all sorts of different
objects all with the same database object underneath. Usually attributes are
used to permanently store things not hard-coded as field on the database object.
The admin should usually not have to deal directly  with the database object
layer.

This module also contains the Managers for the respective models; inherit from
these to create custom managers.

"""
from django.db.models import signals
from django.db.models.base import ModelBase

from evennia.server.signals import SIGNAL_TYPED_OBJECT_POST_RENAME
from evennia.typeclasses import managers
    Attribute,
    AttributeProperty,
    DbHolder,
    ModelAttributeBackend,
)
from evennia.typeclasses.tags import (
    AliasHandler,
    PermissionHandler,
    Tag,
    TagCategoryProperty,
    TagHandler,
    TagProperty,
)
from evennia.utils.idmapper.models import SharedMemoryModel, SharedMemoryModelBase
from evennia.utils.utils import class_from_module, inherits_from, is_iter, lazy_property

__all__ = ("TypedObject",)

_PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]

# signal receivers. Connected in __new__

def call_at_first_save(sender, instance, created, **kwargs):
    """
    Receives a signal just after the object is saved.

    """
    if created:
        instance.at_first_save()

def remove_attributes_on_delete(sender, instance, **kwargs):
    """
    Wipe object's Attributes when it's deleted

    """
    instance.db_attributes.all().delete()

#
# Typed Objects
#

#
# Meta class for typeclasses
#

class TypeclassBase(SharedMemoryModelBase):
    """
    Metaclass which should be set for the root of model proxies
    that don't define any new fields, like Object, Script etc. This
    is the basis for the typeclassing system.

    """

    def __new__(cls, name, bases, attrs):
        """
        We must define our Typeclasses as proxies. We also store the
        path directly on the class, this is required by managers.
        """

        # storage of stats
        attrs["typename"] = name
        attrs["path"] = "%s.%s" % (attrs["__module__"], name)

        def _get_dbmodel(bases):
            """Recursively get the dbmodel"""
            if not hasattr(bases, "__iter__"):
                bases = [bases]
            for base in bases:
                try:
                    if base._meta.proxy or base._meta.abstract:
                        for kls in base._meta.parents:
                            return _get_dbmodel(kls)
                    # this happens if trying to parse a non-typeclass mixin parent,
                    # without a _meta
                    continue
                else:
                    return base

        dbmodel = _get_dbmodel(bases)

        if not dbmodel:
            raise TypeError(f"{name} does not appear to inherit from a database model.")

        # typeclass proxy setup
        # first check explicit __applabel__ on the typeclass, then figure
        # it out from the dbmodel
        if "__applabel__" not in attrs:
            # find the app-label in one of the bases, usually the dbmodel
            attrs["__applabel__"] = dbmodel._meta.app_label

        if "Meta" not in attrs:

            class Meta:
                proxy = True
                app_label = attrs.get("__applabel__", "typeclasses")

            attrs["Meta"] = Meta
        attrs["Meta"].proxy = True

        new_class = ModelBase.__new__(cls, name, bases, attrs)

        # django doesn't support inheriting proxy models so we hack support for
        # it here by injecting `proxy_for_model` to the actual dbmodel.
        # Unfortunately we cannot also set the correct model_name, because this
        # would block multiple-inheritance of typeclasses (Django doesn't allow
        # multiple bases of the same model).
        if dbmodel:
            new_class._meta.proxy_for_model = dbmodel
            # Maybe Django will eventually handle this in the future:
            # new_class._meta.model_name = dbmodel._meta.model_name

        # attach signals
        signals.post_save.connect(call_at_first_save, sender=new_class)
        signals.pre_delete.connect(remove_attributes_on_delete, sender=new_class)
        return new_class

#
# Main TypedObject abstraction
#

class TypedObject(SharedMemoryModel):
    """
    Abstract Django model.

    This is the basis for a typed object. It also contains all the
    mechanics for managing connected attributes.

    The TypedObject has the following properties:

    - key - main name
    - name - alias for key
    - typeclass_path - the path to the decorating typeclass
    - typeclass - auto-linked typeclass
    - date_created - time stamp of object creation
    - permissions - perm strings
    - dbref - #id of object
    - db - persistent attribute storage
    - ndb - non-persistent attribute storage

    """

    #
    # TypedObject Database Model setup
    #
    #
    # These databse fields are all accessed and set using their corresponding
    # properties, named same as the field, but without the db_* prefix
    # (no separate save() call is needed)

    # Main identifier of the object, for searching. Is accessed with self.key
    # or self.name
    # This is the python path to the type class this object is tied to. The
    # typeclass is what defines what kind of Object this is)
    db_typeclass_path = models.CharField(
        "typeclass",
            "this defines what 'type' of entity this is. This variable holds "
            "a Python path to a module with a valid Evennia Typeclass."
        ),
    )
    db_date_created = models.DateTimeField("creation date", editable=False, auto_now_add=True)
        "locks",
            "locks limit access to an entity. A lock is defined as a 'lock string' "
            "on the form 'type:lockfunctions', defining what functionality is locked and "
            "how to determine access. Not defining a lock means no access is granted."
        ),
    )
    # many2many relationships
    db_attributes = models.ManyToManyField(
        Attribute,
            "attributes on this object. An attribute can hold any pickle-able "
            "python object (see docs for special cases)."
        ),
    )
        Tag,
            "tags on this object. Tags are simple string markers to identify, "
            "group and alias objects."
        ),
    )

    objects = managers.TypedObjectManager()

    # quick on-object typeclass cache for speed
    _cached_typeclass = None

    # typeclass mechanism

    def set_class_from_typeclass(self, typeclass_path=None):
            try:
                self.__class__ = class_from_module(
                    typeclass_path, defaultpaths=settings.TYPECLASS_PATHS
                )
                try:
                    self.__class__ = class_from_module(self.__settingsclasspath__)
                    try:
                        self.__class__ = class_from_module(self.__defaultclasspath__)
                        self.__class__ = self._meta.concrete_model or self.__class__
            finally:
                self.db_typeclass_path = typeclass_path
        elif self.db_typeclass_path:
            try:
                self.__class__ = class_from_module(self.db_typeclass_path)
                try:
                    self.__class__ = class_from_module(self.__defaultclasspath__)
                    self.__dbclass__ = self._meta.concrete_model or self.__class__
        else:
            self.db_typeclass_path = "%s.%s" % (self.__module__, self.__class__.__name__)
        # important to put this at the end since _meta is based on the set __class__
        try:
            self.__dbclass__ = self._meta.concrete_model or self.__class__
            err_class = repr(self.__class__)
            self.__class__ = class_from_module("evennia.objects.objects.DefaultObject")
            self.__dbclass__ = class_from_module("evennia.objects.models.ObjectDB")
            self.db_typeclass_path = "evennia.objects.objects.DefaultObject"
                "Critical: Class %s of %s is not a valid typeclass!\nTemporarily falling back"
                " to %s." % (err_class, self, self.__class__)
            )

        """
        The `__init__` method of typeclasses is the core operational
        code of the typeclass system, where it dynamically re-applies
        a class based on the db_typeclass_path database field rather
        than use the one in the model.

        Args:
            Passed through to parent.

        Notes:
            The loading mechanism will attempt the following steps:

            1. Attempt to load typeclass given on command line
            2. Attempt to load typeclass stored in db_typeclass_path
            3. Attempt to load `__settingsclasspath__`, which is by the
               default classes defined to be the respective user-set
               base typeclass settings, like `BASE_OBJECT_TYPECLASS`.
            4. Attempt to load `__defaultclasspath__`, which is the
               base classes in the library, like DefaultObject etc.
            5. If everything else fails, use the database model.

            Normal operation is to load successfully at either step 1
            or 2 depending on how the class was called. Tracebacks
            will be logged for every step the loader must take beyond
            2.

        """
        typeclass_path = kwargs.pop("typeclass", None)
        self.set_class_from_typeclass(typeclass_path=typeclass_path)

    def init_evennia_properties(self):
        """
        Called by creation methods; makes sure to initialize Attribute/TagProperties
        by fetching them once.
        """
        for propkey, prop in self.__class__.__dict__.items():
            if isinstance(prop, (AttributeProperty, TagProperty, TagCategoryProperty)):
                try:
                    getattr(self, propkey)

    # initialize all handlers in a lazy fashion
        return AttributeHandler(self, ModelAttributeBackend)

    def permissions(self):
        return PermissionHandler(self)

        """
        Django setup info.
        """

        abstract = True
        verbose_name = "Evennia Database Object"
        ordering = ["-db_date_created", "id", "db_typeclass_path", "db_key"]

    # wrapper

    # name property (alias to self.key)
    def __name_get(self):

    def __name_set(self, value):
        self.key = value

    def __name_del(self):
        raise Exception("Cannot delete name")

    name = property(__name_get, __name_set, __name_del)

    # key property (overrides's the idmapper's db_key for the at_rename hook)
    @property
    def key(self):

    @key.setter
    def key(self, value):
        oldname = str(self.db_key)
        self.save(update_fields=["db_key"])
        self.at_rename(oldname, value)
        SIGNAL_TYPED_OBJECT_POST_RENAME.send(sender=self, old_key=oldname, new_key=value)

    #
    #
    # TypedObject main class methods and properties
    #
    #

        try:
            return self.__dbclass__ == other.__dbclass__ and self.dbid == other.dbid

        # this is required to maintain hashing
        return super().__hash__()

        return smart_str("%s" % self.db_key)

        return "%s" % self.db_key

    def __dbid_get(self):
        """
        Caches and returns the unique id of the object.
        Use this instead of self.id, which is not cached.
        """

    def __dbid_set(self, value):
        raise Exception("dbid cannot be set!")

    def __dbid_del(self):
        raise Exception("dbid cannot be deleted!")

    dbid = property(__dbid_get, __dbid_set, __dbid_del)

    def __dbref_get(self):
        """
        Returns the object's dbref on the form #NN.
        """
        return "#%s" % self.id

    def __dbref_set(self):
        raise Exception("dbref cannot be set!")

    def __dbref_del(self):
        raise Exception("dbref cannot be deleted!")

    dbref = property(__dbref_get, __dbref_set, __dbref_del)

        """
        This is called when the idmapper cache is flushed and
        allows customized actions when this happens.

        Returns:
            do_flush (bool): If True, flush this object as normal. If
                False, don't flush and expect this object to handle
                the flushing on its own.

        Notes:
            The default implementation relies on being able to clear
            Django's Foreignkey cache on objects not affected by the
            flush (notably objects with an NAttribute stored). We rely
            on this cache being stored on the format "_<fieldname>_cache".
            If Django were to change this name internally, we need to
            update here (unlikely, but marking just in case).

        """
        if self.nattributes.all():
            # we can't flush this object if we have non-persistent
            # attributes stored - those would get lost! Nevertheless
            # we try to flush as many references as we can.
            self.attributes.reset_cache()
            self.tags.reset_cache()
            # flush caches for all related fields
            for field in self._meta.fields:
                name = "_%s_cache" % field.name
                if field.is_relation and name in self.__dict__:
                    # a foreignkey - remove its cache
                    del self.__dict__[name]
        # a normal flush

    #
    # Object manipulation methods
    #

        """
        Called when this object is loaded into cache. This is  more reliable
        than to override `__init__`.

        """
        pass

    def search(cls, query, **kwargs):
        """
        Overridden by class children. This implements a common API.

        Args:
            query (str): A search query.
            **kwargs: Other search parameters.

        Returns:
            list: A list of 0, 1 or more matches, only of this typeclass.

        """
        if cls.objects.dbref(query):
            return [cls.objects.get_id(query)]
        return list(cls.objects.filter(db_key__lower=query))

    def is_typeclass(self, typeclass, exact=False):
        """
        Returns true if this object has this type OR has a typeclass
        which is an subclass of the given typeclass. This operates on
        the actually loaded typeclass (this is important since a
        failing typeclass may instead have its default currently
        loaded) typeclass - can be a class object or the python path
        to such an object to match against.

        Args:
            typeclass (str or class): A class or the full python path
                to the class to check.
            exact (bool, optional): Returns true only if the object's
                type is exactly this typeclass, ignoring parents.

        Returns:
            is_typeclass (bool): If this typeclass matches the given
                typeclass.

        """
            typeclass = [typeclass] + [
                "%s.%s" % (prefix, typeclass) for prefix in settings.TYPECLASS_PATHS
            ]
        else:
            typeclass = [typeclass.path]

        selfpath = self.path
        if exact:
            # check only exact match
            return selfpath in typeclass
        else:
            # check parent chain
                hasattr(cls, "path") and cls.path in typeclass for cls in self.__class__.mro()
            )

    def swap_typeclass(
        self,
        new_typeclass,
        clean_attributes=False,
        run_start_hooks="all",
        no_default=True,
        clean_cmdsets=False,
    ):
        """
        This performs an in-situ swap of the typeclass. This means
        that in-game, this object will suddenly be something else.
        Account will not be affected. To 'move' an account to a different
        object entirely (while retaining this object's type), use
        self.account.swap_object().

        Note that this might be an error prone operation if the
        old/new typeclass was heavily customized - your code
        might expect one and not the other, so be careful to
        bug test your code if using this feature! Often its easiest
        to create a new object and just swap the account over to
        that one instead.

        Args:
            new_typeclass (str or classobj): Type to switch to.
            clean_attributes (bool or list, optional): Will delete all
                attributes stored on this object (but not any of the
                database fields such as name or location). You can't get
                attributes back, but this is often the safest bet to make
                sure nothing in the new typeclass clashes with the old
                one. If you supply a list, only those named attributes
                will be cleared.
            run_start_hooks (str or None, optional): This is either None,
                to not run any hooks, "all" to run all hooks defined by
                at_first_start, or a string with space-separated hook-names to run
                (for example 'at_object_creation'). This will
                always be called without arguments.
            no_default (bool, optiona): If set, the swapper will not
                allow for swapping to a default typeclass in case the
                given one fails for some reason. Instead the old one will
                be preserved.
            clean_cmdsets (bool, optional): Delete all cmdsets on the object.

        """

        if not callable(new_typeclass):
            # this is an actual class object - build the path
            new_typeclass = class_from_module(new_typeclass, defaultpaths=settings.TYPECLASS_PATHS)

        # if we get to this point, the class is ok.

        if inherits_from(self, "evennia.scripts.models.ScriptDB"):
            if self.interval > 0:
                    "Cannot use swap_typeclass on time-dependent "
                    "Script '%s'.\nStop and start a new Script of the "
                    "right type instead." % self.key
                )

        self.typeclass_path = new_typeclass.path
        self.__class__ = new_typeclass

        if clean_attributes:
            # Clean out old attributes
            if is_iter(clean_attributes):
                for attr in clean_attributes:
                    self.attributes.remove(attr)
                for nattr in clean_attributes:
                    if hasattr(self.ndb, nattr):
                        self.nattributes.remove(nattr)
            else:
                self.attributes.clear()
                self.nattributes.clear()
        if clean_cmdsets:
            # purge all cmdsets
            self.cmdset.clear()
            self.cmdset.remove_default()

        if run_start_hooks == "all":
            # fake this call to mimic the first save
            self.at_first_save()
        elif run_start_hooks:
            # a custom hook-name to call.
            for start_hook in str(run_start_hooks).split():
                getattr(self, run_start_hooks)()

    #
    # Lock / permission methods
    #

    ):
        """
        Determines if another object has permission to access this one.

        Args:
            accessing_obj (str): Object trying to access this one.
            default (bool, optional): What to return if no lock of
                access_type was found
            no_superuser_bypass (bool, optional): Turn off the
                superuser lock bypass (be careful with this one).

            kwar (any): Ignored, but is there to make the api
                consistent with the object-typeclass method access, which
                use it to feed to its hook methods.

        """
        )

    def check_permstring(self, permstring):
        """
        This explicitly checks if we hold particular permission
        without involving any locks.

        Args:
            permstring (str): The permission string to check against.

        Returns:
            result (bool): If the permstring is passed or not.

        """
        if inherits_from(self, evennia.DefaultObject):
            if (
                self.account
                and self.account.is_superuser
                and not self.account.attributes.get("_quell")
            ):
        else:
            if self.is_superuser and not self.attributes.get("_quell"):

        if not permstring:
        perm = permstring.lower()
        perms = [p.lower() for p in self.permissions.all()]
        if perm in perms:
            # simplest case - we have a direct match
        if perm in _PERMISSION_HIERARCHY:
            # check if we have a higher hierarchy position
            ppos = _PERMISSION_HIERARCHY.index(perm)
                True
                if hperm in perms and hpos > ppos
            )
        # we ignore pluralization (english only)
        if perm.endswith("s"):
            return self.check_permstring(perm[:-1])

    #
    # Deletion methods
    #

    def _deleted(self, *args, **kwargs):
        """
        Scrambling method for already deleted objects
        """
        raise ObjectDoesNotExist("This object was already deleted!")

        """
        Cleaning up handlers on the typeclass level

        """
        global TICKER_HANDLER
        self.permissions.clear()
        if hasattr(self, "nicks"):
            self.nicks.clear()
        # scrambling properties
        self.delete = self._deleted

    #
    # Attribute storage
    #

    @property
    def db(self):
        """
        Attribute handler wrapper. Allows for the syntax

        ```python
           obj.db.attrname = value
           # and
           value = obj.db.attrname
           # and
           del obj.db.attrname
           # and
           all_attr = obj.db.all()
           # (unless there is an attribute
           #  named 'all', in which case that will be returned instead).
        ```

        """
        try:
            return self._db_holder
            self._db_holder = DbHolder(self, "attributes")

    @db.setter
    def db(self, value):
        "Stop accidentally replacing the db object"
        string = "Cannot assign directly to db object! "
        string += "Use db.attr=value instead."

    @db.deleter
        "Stop accidental deletion."
        raise Exception("Cannot delete the db object!")

    #
    # Non-persistent (ndb) storage
    #

    @property
    def ndb(self):
        """
        A non-attr_obj store (ndb: NonDataBase). Everything stored
        """
        try:

    @ndb.setter
    def ndb(self, value):
        "Stop accidentally replacing the ndb object"

    @ndb.deleter

        """

        Args:
            looker (TypedObject, optional): The object or account that is looking
                at/getting inforamtion for this object. If not given, some
                'safe' minimum level should be returned.

        Returns:
                said object.

        Notes:
            builders.

        """
        if self.access(looker, access_type="controls"):

    def get_extra_info(self, looker, **kwargs):
        """
        Used when an object is in a list of ambiguous objects as an
        additional information tag.

        For instance, if you had potions which could have varying
        levels of liquid left in them, you might want to display how
        many drinks are left in each when selecting which to drop, but
        not in your normal inventory listing.

        Args:
                at/getting information for this object.

        Returns:
            info (str): A string with disambiguating information,
                conventionally with a leading space.

        """

        if self.location == looker:
            return " (carried)"
        return ""

    def at_rename(self, oldname, newname):
        """
        This Hook is called by @name on a successful rename.

        Args:
            oldname (str): The instance's original name.
            newname (str): The new name for the instance.

        """
        pass

    #
    #

        """

        Returns:

        """
        )

        """

        ex.
        url(r'characters/create/', ChargenView.as_view(), name='character-create')

        Returns:

        """
        try:
            return "#"

        """

        Returns:

        Examples:

            ```python
            Oscar (Character) = '/characters/oscar/1/'
            ```

            For this to work, the developer must have defined a named view somewhere
            in urls.py that follows the format 'modelname-action', so in this case
            a named view of 'character-detail' would be referenced by this method.

            ```python
            ```

            If no View has been created and defined in urls.py, returns an
            HTML anchor.

            This method is naive and simply returns a path. Securing access to
            the actual view and limiting who can view this object is the
            developer's responsibility.

        """
        try:
                kwargs={"pk": self.pk, "slug": slugify(self.name)},
            )
            return "#"

    def web_get_puppet_url(self):
        """
        Returns the URI path for a View that allows users to puppet a specific
        object.

        Returns:
            str: URI path to object puppet page, if defined.

        Examples:
            ::

                Oscar (Character) = '/characters/oscar/1/puppet/'

            a named view of 'character-puppet' would be referenced by this method.
            ::

                url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/puppet/$',
                    CharPuppetView.as_view(), name='character-puppet')

            the actual view and limiting who can view this object is the developer's
            responsibility.

        """
        try:
                "%s-puppet" % slugify(self._meta.verbose_name),
            )
            return "#"

        """
        object.

        Returns:
            str: URI path to object update page, if defined.

        Examples:

            ```python
            Oscar (Character) = '/characters/oscar/1/change/'
            ```

            a named view of 'character-update' would be referenced by this method.
            ::

                url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',

            the actual view and limiting who can modify objects is the developer's

        """
        try:
            )
            return "#"

        """

        Returns:

        Examples:

            ```python
            Oscar (Character) = '/characters/oscar/1/delete/'
            ```

            For this to work, the developer must have defined a named view
            somewhere in urls.py that follows the format 'modelname-action', so
            in this case a named view of 'character-detail' would be referenced
            by this method.
            ::

                url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',

            If no View has been created and defined in urls.py, returns an HTML
            anchor.

            the actual view and limiting who can delete this object is the

        """
        try:
            )
            return "#"

"""
Tags are entities that are attached to objects in the same way as
Attributes. But contrary to Attributes, which are unique to an
individual object, a single Tag can be attached to any number of
objects at the same time.

Tags are used for tagging, obviously, but the data structure is also
used for storing Aliases and Permissions. This module contains the
respective handlers.

"""

from evennia.locks.lockfuncs import perm as perm_lockfunc
from evennia.utils.utils import make_iter, to_str

#
# Tags
#

class Tag(models.Model):
    """
    Tags are quick markers for objects in-game. An typeobject can have
    any number of tags, stored via its db_tags property.  Tagging
    similar objects will make it easier to quickly locate the group
    later (such as when implementing zones). The main advantage of
    tagging as opposed to using tags is speed; a tag is very
    limited in what data it can hold, and the tag key+category is
    indexed for efficient lookup in the database. Tags are shared
    between objects - a new tag is only created if the key+category
    combination did not previously exist, making them unsuitable for
    storing object-related data (for this a regular Attribute should be
    used).

    The 'db_data' field is intended as a documentation field for the
    tag itself, such as to document what this tag+category stands for
    and display that in a web interface or similar.

    The main default use for Tags is to implement Aliases for objects.
    this uses the 'aliases' tag category, which is also checked by the
    default search functions of Evennia to allow quick searches by alias.

    """

        "key", max_length=255, null=True, help_text="tag identifier", db_index=True
    )
        "category", max_length=64, null=True, blank=True, help_text="tag category", db_index=True
    )
    db_data = models.TextField(
        "data",
        help_text="optional data field with extra information. This is not searched for.",
    )
    # this is "objectdb" etc. Required behind the scenes
        "model", max_length=32, null=True, help_text="database model to Tag", db_index=True
    )
    # this is None, alias or permission
    db_tagtype = models.CharField(
        "tagtype",
        help_text="overall type of Tag",
    )

        verbose_name = "Tag"
        unique_together = (("db_key", "db_category", "db_tagtype", "db_model"),)
        index_together = (("db_key", "db_category", "db_tagtype", "db_model"),)

        return str(self) < str(other)

        return str(
            "<Tag: %s%s>"
            % (self.db_key, "(category:%s)" % self.db_category if self.db_category else "")
        )

#
# Handlers making use of the Tags model
#

class TagProperty:
    """
    Tag Property.
    """

    taghandler_name = "tags"

    def __init__(self, category=None, data=None):
        """
        Tag property descriptor. Allows for setting tags on an object as Django-like 'fields'
        on the class level. Since Tags are almost always used for querying, Tags are always
        created/assigned along with the object. Make sure the property/tagname does not collide
        with an existing method/property on the class. If it does, you must use tags.add()
        instead.

        Note that while you _can_ check e.g. `obj.tagname,this will give an AttributeError
        if the Tag is not set. Most often you want to use `obj.tags.get("tagname")` to check
        if a tag is set on an object.

        Example:
        ::

                class Character(DefaultCharacter):
                    mytag = TagProperty()  # category=None
                    mytag2 = TagProperty(category="tagcategory")
        """

        self._data = data

        """
        Called when descriptor is first assigned to the class (not the instance!).
        It is called with the name of the field.

        """

        """
        Called when accessing the tag as a property on the instance.

        """
        try:
            return getattr(instance, self.taghandler_name).get(
                key=self._key, category=self._category, return_list=False, raise_exception=True
            )
            self.__set__(instance, self._category)

    def __set__(self, instance, category):
        """
        Assign a new category to the tag. It's not possible to set 'data' this way.

        """
        (
            getattr(instance, self.taghandler_name).add(
                key=self._key, category=self._category, data=self._data
            )
        )

        """
        Called when running `del` on the property. Will disconnect the object from
        the Tag. Note that the tag will be readded on next fetch unless the
        TagProperty is also removed in code!

        """
        getattr(instance, self.taghandler_name).remove(key=self._key, category=self._category)

class TagCategoryProperty:
    """
    Tag Category Property.

    """

    def __init__(self, *default_tags):
        """
        Assign a property for a Tag Category, with any number of Tag keys.
        This is often more useful than the `TagProperty` since it's common to want to check which
        tags of a particular category the object is a member of.

        Args:
            *args (str or callable): Tag keys to assign to this property, using the category given
                by the name of the property. Note that, if these tags are always set on the object,
                if they are removed by some other means, they will be re-added when this property
                is accessed. Furthermore, changing this list after the object was created, will
                not remove any old tags (there is no way for the property to know if the
                new list is new or not).  If a callable, it will be called without arguments to
                return the tag key. It is not possible to set tag `data` this way (use the
                Taghandler directly for that). Tag keys are not case sensitive.

        Raises:
            ValueError: If the input is not a valid tag key or tuple.

        Notes:
            It is not possible to set Tags with a `None` category using a `TagCategoryProperty` -
            use `obj.tags.add()` instead.

        Example:
        ::

                class RogueCharacter(DefaultCharacter):
                    guild = TagProperty("thieves_guild", "merchant_guild")

        """
        self._category = ""
        self._default_tags = self._parse_tag_input(*default_tags)

    def _parse_tag_input(self, *args):
        """
        Parse input to the property.

        Args:
            *args (str or callable): Tags, either as strings or `callable`, which should return
            the tag key when called without arguments. Keys are not case sensitive.

        Returns:
            list: A list of tag keys.

        """
        tags = []
        for tagkey in args:
            if callable(tagkey):
                tagkey = tagkey()
            tags.append((str(tagkey).lower()))
        return tags

        """

        """
        self._category = name

        """
        Called when accessing the tag as a property on the instance. Returns a list
        of tags under the given category.
        """
        taghandler = getattr(instance, self.taghandler_name)

        default_tags = self._default_tags
        tags = taghandler.get(category=self._category, return_list=True)

        missing_default_tags = set(default_tags) - set(tags)

        if missing_default_tags:
            getattr(instance, self.taghandler_name).batch_add(
                *[(tag, self._category) for tag in missing_default_tags]
            )

            tags += missing_default_tags  # to avoid a second db call

    def __set__(self, instance, *args):
        """
        Assign a new set of tags to the category. Note that we can't know if previous
        tags were assigned from this property or from TagHandler, so we don't
        remove old tags. To refresh to only have the tags in this constructor, first
        use `del` on this property and re-access the property with the changed default list.

        """
        getattr(instance, self.taghandler_name).batch_add(*[(tag, self._category) for tag in args])

        """
        Called when running `del` on the property. Will remove all tags of this
        category from the object. Note that next time this desriptor is accessed, the
        default ones will be re-added!

        Note:
            This will remove _all_ tags of this category from the object. This is necessary
            in order to be able to be able to combine this with `__set__` to get a tag
            list where property and handler are in sync.

        """
        getattr(instance, self.taghandler_name).remove(category=self._category)

class TagHandler(object):
    """
    Generic tag-handler. Accessed via TypedObject.tags.

    """

    _m2m_fieldname = "db_tags"
    _tagtype = None

        """
        Tags are stored internally in the TypedObject.db_tags m2m
        field with an tag.db_model based on the obj the taghandler is
        stored on and with a tagtype given by self.handlertype

        Args:
            obj (object): The object on which the handler is set.

        """
        self._objid = obj.id
        self._model = obj.__dbclass__.__name__.lower()
        # full cache was run on all tags

    def _query_all(self):
        """
        Get all tags for this object.

        """
        query = {
            "tag__db_model": self._model,
            "tag__db_tagtype": self._tagtype,
        }
        return [
            conn.tag
        ]

    def _fullcache(self):
        """
        Cache all tags of this object.

        """
            return
        tags = self._query_all()
        self._cache = dict(
            (
                "%s-%s"
                % (
                    to_str(tag.db_key).lower(),
                    tag.db_category.lower() if tag.db_category else None,
                ),
                tag,
            )
            for tag in tags
        )

    def _getcache(self, key=None, category=None):
        """

        Args:
            key (str, optional): Tag key to query for
            category (str, optional): Tag category

        Returns:
        Notes:
            of that category is done and a the category *name* is is
            list of cached tags of this category is up-to-date
            caching, causing each tag access to trigger a

        """
        key = str(key).strip().lower() if key else None
        category = category.strip().lower() if category else None
        if key:
            tag = _TYPECLASS_AGGRESSIVE_CACHE and self._cache.get(cachekey, None)
            if tag and (not hasattr(tag, "pk") and tag.pk is None):
                # clear out Tags deleted from elsewhere. We must search this anew.
                tag = None
                del self._cache[cachekey]
            if tag:
                return [tag]  # return cached entity
            else:
                query = {
                    "%s__id" % self._model: self._objid,
                    "tag__db_model": self._model,
                    "tag__db_tagtype": self._tagtype,
                    "tag__db_key__iexact": key.lower(),
                    "tag__db_category__iexact": category.lower() if category else None,
                }
                conn = getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)
                if conn:
                    tag = conn[0].tag
                    if _TYPECLASS_AGGRESSIVE_CACHE:
                        self._cache[cachekey] = tag
                    return [tag]
        else:
            # only category given (even if it's None) - we can't
            # assume the cache to be complete unless we have queried
            # for this category before
            catkey = "-%s" % category
            if _TYPECLASS_AGGRESSIVE_CACHE and catkey in self._catcache:
                return [tag for key, tag in self._cache.items() if key.endswith(catkey)]
            else:
                # we have to query to make this category up-date in the cache
                query = {
                }
                tags = [
                    conn.tag
                    for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(
                        **query
                    )
                ]
                    for tag in tags:
                        cachekey = "%s-%s" % (tag.db_key, category)
                    # mark category cache as up-to-date
                    self._catcache[catkey] = True
                return tags
        return []

    def _setcache(self, key, category, tag_obj):
        """

        Args:
            tag_obj (tag): The newly saved tag

        """
            return
            return
        key, category = (
            str(key).strip().lower(),
            category.strip().lower() if category else category,
        )
        self._cache[cachekey] = tag_obj

    def _delcache(self, key, category):
        """
        Remove tag from cache

        Args:

        """
        )
        if key:
        else:
            [self._cache.pop(key, None) for key in self._cache if key.endswith(catkey)]

        """
        Reset the cache from the outside.

        """

    def add(self, key=None, category=None, data=None):
        """
        Add a new tag to the handler.

        Args:
            key (str or list): The name of the tag to add. If a list,
                add several Tags.
            category (str, optional): Category of Tag. `None` is the default category.
            data (str, optional): Info text about the tag(s) added.
                This can not be used to store object-unique info but only
                eventual info about the tag itself.

        Notes:
            If the tag + category combination matches an already
            existing Tag object, this will be re-used and no new Tag
            will be created.

        """
        if not key:
            return
            self._fullcache()
        for tagstr in make_iter(key):
            if not tagstr:
                continue
            tagstr = str(tagstr).strip().lower()
            category = str(category).strip().lower() if category else category
            data = str(data) if data is not None else None
            # this will only create tag if no matches existed beforehand (it
            # will overload data on an existing tag since that is not
            # considered part of making the tag unique)
            tagobj = self.obj.__class__.objects.create_tag(
                key=tagstr, category=category, data=data, tagtype=self._tagtype
            )
            getattr(self.obj, self._m2m_fieldname).add(tagobj)
            self._setcache(tagstr, category, tagobj)

    def has(self, key=None, category=None, return_list=False):
        """
        Checks if the given Tag (or list of Tags) exists on the object.

        Args:
            key (str or iterable): The Tag key or tags to check for.
            category (str, optional): Limit the check to Tags with this

        Returns:
            has_tag (bool or list): If the Tag exists on this object or not.
             If `tag` was given as an iterable then the return is a list of booleans.

        Raises:
            ValueError: If neither `tag` nor `category` is given.

        """
        ret = []
        if key:
            for tag_str in make_iter(key):
                tag_str = str(tag_str).strip().lower()
                ret.append(bool(self._getcache(tag_str, category)))
        elif category:
            ret.extend(bool(tag) for tag in self._getcache(category=category))
        else:
            raise ValueError("Either tag or category must be provided.")

            return ret

    def get(
        self,
        key=None,
        return_tagobj=False,
    ):
        """
        Get the tag for the given key, category or combination of the two.

        Args:
            key (str or list, optional): The tag or tags to retrieve.
            default (any, optional): The value to return in case of no match.
            category (str, optional): The Tag category to limit the
                request to. Note that `None` is the valid, default
                category. If no `key` is given, all tags of this category will be
                returned.
            return_tagobj (bool, optional): Return the Tag object itself
                instead of a string representation of the Tag.
            return_list (bool, optional): Always return a list, regardless
                of number of matches.
            raise_exception (bool, optional): Raise AttributeError if no matches
                are found.

        Returns:
            tags (list): The matches, either string
                representations of the tags or the Tag objects themselves
                depending on `return_tagobj`. If 'default' is set, this
                will be a list with the default value as its only element.

        Raises:
            AttributeError: If finding no matches and `raise_exception` is True.

        """
        ret = []
            # note - the _getcache call removes case sensitivity for us
            ret.extend(
                [
                    tag if return_tagobj else to_str(tag.db_key)
                    for tag in self._getcache(keystr, category)
                ]
            )
        if not ret:
            if raise_exception:
                raise AttributeError(f"No tags found matching input {key}, {category}.")
            elif return_list:
                return [default] if default is not None else []
            else:
        return ret if return_list else (ret[0] if len(ret) == 1 else ret)

    def remove(self, key=None, category=None):
        """
        Remove a tag from the handler based ond key and/or category.

        Args:
                category
        Notes:
            If neither key nor category is specified, this acts
            as .clear().

        """
        if not key:
            # only category
            self.clear(category=category)
            return

        for key in make_iter(key):
            if not (key or key.strip()):  # we don't allow empty tags
                continue
            tagstr = str(key).strip().lower()
            category = category.strip().lower() if category else category

            # This does not delete the tag object itself. Maybe it should do
            # that when no objects reference the tag anymore (but how to check)?
            # For now, tags are never deleted, only their connection to objects.
            tagobj = getattr(self.obj, self._m2m_fieldname).filter(
                db_key=tagstr, db_category=category, db_model=self._model, db_tagtype=self._tagtype
            )
            if tagobj:
                getattr(self.obj, self._m2m_fieldname).remove(tagobj[0])
            self._delcache(key, category)

    def clear(self, category=None):
        """
        Remove all tags from the handler.

        Args:
                category.

        """
        query = {
        }
            query["tag__db_category"] = category.strip().lower()
        getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query).delete()

    def all(self, return_key_and_category=False, return_objs=False):
        """
        Get all tags in this handler, regardless of category.

        Args:
            return_key_and_category (bool, optional): Return a list of
                tuples `[(key, category), ...]`.
            return_objs (bool, optional): Return tag objects.

        Returns:
            tags (list): A list of tag keys `[tagkey, tagkey, ...]` or
                a list of tuples `[(key, category), ...]` if
                `return_key_and_category` is set.

        """
                self._fullcache()
            tags = sorted(self._cache.values())
        else:
            tags = sorted(self._query_all())

        if return_key_and_category:
            # return tuple (key, category)
            return [(to_str(tag.db_key), tag.db_category) for tag in tags]
        elif return_objs:
            return tags
        else:
            return [to_str(tag.db_key) for tag in tags]

    def batch_add(self, *args):
        """
        Batch-add tags from a list of tuples.

        Args:
            *args (tuple or str): Each argument should be a `tagstr` keys or tuple
                `(keystr, category)` or `(keystr, category, data)`. It's possible to mix input
                types.

        Notes:
            This will generate a mimimal number of self.add calls,
            based on the number of categories involved (including
            `None`) (data is not unique and may be overwritten by the content
            of a latter tuple with the same category).

        """
        keys = defaultdict(list)
        data = {}
            tup = make_iter(tup)
            nlen = len(tup)
            if nlen == 1:  # just a key, no category
                keys[None].append(tup[0])
            elif nlen == 2:
                keys[tup[1]].append(tup[0])
            else:
                data[tup[1]] = tup[2]  # overwrite previous
        for category, key in keys.items():
            self.add(key=key, category=category, data=data.get(category, None))

    def batch_remove(self, *args):
        """
        Batch-remove tags from a list of tuples.

        Args:
                `(keystr, category)` or `(keystr, category, data)` (the `data` field is ignored,
                only `keystr`/`category` matters). It's possible to mix input types.

        """
            elif nlen > 1:
            self.remove(key=key, category=category, data=data.get(category, None))

        return ",".join(self.all())

class AliasProperty(TagProperty):
    """
    Allows for setting aliases like Django fields:
    ::

            # note that every character will get the alias bob. Make sure
            # the alias property does not collide with an existing method
            # or property on the class.
            bob = AliasProperty()

    """

    taghandler_name = "aliases"

class AliasHandler(TagHandler):
    """
    A handler for the Alias Tag type.

    """

    _tagtype = "alias"

class PermissionProperty(TagProperty):
    """
    Allows for setting permissions like Django fields:
    ::

            # note that every character will get this permission! Make
            # sure it doesn't collide with an existing method or property.
            myperm = PermissionProperty()

    """

    taghandler_name = "permissions"

class PermissionHandler(TagHandler):
    """
    A handler for the Permission Tag type.

    """

    _tagtype = "permission"

    def check(self, *permissions, require_all=False):
        """
        Straight-up check the provided permission against this handler. The check will pass if

        - any/all given permission exists on the handler (depending on if `require_all` is set).
        - If handler sits on puppeted object and this is a hierarachical perm, the puppeting
          Account's permission will also be included in the check, prioritizing the Account's perm
          (this avoids escalation exploits by puppeting a too-high prio character)
        - a permission is also considered to exist on the handler, if it is *lower* than
          a permission on the handler and this is a 'hierarchical' permission given
          in `settings.PERMISSION_HIERARCHY`. Example: If the 'Developer' hierarchical
          perm perm is set on the handler, and we check for the 'Builder' perm, the
          check will pass.

        Args:
            *permissions (str): Any number of permissions to check. By default,
                the permission is passed if any of these (or higher, if a
                hierarchical permission defined in settings.PERMISSION_HIERARCHY)
                exists in the handler. Permissions are not case-sensitive.
            require_all (bool): If set, *all* provided permissions much pass
                the check for the entire check to pass. By default only one
                needs to pass.

        Returns:
            bool: If the provided permission(s) pass the check on this handler.

        Example:
            ::
                can_enter = obj.permissions.check("Blacksmith", "Builder")

        Notes:
            This works the same way as the `perms` lockfunc and could be
            replicated with a lock check against the lockstring

                "locktype: perm(perm1) OR perm(perm2) OR ..."

            (using AND for the `require_all` condition).

        """
        if require_all:
            return all(perm_lockfunc(self.obj, None, perm) for perm in permissions)
        else:
            return any(perm_lockfunc(self.obj, None, perm) for perm in permissions)

from django.db import router

class BaseManager:
    # To retain order, track each time a Manager instance is created.
    creation_counter = 0

    # Set to True for the 'objects' managers that are automatically created.
    auto_created = False

    #: If set to True the manager will be serialized into migrations and will
    #: thus be available in e.g. RunPython operations.
    use_in_migrations = False

    def __new__(cls, *args, **kwargs):
        # Capture the arguments to make returning them trivial.
        obj = super().__new__(cls)
        obj._constructor_args = (args, kwargs)
        return obj

        self._set_creation_counter()
        self.model = None
        self.name = None
        self._db = None
        self._hints = {}

        """Return "app_label.model_label.manager_name"."""
        return "%s.%s" % (self.model._meta.label, self.name)

    def __class_getitem__(cls, *args, **kwargs):
        return cls

        """
        Return a 5-tuple of the form (as_manager (True), manager_class,
        queryset_class, args, kwargs).

        Raise a ValueError if the manager is dynamically generated.
        """
        qs_class = self._queryset_class
        if getattr(self, "_built_with_as_manager", False):
            # using MyQuerySet.as_manager()
            return (
                True,  # as_manager
                None,  # manager_class
                "%s.%s" % (qs_class.__module__, qs_class.__name__),  # qs_class
                None,  # args
                None,  # kwargs
            )
        else:
            module_name = self.__module__
            name = self.__class__.__name__
            # Make sure it's actually there and not an inner class
            module = import_module(module_name)
            if not hasattr(module, name):
                    "Could not find manager %s in %s.\n"
                    "Please note that you need to inherit from managers you "
                    "dynamically generated with 'from_queryset()'."
                    % (name, module_name)
                )
            return (
                False,  # as_manager
                "%s.%s" % (module_name, name),  # manager_class
                None,  # qs_class
                self._constructor_args[0],  # args
                self._constructor_args[1],  # kwargs
            )

    def check(self, **kwargs):
        return []

    def _get_queryset_methods(cls, queryset_class):
        def create_method(name, method):
            @wraps(method)
            def manager_method(self, *args, **kwargs):
                return getattr(self.get_queryset(), name)(*args, **kwargs)

            return manager_method

        new_methods = {}
        for name, method in inspect.getmembers(
            queryset_class, predicate=inspect.isfunction
        ):
            # Only copy missing methods.
            if hasattr(cls, name):
                continue
            # Only copy public methods or methods with the attribute
            # queryset_only=False.
            queryset_only = getattr(method, "queryset_only", None)
            if queryset_only or (queryset_only is None and name.startswith("_")):
                continue
            # Copy the method onto the manager.
            new_methods[name] = create_method(name, method)
        return new_methods

    def from_queryset(cls, queryset_class, class_name=None):
        if class_name is None:
            class_name = "%sFrom%s" % (cls.__name__, queryset_class.__name__)
            class_name,
            (cls,),
            {
                "_queryset_class": queryset_class,
                **cls._get_queryset_methods(queryset_class),
            },
        )

    def contribute_to_class(self, cls, name):
        self.name = self.name or name
        self.model = cls

        setattr(cls, name, ManagerDescriptor(self))

        cls._meta.add_manager(self)

    def _set_creation_counter(self):
        """
        Set the creation counter value for this instance and increment the
        class-level copy.
        """
        self.creation_counter = BaseManager.creation_counter
        BaseManager.creation_counter += 1

    def db_manager(self, using=None, hints=None):
        obj = copy.copy(self)
        obj._db = using or self._db
        obj._hints = hints or self._hints
        return obj

    @property
        return self._db or router.db_for_read(self.model, **self._hints)

    #######################
    # PROXIES TO QUERYSET #

    def get_queryset(self):
        """
        Return a new QuerySet object. Subclasses can override this method to
        customize the behavior of the Manager.
        """
        return self._queryset_class(model=self.model, using=self._db, hints=self._hints)

        # We can't proxy this method through the `QuerySet` like we do for the
        # rest of the `QuerySet` methods. This is because `QuerySet.all()`
        # works by creating a "copy" of the current queryset and in making said
        # copy, all the cached `prefetch_related` lookups are lost. See the
        # implementation of `RelatedManager.get_queryset()` for a better
        # understanding of how this comes into play.
        return self.get_queryset()

        return (
            isinstance(other, self.__class__)
            and self._constructor_args == other._constructor_args
        )

        return id(self)

class Manager(BaseManager.from_queryset(QuerySet)):
    pass

class ManagerDescriptor:
    def __init__(self, manager):
        self.manager = manager

        if instance is not None:
                "Manager isn't accessible via %s instances" % cls.__name__
            )

        if cls._meta.abstract:
                "Manager isn't available; %s is abstract" % (cls._meta.object_name,)
            )

        if cls._meta.swapped:
                "Manager isn't available; '%s' has been swapped for '%s'"
                % (
                    cls._meta.label,
                    cls._meta.swapped,
                )
            )

        return cls._meta.managers_map[self.manager.name]

class EmptyManager(Manager):
    def __init__(self, model):
        self.model = model

        return super().get_queryset().none()

"""
ANSI - Gives colour to text.

Use the codes defined in the *ANSIParser* class to apply colour to text. The
`parse_ansi` function in this module parses text for markup and `strip_ansi`
removes it.

You should usually not need to call `parse_ansi` explicitly; it is run by
Evennia just before returning data to/from the user. Alternative markup is
possible by overriding the parser class (see also contrib/ for deprecated
markup schemes).

Supported standards:

- ANSI 8 bright and 8 dark fg (foreground) colors
- ANSI 8 dark bg (background) colors
- 'ANSI' 8 bright bg colors 'faked' with xterm256 (bright bg not included in ANSI standard)
- Xterm256 - 255 fg/bg colors + 26 greyscale fg/bg colors

## Markup

ANSI colors: `r` ed, `g` reen, `y` ellow, `b` lue, `m` agenta, `c` yan, `n` ormal (no color).
Capital letters indicate the 'dark' variant.

- `|r` fg bright red
- `|R` fg dark red
- `|[r` bg bright red
- `|[R` bg dark red
- `|[R|g` bg dark red, fg bright green

```python
"This is |rRed text|n and this is normal again."

```

Xterm256 colors are given as RGB (Red-Green-Blue), with values 0-5:

- `|500` fg bright red
- `|050` fg bright green
- `|005` fg bright blue
- `|110` fg dark brown
- `|425` fg pink
- `|[431` bg orange

Xterm256 greyscale:

- `|=a` fg black
- `|=g` fg dark grey
- `|=o` fg middle grey
- `|=v` fg bright grey
- `|=z` fg white
- `|[=r` bg middle grey

```python
"This is |500Red text|n and this is normal again."
"This is |[=jText on dark grey background"

```

----

"""
import re
from collections import OrderedDict

MXP_ENABLED = settings.MXP_ENABLED

# ANSI definitions

ANSI_BEEP = "\07"
ANSI_ESCAPE = "\033"
ANSI_NORMAL = "\033[0m"

ANSI_UNDERLINE = "\033[4m"
ANSI_HILITE = "\033[1m"
ANSI_UNHILITE = "\033[22m"
ANSI_BLINK = "\033[5m"
ANSI_INVERSE = "\033[7m"
ANSI_INV_HILITE = "\033[1;7m"
ANSI_INV_BLINK = "\033[7;5m"
ANSI_BLINK_HILITE = "\033[1;5m"
ANSI_INV_BLINK_HILITE = "\033[1;5;7m"

# Foreground colors
ANSI_BLACK = "\033[30m"
ANSI_RED = "\033[31m"
ANSI_GREEN = "\033[32m"
ANSI_YELLOW = "\033[33m"
ANSI_BLUE = "\033[34m"
ANSI_MAGENTA = "\033[35m"
ANSI_CYAN = "\033[36m"
ANSI_WHITE = "\033[37m"

# Background colors
ANSI_BACK_BLACK = "\033[40m"
ANSI_BACK_RED = "\033[41m"
ANSI_BACK_GREEN = "\033[42m"
ANSI_BACK_YELLOW = "\033[43m"
ANSI_BACK_BLUE = "\033[44m"
ANSI_BACK_MAGENTA = "\033[45m"
ANSI_BACK_CYAN = "\033[46m"
ANSI_BACK_WHITE = "\033[47m"

# Formatting Characters
ANSI_RETURN = "\r\n"
ANSI_TAB = "\t"
ANSI_SPACE = " "

# Escapes
ANSI_ESCAPES = ("{{", "\\\\", "\|\|")

_PARSE_CACHE = OrderedDict()
_PARSE_CACHE_SIZE = 10000

_COLOR_NO_DEFAULT = settings.COLOR_NO_DEFAULT

class ANSIParser(object):
    """
    A class that parses ANSI markup
    to ANSI command sequences

    We also allow to escape colour codes
    by prepending with an extra `|`.

    """

    # Mapping using {r {n etc

    ansi_map = [
        # alternative |-format
        (r"|n", ANSI_NORMAL),  # reset
        (r"|/", ANSI_RETURN),  # line break
        (r"|-", ANSI_TAB),  # tab
        (r"|>", ANSI_SPACE * 4),  # indent (4 spaces)
        (r"|_", ANSI_SPACE),  # space
        (r"|*", ANSI_INVERSE),  # invert
        (r"|^", ANSI_BLINK),  # blinking text (very annoying and not supported by all clients)
        (r"|u", ANSI_UNDERLINE),  # underline
        (r"|r", ANSI_HILITE + ANSI_RED),
        (r"|g", ANSI_HILITE + ANSI_GREEN),
        (r"|y", ANSI_HILITE + ANSI_YELLOW),
        (r"|b", ANSI_HILITE + ANSI_BLUE),
        (r"|m", ANSI_HILITE + ANSI_MAGENTA),
        (r"|c", ANSI_HILITE + ANSI_CYAN),
        (r"|w", ANSI_HILITE + ANSI_WHITE),  # pure white
        (r"|x", ANSI_HILITE + ANSI_BLACK),  # dark grey
        (r"|R", ANSI_UNHILITE + ANSI_RED),
        (r"|G", ANSI_UNHILITE + ANSI_GREEN),
        (r"|Y", ANSI_UNHILITE + ANSI_YELLOW),
        (r"|B", ANSI_UNHILITE + ANSI_BLUE),
        (r"|M", ANSI_UNHILITE + ANSI_MAGENTA),
        (r"|C", ANSI_UNHILITE + ANSI_CYAN),
        (r"|W", ANSI_UNHILITE + ANSI_WHITE),  # light grey
        (r"|X", ANSI_UNHILITE + ANSI_BLACK),  # pure black
        # hilight-able colors
        (r"|h", ANSI_HILITE),
        (r"|H", ANSI_UNHILITE),
        (r"|!R", ANSI_RED),
        (r"|!G", ANSI_GREEN),
        (r"|!Y", ANSI_YELLOW),
        (r"|!B", ANSI_BLUE),
        (r"|!M", ANSI_MAGENTA),
        (r"|!C", ANSI_CYAN),
        (r"|!W", ANSI_WHITE),  # light grey
        (r"|!X", ANSI_BLACK),  # pure black
        # normal ANSI backgrounds
        (r"|[R", ANSI_BACK_RED),
        (r"|[G", ANSI_BACK_GREEN),
        (r"|[Y", ANSI_BACK_YELLOW),
        (r"|[B", ANSI_BACK_BLUE),
        (r"|[M", ANSI_BACK_MAGENTA),
        (r"|[C", ANSI_BACK_CYAN),
        (r"|[W", ANSI_BACK_WHITE),  # light grey background
        (r"|[X", ANSI_BACK_BLACK),  # pure black background
    ]

    ansi_xterm256_bright_bg_map = [
        # "bright" ANSI backgrounds using xterm256 since ANSI
        # standard does not support it (will
        # fallback to dark ANSI background colors if xterm256
        # is not supported by client)
        # |-style variations
        (r"|[r", r"|[500"),
        (r"|[g", r"|[050"),
        (r"|[y", r"|[550"),
        (r"|[b", r"|[005"),
        (r"|[m", r"|[505"),
        (r"|[c", r"|[055"),
        (r"|[w", r"|[555"),  # white background
        (r"|[x", r"|[222"),
    ]  # dark grey background

    # xterm256. These are replaced directly by
    # the sub_xterm256 method

    if settings.COLOR_NO_DEFAULT:
        ansi_map = settings.COLOR_ANSI_EXTRA_MAP
        xterm256_fg = settings.COLOR_XTERM256_EXTRA_FG
        xterm256_bg = settings.COLOR_XTERM256_EXTRA_BG
        xterm256_gfg = settings.COLOR_XTERM256_EXTRA_GFG
        xterm256_gbg = settings.COLOR_XTERM256_EXTRA_GBG
        ansi_xterm256_bright_bg_map = settings.COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP
    else:
        xterm256_fg = [r"\|([0-5])([0-5])([0-5])"]  # |123 - foreground colour
        xterm256_bg = [r"\|\[([0-5])([0-5])([0-5])"]  # |[123 - background colour
        xterm256_gfg = [r"\|=([a-z])"]  # |=a - greyscale foreground
        xterm256_gbg = [r"\|\[=([a-z])"]  # |[=a - greyscale background
        ansi_map += settings.COLOR_ANSI_EXTRA_MAP
        xterm256_fg += settings.COLOR_XTERM256_EXTRA_FG
        xterm256_bg += settings.COLOR_XTERM256_EXTRA_BG
        xterm256_gfg += settings.COLOR_XTERM256_EXTRA_GFG
        xterm256_gbg += settings.COLOR_XTERM256_EXTRA_GBG
        ansi_xterm256_bright_bg_map += settings.COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP

    mxp_re = r"\|lc(.*?)\|lt(.*?)\|le"
    mxp_url_re = r"\|lu(.*?)\|lt(.*?)\|le"

    # prepare regex matching
    brightbg_sub = re.compile(
        r"|".join([r"(?<!\|)%s" % re.escape(tup[0]) for tup in ansi_xterm256_bright_bg_map]),
        re.DOTALL,
    )
    xterm256_fg_sub = re.compile(r"|".join(xterm256_fg), re.DOTALL)
    xterm256_bg_sub = re.compile(r"|".join(xterm256_bg), re.DOTALL)
    xterm256_gfg_sub = re.compile(r"|".join(xterm256_gfg), re.DOTALL)
    xterm256_gbg_sub = re.compile(r"|".join(xterm256_gbg), re.DOTALL)

    # xterm256_sub = re.compile(r"|".join([tup[0] for tup in xterm256_map]), re.DOTALL)
    ansi_sub = re.compile(r"|".join([re.escape(tup[0]) for tup in ansi_map]), re.DOTALL)
    mxp_sub = re.compile(mxp_re, re.DOTALL)
    mxp_url_sub = re.compile(mxp_url_re, re.DOTALL)

    # used by regex replacer to correctly map ansi sequences
    ansi_map_dict = dict(ansi_map)
    ansi_xterm256_bright_bg_map_dict = dict(ansi_xterm256_bright_bg_map)

    # prepare matching ansi codes overall
    ansi_re = r"\033\[[0-9;]+m"
    ansi_regex = re.compile(ansi_re)

    # escapes - these double-chars will be replaced with a single
    # instance of each
    ansi_escapes = re.compile(r"(%s)" % "|".join(ANSI_ESCAPES), re.DOTALL)

    # tabs/linebreaks |/ and |- should be able to be cleaned
    unsafe_tokens = re.compile(r"\|\/|\|-", re.DOTALL)

    def sub_ansi(self, ansimatch):
        """
        Replacer used by `re.sub` to replace ANSI
        markers with correct ANSI sequences

        Args:
            ansimatch (re.matchobject): The match.

        Returns:
            processed (str): The processed match string.

        """
        return self.ansi_map_dict.get(ansimatch.group(), "")

    def sub_brightbg(self, ansimatch):
        """
        bright background markers with Xterm256 replacement

        Args:

        Returns:

        """
        return self.ansi_xterm256_bright_bg_map_dict.get(ansimatch.group(), "")

    def sub_xterm256(self, rgbmatch, use_xterm256=False, color_type="fg"):
        """
        This is a replacer method called by `re.sub` with the matched
        tag. It must return the correct ansi sequence.

        It checks `self.do_xterm256` to determine if conversion
        to standard ANSI should be done or not.

        Args:
            rgbmatch (re.matchobject): The match.
            use_xterm256 (bool, optional): Don't convert 256-colors to 16.
            color_type (str): One of 'fg', 'bg', 'gfg', 'gbg'.

        Returns:

        """
        if not rgbmatch:
            return ""

        # get tag, stripping the initial marker
        # rgbtag = rgbmatch.group()[1:]

        background = color_type in ("bg", "gbg")
        grayscale = color_type in ("gfg", "gbg")

        if not grayscale:
            # 6x6x6 color-cube (xterm indexes 16-231)
            try:
                red, green, blue = [int(val) for val in rgbmatch.groups() if val is not None]
            except (IndexError, ValueError):
                return rgbmatch.group(0)
        else:
            # grayscale values (xterm indexes 0, 232-255, 15) for full spectrum
            try:
                letter = [val for val in rgbmatch.groups() if val is not None][0]

            if letter == "a":
                colval = 16  # pure black @ index 16 (first color cube entry)
            elif letter == "z":
                colval = 231  # pure white @ index 231 (last color cube entry)
            else:
                # letter in range [b..y] (exactly 24 values!)
                colval = 134 + ord(letter)

            # ansi fallback logic expects r,g,b values in [0..5] range
            gray = round((ord(letter) - 97) / 5.0)
            red, green, blue = gray, gray, gray

        if use_xterm256:
            if not grayscale:
                colval = 16 + (red * 36) + (green * 6) + blue

            return "\033[%s8;5;%sm" % (3 + int(background), colval)
            # replaced since some clients (like Potato) does not accept codes with leading zeroes,
            # see issue #1024.
            # return "\033[%s8;5;%s%s%sm" % (3 + int(background), colval // 100, (colval % 100) // 10, colval%10)  # noqa

        else:
            # xterm256 not supported, convert the rgb value to ansi instead
            rgb = (red, green, blue)

            def _convert_for_ansi(val):
                return int((val + 1) // 2)

            # greys
            if (max(rgb) - min(rgb)) <= 1:
                match rgb:
                    case (0, 0, 0):
                        return ANSI_BACK_BLACK if background else ANSI_NORMAL + ANSI_BLACK
                    case ((1 | 2), (1 | 2), (1 | 2)):
                        return ANSI_BACK_BLACK if background else ANSI_HILITE + ANSI_BLACK
                    case ((2 | 3), (2 | 3), (2 | 3)):
                        return ANSI_BACK_WHITE if background else ANSI_NORMAL + ANSI_WHITE
                    case ((3 | 4), (3 | 4), (3 | 4)):
                    case ((4 | 5), (4 | 5), (4 | 5)):
                        return ANSI_BACK_WHITE if background else ANSI_HILITE + ANSI_WHITE

            match tuple(_convert_for_ansi(c) for c in rgb):
                # red
                case ((2 | 3), (0 | 1), (0 | 1)):
                    return ANSI_BACK_RED if background else ANSI_HILITE + ANSI_RED
                case ((1 | 2), 0, 0):
                    return ANSI_BACK_RED if background else ANSI_NORMAL + ANSI_RED
                # green
                case ((0 | 1), (2 | 3), (0 | 1)):
                    return ANSI_BACK_GREEN if background else ANSI_HILITE + ANSI_GREEN
                case ((0 | 1), 1, 0) if green > red:
                    return ANSI_BACK_GREEN if background else ANSI_NORMAL + ANSI_GREEN
                # blue
                case ((0 | 1), (0 | 1), (2 | 3)):
                    return ANSI_BACK_BLUE if background else ANSI_HILITE + ANSI_BLUE
                case (0, 0, 1):
                    return ANSI_BACK_BLUE if background else ANSI_NORMAL + ANSI_BLUE
                # cyan
                case ((0 | 1 | 2), (2 | 3), (2 | 3)) if red == min(rgb):
                    return ANSI_BACK_CYAN if background else ANSI_HILITE + ANSI_CYAN
                case (0, (1 | 2), (1 | 2)):
                    return ANSI_BACK_CYAN if background else ANSI_NORMAL + ANSI_CYAN
                # yellow
                case ((2 | 3), (2 | 3), (0 | 1 | 2)) if blue == min(rgb):
                    return ANSI_BACK_YELLOW if background else ANSI_HILITE + ANSI_YELLOW
                case ((2 | 1), (2 | 1), (0 | 1)):
                    return ANSI_BACK_YELLOW if background else ANSI_NORMAL + ANSI_YELLOW
                # magenta
                case ((2 | 3), (0 | 1 | 2), (2 | 3)) if green == min(rgb):
                    return ANSI_BACK_MAGENTA if background else ANSI_HILITE + ANSI_MAGENTA
                case ((1 | 2), 0, (1 | 2)):
                    return ANSI_BACK_MAGENTA if background else ANSI_NORMAL + ANSI_MAGENTA

    def strip_raw_codes(self, string):
        """
        Strips raw ANSI codes from a string.

        Args:
            string (str): The string to strip.

        Returns:
            string (str): The processed string.

        """
        return self.ansi_regex.sub("", string)

    def strip_mxp(self, string):
        """
        Strips all MXP codes from a string.

        Args:

        Returns:

        """
        string = self.mxp_sub.sub(r"\2", string)
        string = self.mxp_url_sub.sub(r"\1", string)  # replace with url verbatim

    def strip_unsafe_tokens(self, string):
        """
        Strip explicitly ansi line breaks and tabs.

        """
        return self.unsafe_tokens.sub("", string)

    def parse_ansi(self, string, strip_ansi=False, xterm256=False, mxp=False):
        """
        Parses a string, subbing color codes according to the stored
        mapping.

        Args:
            string (str): The string to parse.
            strip_ansi (boolean, optional): Strip all found ansi markup.
            xterm256 (boolean, optional): If actually using xterm256 or if
                these values should be converted to 16-color ANSI.
            mxp (boolean, optional): Parse MXP commands in string.

        Returns:
            string (str): The parsed string.

        """
        if hasattr(string, "_raw_string"):
            if strip_ansi:
                return string.clean()
            else:
                return string.raw()

        if not string:
            return ""

        # check cached parsings
        global _PARSE_CACHE
        cachekey = "%s-%s-%s-%s" % (string, strip_ansi, xterm256, mxp)
        if cachekey in _PARSE_CACHE:
            return _PARSE_CACHE[cachekey]

        # pre-convert bright colors to xterm256 color tags
        string = self.brightbg_sub.sub(self.sub_brightbg, string)

        def do_xterm256_fg(part):
            return self.sub_xterm256(part, xterm256, "fg")

        def do_xterm256_bg(part):
            return self.sub_xterm256(part, xterm256, "bg")

        def do_xterm256_gfg(part):
            return self.sub_xterm256(part, xterm256, "gfg")

        def do_xterm256_gbg(part):
            return self.sub_xterm256(part, xterm256, "gbg")

        in_string = utils.to_str(string)

        # do string replacement
        parsed_string = []
        parts = self.ansi_escapes.split(in_string) + [" "]
        for part, sep in zip(parts[::2], parts[1::2]):
            pstring = self.xterm256_fg_sub.sub(do_xterm256_fg, part)
            pstring = self.xterm256_bg_sub.sub(do_xterm256_bg, pstring)
            pstring = self.xterm256_gfg_sub.sub(do_xterm256_gfg, pstring)
            pstring = self.xterm256_gbg_sub.sub(do_xterm256_gbg, pstring)
            pstring = self.ansi_sub.sub(self.sub_ansi, pstring)
            parsed_string.append("%s%s" % (pstring, sep[0].strip()))
        parsed_string = "".join(parsed_string)

        if not mxp:
            parsed_string = self.strip_mxp(parsed_string)

        if strip_ansi:
            # remove all ansi codes (including those manually
            # inserted in string)
            return self.strip_raw_codes(parsed_string)

        # cache and crop old cache
        _PARSE_CACHE[cachekey] = parsed_string
        if len(_PARSE_CACHE) > _PARSE_CACHE_SIZE:
            _PARSE_CACHE.popitem(last=False)

        return parsed_string

ANSI_PARSER = ANSIParser()

#
#

def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False, mxp=False):
    """
    Parses a string, subbing color codes as needed.

    Args:
        strip_ansi (bool, optional): Strip all ANSI sequences.
        parser (ansi.AnsiParser, optional): A parser instance to use.
        xterm256 (bool, optional): Support xterm256 or not.
        mxp (bool, optional): Support MXP markup or not.

    Returns:
        string (str): The parsed string.

    """
    string = string or ""
    return parser.parse_ansi(string, strip_ansi=strip_ansi, xterm256=xterm256, mxp=mxp)

def strip_ansi(string, parser=ANSI_PARSER):
    """
    Strip all ansi from the string. This handles the Evennia-specific
    markup.

    Args:
        string (str): The string to strip.
        parser (ansi.AnsiParser, optional): The parser to use.

    Returns:
        string (str): The stripped string.

    """
    return parser.parse_ansi(string, strip_ansi=True)

def strip_raw_ansi(string, parser=ANSI_PARSER):
    """
    Remove raw ansi codes from string. This assumes pure
    ANSI-bytecodes in the string.

    Args:
        parser (bool, optional): The parser to use.

    Returns:
        string (str): the stripped string.

    """
    return parser.strip_raw_codes(string)

def strip_unsafe_tokens(string, parser=ANSI_PARSER):
    """
    Strip markup that can be used to create visual exploits
    (notably linebreaks and tags)

    """
    return parser.strip_unsafe_tokens(string)

def strip_mxp(string, parser=ANSI_PARSER):
    """
    Strip MXP markup.

    """
    return parser.strip_mxp(string)

def raw(string):
    """
    Escapes a string into a form which won't be colorized by the ansi
    parser.

    Returns:
        string (str): The raw, escaped string.

    """
    return string.replace("{", "{{").replace("|", "||")

#
# ANSIString - ANSI-aware string class
#

def _spacing_preflight(func):
    """
    This wrapper function is used to do some preflight checks on
    functions used for padding ANSIStrings.

    """

    @functools.wraps(func)
    def wrapped(self, width=78, fillchar=None):
        if fillchar is None:
            fillchar = " "
        if (len(fillchar) != 1) or (not isinstance(fillchar, str)):
            raise TypeError("must be char, not %s" % type(fillchar))
        if not isinstance(width, int):
            raise TypeError("integer argument expected, got %s" % type(width))
        _difference = width - len(self)
        if _difference <= 0:
        return func(self, width, fillchar, _difference)

    return wrapped

def _query_super(func_name):
    """
    Have the string class handle this with the cleaned string instead
    of ANSIString.

    """

    def wrapped(self, *args, **kwargs):
        return getattr(self.clean(), func_name)(*args, **kwargs)

def _on_raw(func_name):
    """
    Like query_super, but makes the operation run on the raw string.

    """

        try:
            string = args.pop(0)
            if hasattr(string, "_raw_string"):
                args.insert(0, string.raw())
            else:
                args.insert(0, string)
            # just skip out if there are no more strings
            pass
        result = getattr(self._raw_string, func_name)(*args, **kwargs)
        if isinstance(result, str):
            return ANSIString(result, decoded=True)

def _transform(func_name):
    """
    Some string functions, like those manipulating capital letters,
    return a string the same length as the original. This function
    allows us to do the same, replacing all the non-coded characters
    with the resulting string.

    """

        replacement_string = _query_super(func_name)(self, *args, **kwargs)
        to_string = []
        char_counter = 0
        for index in range(0, len(self._raw_string)):
            if index in self._code_indexes:
                to_string.append(self._raw_string[index])
            elif index in self._char_indexes:
                to_string.append(replacement_string[char_counter])
                char_counter += 1
        return ANSIString(
            "".join(to_string),
            decoded=True,
            code_indexes=self._code_indexes,
            char_indexes=self._char_indexes,
            clean_string=replacement_string,
        )

class ANSIMeta(type):
    """
    Many functions on ANSIString are just light wrappers around the string
    base class. We apply them here, as part of the classes construction.

    """

        for func_name in [
            "count",
            "startswith",
            "endswith",
            "find",
            "index",
            "isalnum",
            "isalpha",
            "isdigit",
            "islower",
            "isspace",
            "istitle",
            "isupper",
            "rfind",
            "rindex",
            "__len__",
        ]:
            setattr(cls, func_name, _query_super(func_name))
        for func_name in ["__mod__", "expandtabs", "decode", "replace", "format", "encode"]:
            setattr(cls, func_name, _on_raw(func_name))
        for func_name in ["capitalize", "translate", "lower", "upper", "swapcase"]:
            setattr(cls, func_name, _transform(func_name))

class ANSIString(str, metaclass=ANSIMeta):
    """
    Unicode-like object that is aware of ANSI codes.

    This class can be used nearly identically to strings, in that it will
    report string length, handle slices, etc, much like a string object
    would. The methods should be used identically as string methods are.

    There is at least one exception to this (and there may be more, though
    they have not come up yet). When using ''.join() or u''.join() on an
    ANSIString, color information will get lost. You must use
    ANSIString('').join() to preserve color information.

    This implementation isn't perfectly clean, as it doesn't really have an
    understanding of what the codes mean in order to eliminate
    redundant characters-- though cleaning up the strings might end up being
    inefficient and slow without some C code when dealing with larger values.
    Such enhancements could be made as an enhancement to ANSI_PARSER
    if needed, however.

    If one is going to use ANSIString, one should generally avoid converting
    away from it until one is about to send information on the wire. This is
    because escape sequences in the string may otherwise already be decoded,
    and taken literally the second time around.

    """

    # A compiled Regex for the format mini-language:
    # https://docs.python.org/3/library/string.html#formatspec
    re_format = re.compile(
        r"(?i)(?P<just>(?P<fill>.)?(?P<align>\<|\>|\=|\^))?(?P<sign>\+|\-| )?(?P<alt>\#)?"
        r"(?P<zero>0)?(?P<width>\d+)?(?P<grouping>\_|\,)?(?:\.(?P<precision>\d+))?"
        r"(?P<type>b|c|d|e|E|f|F|g|G|n|o|s|x|X|%)?"
    )

        """
        When creating a new ANSIString, you may use a custom parser that has
        the same attributes as the standard one, and you may declare the
        string to be handled as already decoded. It is important not to double
        decode strings, as escapes can only be respected once.

        Internally, ANSIString can also passes itself precached code/character
        indexes and clean strings to avoid doing extra work when combining
        ANSIStrings.

        """
        string = args[0]
        if not isinstance(string, str):
            string = to_str(string)
        parser = kwargs.get("parser", ANSI_PARSER)
        decoded = kwargs.get("decoded", False) or hasattr(string, "_raw_string")
        code_indexes = kwargs.pop("code_indexes", None)
        char_indexes = kwargs.pop("char_indexes", None)
        clean_string = kwargs.pop("clean_string", None)
        # All True, or All False, not just one.
        checks = [x is None for x in [code_indexes, char_indexes, clean_string]]
        if not len(set(checks)) == 1:
                "You must specify code_indexes, char_indexes, "
                "and clean_string together, or not at all."
            )
        if not all(checks):
            decoded = True
        if not decoded:
            # Completely new ANSI String
            clean_string = parser.parse_ansi(string, strip_ansi=True, mxp=MXP_ENABLED)
            string = parser.parse_ansi(string, xterm256=True, mxp=MXP_ENABLED)
        elif clean_string is not None:
            # We have an explicit clean string.
            pass
        elif hasattr(string, "_clean_string"):
            # It's already an ANSIString
            clean_string = string._clean_string
            code_indexes = string._code_indexes
            char_indexes = string._char_indexes
            string = string._raw_string
        else:
            # It's a string that has been pre-ansi decoded.
            clean_string = parser.strip_raw_codes(string)

            string = string.decode("utf-8")

        ansi_string = super().__new__(ANSIString, to_str(clean_string))
        ansi_string._raw_string = string
        ansi_string._clean_string = clean_string
        ansi_string._code_indexes = code_indexes
        ansi_string._char_indexes = char_indexes
        return ansi_string

        return self._raw_string

    def __format__(self, format_spec):
        """
        This magic method covers ANSIString's behavior within a str.format() or f-string.

        Current features supported: fill, align, width.

        Args:
            format_spec (str): The format specification passed by f-string or str.format(). This is
            a string such as "0<30" which would mean "left justify to 30, filling with zeros".
            The full specification can be found at
            https://docs.python.org/3/library/string.html#formatspec

        Returns:
            ansi_str (str): The formatted ANSIString's .raw() form, for display.

        """
        # This calls the compiled regex stored on ANSIString's class to analyze the format spec.
        # It returns a dictionary.
        format_data = self.re_format.match(format_spec).groupdict()
        clean = self.clean()
        base_output = ANSIString(self.raw())
        align = format_data.get("align", "<")
        fill = format_data.get("fill", " ")

        # Need to coerce width into an integer. We can be certain that it's numeric thanks to regex.
        width = format_data.get("width", None)
        if width is None:
            width = len(clean)
        else:
            width = int(width)

        if align == "<":
            base_output = self.ljust(width, fill)
        elif align == ">":
            base_output = self.rjust(width, fill)
        elif align == "^":
            base_output = self.center(width, fill)
        elif align == "=":
            pass

        # Return the raw string with ANSI markup, ready to be displayed.
        return base_output.raw()

        """
        Let's make the repr the command that would actually be used to
        construct this object, for convenience and reference.

        """
        return "ANSIString(%s, decoded=True)" % repr(self._raw_string)

    def __init__(self, *_, **kwargs):
        """
        When the ANSIString is first initialized, a few internal variables
        have to be set.

        The first is the parser. It is possible to replace Evennia's standard
        ANSI parser with one of your own syntax if you wish, so long as it
        implements the same interface.

        The second is the _raw_string. This is the original "dumb" string
        with ansi escapes that ANSIString represents.

        The third thing to set is the _clean_string. This is a string that is
        devoid of all ANSI Escapes.

        Finally, _code_indexes and _char_indexes are defined. These are lookup
        tables for which characters in the raw string are related to ANSI
        escapes, and which are for the readable text.

        """
        self.parser = kwargs.pop("parser", ANSI_PARSER)
        if self._code_indexes is None:
            self._code_indexes, self._char_indexes = self._get_indexes()

    def _shifter(iterable, offset):
        """
        Takes a list of integers, and produces a new one incrementing all
        by a number.

        """
        if not offset:
            return iterable
        return [i + offset for i in iterable]

    def _adder(cls, first, second):
        """
        Joins two ANSIStrings, preserving calculated info.

        """

        raw_string = first._raw_string + second._raw_string
        clean_string = first._clean_string + second._clean_string
        code_indexes = first._code_indexes[:]
        char_indexes = first._char_indexes[:]
        code_indexes.extend(cls._shifter(second._code_indexes, len(first._raw_string)))
        char_indexes.extend(cls._shifter(second._char_indexes, len(first._raw_string)))
            raw_string,
            code_indexes=code_indexes,
            char_indexes=char_indexes,
            clean_string=clean_string,
        )

        """
        We have to be careful when adding two strings not to reprocess things
        that don't need to be reprocessed, lest we end up with escapes being
        interpreted literally.

        """
        if not isinstance(other, str):
        if not isinstance(other, ANSIString):
            other = ANSIString(other)
        return self._adder(self, other)

    def __radd__(self, other):
        """
        Likewise, if we're on the other end.

        """
        return self._adder(other, self)

    def __getslice__(self, i, j):
        """
        This function is deprecated, so we just make it call the proper
        function.

        """
        return self.__getitem__(slice(i, j))

    def _slice(self, slc):
        """
        This function takes a slice() object.

        Slices have to be handled specially. Not only are they able to specify
        a start and end with [x:y], but many forget that they can also specify
        an interval with [x:y:z]. As a result, not only do we have to track
        the ANSI Escapes that have played before the start of the slice, we
        must also replay any in these intervals, should they exist.

        Thankfully, slicing the _char_indexes table gives us the actual
        indexes that need slicing in the raw string. We can check between
        those indexes to figure out what escape characters need to be
        replayed.

        """
        char_indexes = self._char_indexes
        slice_indexes = char_indexes[slc]
        # If it's the end of the string, we need to append final color codes.
        if not slice_indexes:
            # if we find no characters it may be because we are just outside
            # of the interval, using an open-ended slice. We must replay all
            # of the escape characters until/after this point.
            if char_indexes:
                if slc.start is None and slc.stop is None:
                    # a [:] slice of only escape characters
                    return ANSIString(self._raw_string[slc])
                if slc.start is None:
                    # this is a [:x] slice
                    return ANSIString(self._raw_string[: char_indexes[0]])
                if slc.stop is None:
                    # a [x:] slice
                    return ANSIString(self._raw_string[char_indexes[-1] + 1 :])
            return ANSIString("")
        try:
            string = self[slc.start or 0]._raw_string
        last_mark = slice_indexes[0]
        # Check between the slice intervals for escape sequences.
        i = None
        for i in slice_indexes[1:]:
            for index in range(last_mark, i):
                if index in self._code_indexes:
                    string += self._raw_string[index]
            last_mark = i
            try:
                string += self._raw_string[i]
                # raw_string not long enough
                pass
        if i is not None:
            append_tail = self._get_interleving(char_indexes.index(i) + 1)
        else:
            append_tail = ""
        return ANSIString(string + append_tail, decoded=True)

    def __getitem__(self, item):
        """
        Gateway for slices and getting specific indexes in the ANSIString. If
        this is a regexable ANSIString, it will get the data from the raw
        string instead, bypassing ANSIString's intelligent escape skipping,
        for reasons explained in the __new__ method's docstring.

        """
        if isinstance(item, slice):
            # Slices must be handled specially.
            return self._slice(item)
        try:
            self._char_indexes[item]
            raise IndexError("ANSIString Index out of range")
        # Get character codes after the index as well.
        if self._char_indexes[-1] == self._char_indexes[item]:
            append_tail = self._get_interleving(item + 1)
        else:
        item = self._char_indexes[item]

        clean = self._raw_string[item]
        result = ""
        # Get the character they're after, and replay all escape sequences
        # previous to it.
        for index in range(0, item + 1):
                result += self._raw_string[index]
        return ANSIString(result + clean + append_tail, decoded=True)

        """
        Return a string object *without* the ANSI escapes.

        Returns:
            clean_string (str): A unicode object with no ANSI escapes.

        """
        return self._clean_string

    def raw(self):
        """
        Return a string object with the ANSI escapes.

        Returns:
            raw (str): A unicode object *with* the raw ANSI escape sequences.

        """

    def partition(self, sep, reverse=False):
        """
        Splits once into three sections (with the separator being the middle section)

        We use the same techniques we used in split() to make sure each are
        colored.

        Args:
            sep (str): The separator to split the string on.
            reverse (boolean): Whether to split the string on the last
                occurrence of the separator rather than the first.

        Returns:
            ANSIString: The part of the string before the separator
            ANSIString: The separator itself
            ANSIString: The part of the string after the separator.

        """
        if hasattr(sep, "_clean_string"):
            sep = sep.clean()
        if reverse:
            parent_result = self._clean_string.rpartition(sep)
        else:
            parent_result = self._clean_string.partition(sep)
        current_index = 0
        result = tuple()
        for section in parent_result:
            result += (self[current_index : current_index + len(section)],)
            current_index += len(section)

    def _get_indexes(self):
        """
        Two tables need to be made, one which contains the indexes of all
        readable characters, and one which contains the indexes of all ANSI
        escapes. It's important to remember that ANSI escapes require more
        that one character at a time, though no readable character needs more
        than one character, since the string base class abstracts that away
        from us. However, several readable characters can be placed in a row.

        We must use regexes here to figure out where all the escape sequences
        are hiding in the string. Then we use the ranges of their starts and
        ends to create a final, comprehensive list of all indexes which are
        dedicated to code, and all dedicated to text.

        It's possible that only one of these tables is actually needed, the
        other assumed to be what isn't in the first.

        """

        code_indexes = []
        for match in self.parser.ansi_regex.finditer(self._raw_string):
            code_indexes.extend(list(range(match.start(), match.end())))
        if not code_indexes:
            # Plain string, no ANSI codes.
            return code_indexes, list(range(0, len(self._raw_string)))
        # all indexes not occupied by ansi codes are normal characters
        char_indexes = [i for i in range(len(self._raw_string)) if i not in code_indexes]
        return code_indexes, char_indexes

    def _get_interleving(self, index):
        """
        Get the code characters from the given slice end to the next
        character.

        """
        try:
            index = self._char_indexes[index - 1]
            return ""
        s = ""
            index += 1
            if index in self._char_indexes:
                break
            elif index in self._code_indexes:
                s += self._raw_string[index]
            else:
                break
        return s

        """
        Multiplication method. Implemented for performance reasons.

        """
        if not isinstance(other, int):
        raw_string = self._raw_string * other
        clean_string = self._clean_string * other
        code_indexes = self._code_indexes[:]
        char_indexes = self._char_indexes[:]
        for i in range(other):
            code_indexes.extend(self._shifter(self._code_indexes, i * len(self._raw_string)))
            char_indexes.extend(self._shifter(self._char_indexes, i * len(self._raw_string)))
        )

    def __rmul__(self, other):
        return self.__mul__(other)

    def split(self, by=None, maxsplit=-1):
        """
        Splits a string based on a separator.

        Stolen from PyPy's pure Python string implementation, tweaked for
        ANSIString.

        PyPy is distributed under the MIT licence.
        http://opensource.org/licenses/MIT

        Args:
            by (str): A string to search for which will be used to split
                the string. For instance, ',' for 'Hello,world' would
                result in ['Hello', 'world']
            maxsplit (int): The maximum number of times to split the string.
                For example, a maxsplit of 2 with a by of ',' on the string
                'Hello,world,test,string' would result in
                ['Hello', 'world', 'test,string']
        Returns:
            result (list of ANSIStrings): A list of ANSIStrings derived from
                this string.

        """
        drop_spaces = by is None
        if drop_spaces:
            by = " "

        bylen = len(by)
        if bylen == 0:
            raise ValueError("empty separator")

        res = []
        start = 0
        while maxsplit != 0:
            next = self._clean_string.find(by, start)
            if next < 0:
                break
            # Get character codes after the index as well.
            res.append(self[start:next])
            start = next + bylen
            maxsplit -= 1  # NB. if it's already < 0, it stays < 0

        res.append(self[start : len(self)])
            return [part for part in res if part != ""]
        return res

    def rsplit(self, by=None, maxsplit=-1):
        """
        Like split, but starts from the end of the string rather than the
        beginning.

        Args:
                ['Hello,world', 'test', 'string']
        Returns:

        """
        res = []
        end = len(self)
            by = " "

            next = self._clean_string.rfind(by, 0, end)
            if next < 0:
                break
            res.append(self[next + bylen : end])
            end = next

        res.append(self[:end])
        res.reverse()
        return res

    def strip(self, chars=None):
        """
        Strip from both ends, taking ANSI markers into account.

        Args:
            chars (str, optional): A string containing individual characters
                to strip off of both ends of the string. By default, any blank
                spaces are trimmed.
        Returns:
            result (ANSIString): A new ANSIString with the ends trimmed of the
                relevant characters.

        """
        clean = self._clean_string
        raw = self._raw_string

        # count continuous sequence of chars from left and right
        nlen = len(clean)
        nlstripped = nlen - len(clean.lstrip(chars))
        nrstripped = nlen - len(clean.rstrip(chars))

        # within the stripped regions, only retain parts of the raw
        # string *not* matching the clean string (these are ansi/mxp tags)
        lstripped = ""
        ic, ir1 = 0, 0
        while nlstripped:
            if ic >= nlstripped:
                break
            elif raw[ir1] != clean[ic]:
                lstripped += raw[ir1]
            else:
                ic += 1
            ir1 += 1
        rstripped = ""
        ic, ir2 = nlen - 1, len(raw) - 1
        while nrstripped:
            if nlen - ic > nrstripped:
                break
            elif raw[ir2] != clean[ic]:
                rstripped += raw[ir2]
            else:
                ic -= 1
            ir2 -= 1
        rstripped = rstripped[::-1]
        return ANSIString(lstripped + raw[ir1 : ir2 + 1] + rstripped)

    def lstrip(self, chars=None):
        """
        Strip from the left, taking ANSI markers into account.

        Args:
                to strip off of the left end of the string. By default, any
                blank spaces are trimmed.
        Returns:
            result (ANSIString): A new ANSIString with the left end trimmed of
                the relevant characters.

        """

                break
            else:
                ic += 1
            ir1 += 1
        return ANSIString(lstripped + raw[ir1:])

    def rstrip(self, chars=None):
        """
        Strip from the right, taking ANSI markers into account.

        Args:
                to strip off of the right end of the string. By default, any
        Returns:
            result (ANSIString): A new ANSIString with the right end trimmed of

        """
                break
            else:
                ic -= 1
            ir2 -= 1
        return ANSIString(raw[: ir2 + 1] + rstripped)

    def join(self, iterable):
        """
        Joins together strings in an iterable, using this string between each
        one.

        NOTE: This should always be used for joining strings when ANSIStrings
        are involved. Otherwise color information will be discarded by python,
        due to details in the C implementation of strings.

        Args:
            iterable (list of strings): A list of strings to join together

        Returns:
            ANSIString: A single string with all of the iterable's
                contents concatenated, with this string between each.

        Examples:
            ::

                >>> ANSIString(', ').join(['up', 'right', 'left', 'down'])
                ANSIString('up, right, left, down')

        """
        result = ANSIString("")
        last_item = None
        for item in iterable:
            if last_item is not None:
                result += self._raw_string
            if not isinstance(item, ANSIString):
                item = ANSIString(item)
            result += item
            last_item = item

    def _filler(self, char, amount):
        """
        Generate a line of characters in a more efficient way than just adding

        """
        if not isinstance(char, ANSIString):
            line = char * amount
            return ANSIString(
                char * amount,
                code_indexes=[],
                char_indexes=list(range(0, len(line))),
                clean_string=char,
            )
        try:
            start = char._code_indexes[0]
            start = None
        end = char._char_indexes[0]
        prefix = char._raw_string[start:end]
        postfix = char._raw_string[end + 1 :]
        line = char._clean_string * amount
        code_indexes = [i for i in range(0, len(prefix))]
        length = len(prefix) + len(line)
        code_indexes.extend([i for i in range(length, length + len(postfix))])
        char_indexes = self._shifter(list(range(0, len(line))), len(prefix))
        raw_string = prefix + line + postfix
            raw_string, clean_string=line, char_indexes=char_indexes, code_indexes=code_indexes
        )

    # The following methods should not be called with the '_difference' argument explicitly. This is
    # data provided by the wrapper _spacing_preflight.
    @_spacing_preflight
    def center(self, width, fillchar, _difference):
        """
        Center some text with some spaces padding both sides.

        Args:
            width (int): The target width of the output string.
            fillchar (str): A single character string to pad the output string
                with.
        Returns:
            result (ANSIString): A string padded on both ends with fillchar.

        """
        remainder = _difference % 2
        _difference //= 2
        spacing = self._filler(fillchar, _difference)
        result = spacing + self + spacing + self._filler(fillchar, remainder)

    def ljust(self, width, fillchar, _difference):
        """
        Left justify some text.

        Args:
                with.
        Returns:
            result (ANSIString): A string padded on the right with fillchar.

        """
        return self + self._filler(fillchar, _difference)

    def rjust(self, width, fillchar, _difference):
        """
        Right justify some text.

        Args:
                with.
        Returns:
            result (ANSIString): A string padded on the left with fillchar.

        """
        return self._filler(fillchar, _difference) + self

"""
This module contains the core methods for the Batch-command- and
Batch-code-processors respectively. In short, these are two different ways to
build a game world using a normal text-editor without having to do so 'on the
fly' in-game. They also serve as an automatic backup so you can quickly
recreate a world also after a server reset. The functions in this module is
meant to form the backbone of a system called and accessed through game
commands.

The Batch-command processor is the simplest. It simply runs a list of in-game
commands in sequence by reading them from a text file. The advantage of this is
that the builder only need to remember the normal in-game commands. They are
also executing with full permission checks etc, making it relatively safe for
builders to use. The drawback is that in-game there is really a
builder-character walking around building things, and it can be important to
create rooms and objects in the right order, so the character can move between
them. Also objects that affects players (such as mobs, dark rooms etc) will
affect the building character too, requiring extra care to turn off/on.

The Batch-code processor is a more advanced system that accepts full
Python code, executing in chunks. The advantage of this is much more
power; practically anything imaginable can be coded and handled using
the batch-code processor. There is no in-game character that moves and
that can be affected by what is being built - the database is
populated on the fly. The drawback is safety and entry threshold - the
code is executed as would any server code, without mud-specific
permission-checks, and you have full access to modifying objects
etc. You also need to know Python and Evennia's API. Hence it's
recommended that the batch-code processor is limited only to
superusers or highly trusted staff.

# Batch-command processor file syntax

The batch-command processor accepts 'batchcommand files' e.g
`batch.ev`, containing a sequence of valid Evennia commands in a
simple format. The engine runs each command in sequence, as if they
had been run at the game prompt.

Each Evennia command must be delimited by a line comment to mark its
end.

::

    look
    # delimiting comment
    create/drop box
    # another required comment

One can also inject another batchcmdfile:

::

    #INSERT path.batchcmdfile

This way entire game worlds can be created and planned offline; it is
especially useful in order to create long room descriptions where a
real offline text editor is often much better than any online text
editor or prompt.

## Example of batch.ev file:

::

    # batch file
    # all lines starting with # are comments; they also indicate
    # that a command definition is over.

    create box

    # this comment ends the @create command.

    set box/desc = A large box.

    Inside are some scattered piles of clothing.

    It seems the bottom of the box is a bit loose.

    # Again, this comment indicates the @set command is over. Note how
    # the description could be freely added. Excess whitespace on a line
    # is ignored.  An empty line in the command definition is parsed as a \n
    # (so two empty lines becomes a new paragraph).

    teleport #221

    # (Assuming #221 is a warehouse or something.)
    # (remember, this comment ends the @teleport command! Don'f forget it)

    # Example of importing another file at this point.
    #IMPORT examples.batch

    drop box

    # Done, the box is in the warehouse! (this last comment is not necessary to
    # close the drop command since it's the end of the file)

An example batch file is `contrib/examples/batch_example.ev`.

# Batch-code processor file syntax

The Batch-code processor accepts full python modules (e.g. `batch.py`)
that looks identical to normal Python files. The difference from
importing and running any Python module is that the batch-code module
is loaded as a file and executed directly, so changes to the file will
apply immediately without a server @reload.

Optionally, one can add some special commented tokens to split the
execution of the code for the benefit of the batchprocessor's
interactive- and debug-modes. This allows to conveniently step through
the code and re-run sections of it easily during development.

Code blocks are marked by commented tokens alone on a line:

- `#HEADER` - This denotes code that should be pasted at the top of all
  other code. Multiple HEADER statements - regardless of where
  it exists in the file - is the same as one big block.
  Observe that changes to variables made in one block is not
  preserved between blocks!
- `#CODE` - This designates a code block that will be executed like a
  stand-alone piece of code together with any HEADER(s)
  defined. It is mainly used as a way to mark stop points for
  the interactive mode of the batchprocessor. If no CODE block
  is defined in the module, the entire module (including HEADERS)
  is assumed to be a CODE block.
- `#INSERT path.filename` - This imports another batch_code.py file and
  runs it in the given position. The inserted file will retain
  its own HEADERs which will not be mixed with the headers of
  this file.

Importing works as normal. The following variables are automatically
made available in the script namespace.

- `caller` - The object executing the batchscript
- `DEBUG` - This is a boolean marking if the batchprocessor is running
            in debug mode. It can be checked to e.g. delete created objects
            when running a CODE block multiple times during testing.
            (avoids creating a slew of same-named db objects)

## Example batch.py file

::

    #HEADER

    from evennia.utils import create
    from types import basetypes

    GOLD = 10

    #CODE

    obj = create.create_object(basetypes.Object)
    obj2 = create.create_object(basetypes.Object)
    obj.location = caller.location
    obj.db.gold = GOLD
    caller.msg("The object was created!")

    if DEBUG:
        obj2.delete()

    #INSERT another_batch_file

    #CODE

    script = create.create_script()

"""
import codecs
import re
import sys

_ENCODINGS = settings.ENCODINGS
_RE_INSERT = re.compile(r"^\#\s*?INSERT (.*)$", re.MULTILINE)
_RE_CLEANBLOCK = re.compile(r"^\#.*?$|^\s*$", re.MULTILINE)
_RE_CMD_SPLIT = re.compile(r"^\#.*?$", re.MULTILINE)
_RE_CODE_OR_HEADER = re.compile(
    r"((?:\A|^)#\s*?CODE|(?:/A|^)#\s*?HEADER|\A)(.*?)$(.*?)(?=^#\s*?CODE.*?$|^#\s*?HEADER.*?$|\Z)",
    re.MULTILINE + re.DOTALL,
)

def read_batchfile(pythonpath, file_ending=".py"):
    """
    This reads the contents of a batch-file.  Filename is considered
    to be a python path to a batch file relative the directory
    specified in `settings.py`.

    file_ending specify which batchfile ending should be assumed (.ev
    or .py). The ending should not be included in the python path.

    Args:
        pythonpath (str): A dot-python path to a file.
        file_ending (str): The file ending of this file (.ev or .py)

    Returns:
        text (str): The text content of the batch file.

    Raises:
        IOError: If problems reading file.

    """

    # find all possible absolute paths
    abspaths = utils.pypath_to_realpath(pythonpath, file_ending, settings.BASE_BATCHPROCESS_PATHS)
    if not abspaths:
        raise IOError("Absolute batchcmd paths could not be found.")
    text = None
    decoderr = []
    for abspath in abspaths:
        # try different paths, until we get a match
        # we read the file directly into string.
        for file_encoding in _ENCODINGS:
            # try different encodings, in order
            try:
                with codecs.open(abspath, "r", encoding=file_encoding) as fobj:
                    text = fobj.read()
            except (ValueError, UnicodeDecodeError) as e:
                # this means an encoding error; try another encoding
                decoderr.append(str(e))
                continue
            break
    if not text and decoderr:
        raise UnicodeDecodeError("\n".join(decoderr), bytearray(), 0, 0, "")

#
# Batch-command processor
#

class BatchCommandProcessor(object):
    """
    This class implements a batch-command processor.

    """

    def parse_file(self, pythonpath):
        """
        This parses the lines of a batch-command-file.

        Args:
            pythonpath (str): The dot-python path to the file.

        Returns:
            list: A list of all parsed commands with arguments, as strings.

        Notes:
            Parsing follows the following rules:

            1. A `#` at the beginning of a line marks the end of the command before
               it. It is also a comment and any number of # can exist on
               subsequent lines (but not inside comments).
            2. #INSERT at the beginning of a line imports another
               batch-cmd file file and pastes it into the batch file as if
               it was written there.
            3. Commands are placed alone at the beginning of a line and their
               arguments are considered to be everything following (on any
               number of lines) until the next comment line beginning with #.
            4. Newlines are ignored in command definitions
            5. A completely empty line in a command line definition is condered
               a newline (so two empty lines is a paragraph).
            6. Excess spaces and indents inside arguments are stripped.

        """

        text = "".join(read_batchfile(pythonpath, file_ending=".ev"))

        def replace_insert(match):
            """Map replace entries"""
            try:
                path = match.group(1)
                return "\n#\n".join(self.parse_file(path))
                raise IOError("#INSERT {} failed.".format(path))

        text = _RE_INSERT.sub(replace_insert, text)
        commands = _RE_CMD_SPLIT.split(text)
        commands = [c.strip("\r\n") for c in commands]
        commands = [c for c in commands if c]

        return commands

#
# Batch-code processor
#

def tb_filename(tb):
    """Helper to get filename from traceback"""
    return tb.tb_frame.f_code.co_filename

def tb_iter(tb):
    """Traceback iterator."""
    while tb is not None:
        yield tb
        tb = tb.tb_next

class BatchCodeProcessor(object):
    """
    This implements a batch-code processor

    """

        """
        This parses the lines of a batch-code file

        Args:

        Returns:
            list: A list of all `#CODE` blocks, each with
                prepended `#HEADER` block data. If no `#CODE`
                blocks were found, this will be a list of one element
                containing all code in the file (so a normal Python file).

        Notes:
            Parsing is done according to the following rules:

            1. Code before a #CODE/HEADER block are considered part of
               the first code/header block or is the ONLY block if no
               `#CODE/HEADER` blocks are defined.
            2. Lines starting with #HEADER starts a header block (ends other blocks)
            3. Lines starting with #CODE begins a code block (ends other blocks)
            4. Lines starting with #INSERT are on form #INSERT filename. Code from
               this file are processed with their headers *separately* before
               being inserted at the point of the #INSERT.
            5. Code after the last block is considered part of the last header/code
               block

        """

        text = "".join(read_batchfile(pythonpath, file_ending=".py"))

            """Run parse_file on the import before sub:ing it into this file"""
            path = match.group(1)
            try:
                return "# batchcode insert (%s):" % path + "\n".join(self.parse_file(path))
            except IOError as err:

        # process and then insert code from all #INSERTS

        headers = []
        codes = []
        for imatch, match in enumerate(list(_RE_CODE_OR_HEADER.finditer(text))):
            mtype = match.group(1).strip().lstrip("#").strip()
            # we need to handle things differently at the start of the file
            if mtype:
                istart, iend = match.span(3)
            else:
                istart, iend = match.start(2), match.end(3)
            code = text[istart:iend]
            if mtype == "HEADER":
                headers.append(code)
            else:  # either CODE or matching from start of file
                codes.append(code)

        # join all headers together to one
        header = "# batchcode header:\n%s\n\n" % "\n\n".join(headers) if headers else ""
        # add header to each code block
        codes = ["%s# batchcode code:\n%s" % (header, code) for code in codes]
        return codes

    def code_exec(self, code, extra_environ=None, debug=False):
        """
        Execute a single code block, including imports and appending
        global vars.

        Args:
            code (str): Code to run.
            extra_environ (dict): Environment variables to run with code.
            debug (bool, optional): Set the DEBUG variable in the execution
                namespace.

        Returns:
            err (str or None): An error code or None (ok).

        """
        # define the execution environment
        environdict = {"settings_module": settings, "DEBUG": debug}
        for key, value in extra_environ.items():
            environdict[key] = value

        # initializing the django settings at the top of code
        code = (
            "# batchcode evennia initialization: \n"
            "try: settings_module.configure()\n"
            "except RuntimeError: pass\n"
            "finally: del settings_module\n\n%s" % code
        )

        # execute the block
        try:
            exec(code, environdict)
            etype, value, tb = sys.exc_info()

            fname = tb_filename(tb)
            for tb in tb_iter(tb):
                if fname != tb_filename(tb):
                    break
            lineno = tb.tb_lineno - 1
            err = ""
            for iline, line in enumerate(code.split("\n")):
                if iline == lineno:
                    err += "\n|w%02i|n: %s" % (iline + 1, line)
                elif lineno - 5 < iline < lineno + 5:
                    err += "\n%02i: %s" % (iline + 1, line)

            err += "\n".join(traceback.format_exception(etype, value, tb))
            return err

BATCHCMD = BatchCommandProcessor()
BATCHCODE = BatchCodeProcessor()

"""
Containers

Containers are storage classes usually initialized from a setting. They
represent Singletons and acts as a convenient place to find resources (
available as properties on the singleton)

evennia.GLOBAL_SCRIPTS
evennia.OPTION_CLASSES

"""

from pickle import dumps

from django.db.utils import OperationalError, ProgrammingError

from evennia.utils.utils import callables_from_module, class_from_module

SCRIPTDB = None

class Container:
    """
    Base container class. A container is simply a storage object whose
    properties can be acquired as a property on it. This is generally
    considered a read-only affair.

    The container is initialized by a list of modules containing callables.

    """

    storage_modules = []

        """
        Read data from module.

        """
        self.loaded_data = None

    def load_data(self):
        """
        Delayed import to avoid eventual circular imports from inside
        the storage modules.

        """
        if self.loaded_data is None:
            self.loaded_data = {}
            for module in self.storage_modules:
                self.loaded_data.update(callables_from_module(module))

        return self.get(key)

        """
        Retrive data by key (in case of not knowing it beforehand).

        Args:
            key (str): The name of the script.
            default (any, optional): Value to return if key is not found.

        Returns:
            any (any): The data loaded on this container.

        """
        self.load_data()
        return self.loaded_data.get(key, default)

        """
        Get all stored data

        Returns:
            scripts (list): All global script objects stored on the container.

        """
        return list(self.loaded_data.values())

class OptionContainer(Container):
    """
    Loads and stores the final list of OPTION CLASSES.

    Can access these as properties or dictionary-contents.
    """

    storage_modules = settings.OPTION_CLASS_MODULES

class GlobalScriptContainer(Container):
    """
    Simple Handler object loaded by the Evennia API to contain and manage a
    game's Global Scripts. This will list global Scripts created on their own
    but will also auto-(re)create scripts defined in `settings.GLOBAL_SCRIPTS`.

    Example:
        import evennia
        evennia.GLOBAL_SCRIPTS.scriptname

    Note:
        This does not use much of the BaseContainer since it's not loading
        callables from settings but a custom dict of tuples.

    """

        """
        Note: We must delay loading of typeclasses since this module may get
        initialized before Scripts are actually initialized.

        """
        self.typeclass_storage = dict()
        self.loaded_data = {
            key: {} if data is None else data for key, data in settings.GLOBAL_SCRIPTS.items()
        }
        self.loaded = False

    def _load_script(self, key):
        typeclass = self.typeclass_storage[key]
        script = typeclass.objects.filter(
            db_key=key, db_account__isnull=True, db_obj__isnull=True
        ).first()

        kwargs = {**self.loaded_data[key]}
        kwargs["persistent"] = kwargs.get("persistent", True)

        compare_hash = str(dumps(kwargs, protocol=4))

        if script:
            script_hash = script.attributes.get("global_script_settings", category="settings_hash")
            if script_hash is None:
                # legacy - store the hash anew and assume no change
                script.attributes.add(
                    "global_script_settings", compare_hash, category="settings_hash"
                )
            elif script_hash != compare_hash:
                # wipe the old version and create anew
                logger.log_info(f"GLOBAL_SCRIPTS: Settings changed for {key} ({typeclass}).")
                script.stop()
                script.delete()
                script = None

            logger.log_info(f"GLOBAL_SCRIPTS: (Re)creating {key} ({typeclass}).")

            script, errors = typeclass.create(**kwargs)
            if errors:
                logger.log_err("\n".join(errors))

            # store a hash representation of the setup
            script.attributes.add("global_script_settings", compare_hash, category="settings_hash")

        """
        Called last in evennia.__init__ to initialize the container late
        (after script typeclasses have finished loading).

        We include all global scripts in the handler and
        make sure to auto-load time-based scripts.

        """
        # populate self.typeclass_storage
        if not self.loaded:
            self.load_data()

        # make sure settings-defined scripts are loaded
        scripts_to_run = []
        for key in self.loaded_data:
            script = self._load_script(key)
            if script:
                scripts_to_run.append(script)
        # start all global scripts
        try:
            for script in scripts_to_run:
                script.start()
        except (OperationalError, ProgrammingError):
            # this can happen if db is not loaded yet (such as when building docs)
            pass

        """
        This delayed import avoids trying to load Scripts before they are
        initialized.

        """
        if self.loaded:
            return
        if not self.typeclass_storage:
            for key, data in list(self.loaded_data.items()):
                typeclass = data.get("typeclass", settings.BASE_SCRIPT_TYPECLASS)
                self.typeclass_storage[key] = class_from_module(
                    typeclass, fallback=settings.BASE_SCRIPT_TYPECLASS
                )
        self.loaded = True

        """
        Retrive data by key (in case of not knowing it beforehand). Any
        scripts that are in settings.GLOBAL_SCRIPTS that are not found
        will be recreated on-demand.

        Args:
            default (any, optional): Value to return if key is not found
                at all on this container (i.e it cannot be loaded at all).

        Returns:
        """
        out_value = default
        if key in self.loaded_data:
            if key not in self.typeclass_storage:
                # this means we are trying to load in a loop
                    f"Trying to access `GLOBAL_SCRIPTS.{key}` before scripts have finished "
                    "initializing. This can happen if accessing GLOBAL_SCRIPTS from the same "
                    "module the script is defined in."
                )
            # recreate if we have the info
            script_found = self._load_script(key)
            if script_found:
                out_value = script_found

        return out_value

        """
        Get all global scripts. Note that this will not auto-start
        scripts defined in settings.

        Returns:

        """
        return self.scripts.values()

# Create all singletons

GLOBAL_SCRIPTS = GlobalScriptContainer()
OPTION_CLASSES = OptionContainer()

"""
This module handles serialization of arbitrary python structural data,
intended primarily to be stored in the database. It also supports
storing Django model instances (which plain pickle cannot do).

This serialization is used internally by the server, notably for
storing data in Attributes and for piping data to process pools.

The purpose of dbserialize is to handle all forms of data. For
well-structured non-arbitrary exchange, such as communicating with a
rich web client, a simpler JSON serialization makes more sense.

This module also implements the `SaverList`, `SaverDict` and `SaverSet`
classes. These are iterables that track their position in a nested
structure and makes sure to send updates up to their root. This is
used by Attributes - without it, one would not be able to update mutables
in-situ, e.g `obj.db.mynestedlist[3][5] = 3` would never be saved and
be out of sync with the database.

"""
from collections import OrderedDict, defaultdict, deque
from collections.abc import MutableMapping, MutableSequence, MutableSet
from functools import update_wrapper

try:
    from pickle import UnpicklingError, dumps, loads
    from pickle import dumps, loads

from django.utils.safestring import SafeString

from evennia.utils.utils import is_iter, to_bytes, uses_database

__all__ = ("to_pickle", "from_pickle", "do_pickle", "do_unpickle", "dbserialize", "dbunserialize")

PICKLE_PROTOCOL = 2

# message to send if editing an already deleted Attribute in a savermutable
_ERROR_DELETED_ATTR = (
    "{cls_name} {obj} has had its root Attribute deleted. "
    "It must be cast to a {non_saver_name} before it can be modified further."
)

def _get_mysql_db_version():
    """
    This is a helper method for specifically getting the version
    string of a MySQL database.

    Returns:
        mysql_version (str): The currently used mysql database
            version.

    """

    conn = connection.cursor()
    conn.execute("SELECT VERSION()")
    version = conn.fetchone()
    return version and str(version[0]) or ""

# initialization and helpers

_FROM_MODEL_MAP = None
_TO_MODEL_MAP = None
_IGNORE_DATETIME_MODELS = None

def _IS_PACKED_DBOBJ(o):
    return isinstance(o, tuple) and len(o) == 4 and o[0] == "__packed_dbobj__"

def _IS_PACKED_SESSION(o):
    return isinstance(o, tuple) and len(o) == 3 and o[0] == "__packed_session__"

if uses_database("mysql") and _get_mysql_db_version() < "5.6.4":
    # mysql <5.6.4 don't support millisecond precision
    _DATESTRING = "%Y:%m:%d-%H:%M:%S:000000"
else:
    _DATESTRING = "%Y:%m:%d-%H:%M:%S:%f"

def _TO_DATESTRING(obj):
    """
    Creates datestring hash.

    Args:
        obj (Object): Database object.

    Returns:
        datestring (str): A datestring hash.

    """
    try:
        return _GA(obj, "db_date_created").strftime(_DATESTRING)
        # this can happen if object is not yet saved - no datestring is then set
        try:
            # we have received a None object, for example due to an erroneous save.

def _init_globals():
    """Lazy importing to avoid circular import issues"""
    global _FROM_MODEL_MAP, _TO_MODEL_MAP, _IGNORE_DATETIME_MODELS
    if not _FROM_MODEL_MAP:
        _FROM_MODEL_MAP = defaultdict(str)
        _FROM_MODEL_MAP.update(dict((c.model, c.natural_key()) for c in ContentType.objects.all()))
    if not _TO_MODEL_MAP:

        _TO_MODEL_MAP = defaultdict(str)
        _TO_MODEL_MAP.update(
            dict((c.natural_key(), c.model_class()) for c in ContentType.objects.all())
        )
        _IGNORE_DATETIME_MODELS = []
        for src_key, dst_key in settings.ATTRIBUTE_STORED_MODEL_RENAME:
            _TO_MODEL_MAP[src_key] = _TO_MODEL_MAP.get(dst_key, None)
            _IGNORE_DATETIME_MODELS.append(src_key)

#
# SaverList, SaverDict, SaverSet - Attribute-specific helper classes and functions
#

def _save(method):
    """method decorator that saves data to Attribute"""

    def save_wrapper(self, *args, **kwargs):
        self.__doc__ = method.__doc__
        ret = method(self, *args, **kwargs)
        self._save_tree()
        return ret

    return update_wrapper(save_wrapper, method)

class _SaverMutable:
    """
    Parent class for properly handling  of nested mutables in
    an Attribute. If not used something like
     obj.db.mylist[1][2] = "test" (allocation to a nested list)
    will not save the updated value to the database.
    """

        """store all properties for tracking the tree"""
        self._parent = kwargs.pop("_parent", None)
        self._db_obj = kwargs.pop("_db_obj", None)
        self._data = None

        """Make sure to evaluate as False if empty"""
        return bool(self._data)

    def _save_tree(self):
        """recursively traverse back up the tree, save when we reach the root"""
        if self._parent:
            self._parent._save_tree()
        elif self._db_obj:
            if not self._db_obj.pk:
                cls_name = self.__class__.__name__
                try:
                    non_saver_name = cls_name.split("_Saver", 1)[1].lower()
                    non_saver_name = cls_name
                    _ERROR_DELETED_ATTR.format(
                        cls_name=cls_name, obj=self, non_saver_name=non_saver_name
                    )
                )
            self._db_obj.value = self
        else:
            logger.log_err("_SaverMutable %s has no root Attribute to save to." % self)

    def _convert_mutables(self, data):
        """converts mutables to Saver* variants and assigns ._parent property"""

        def process_tree(item, parent):
            """recursively populate the tree, storing parents"""
            dtype = type(item)
            if dtype in (str, int, float, bool, tuple):
                return item
            elif dtype == list:
                dat = _SaverList(_parent=parent)
                dat._data.extend(process_tree(val, dat) for val in item)
                return dat
            elif dtype == dict:
                dat = _SaverDict(_parent=parent)
                dat._data.update((key, process_tree(val, dat)) for key, val in item.items())
                return dat
            elif dtype == defaultdict:
                dat = _SaverDefaultDict(item.default_factory, _parent=parent)
                return dat
            elif dtype == deque:
                dat = _SaverDeque(_parent=parent, maxlen=item.maxlen)
            elif dtype == set:
                dat = _SaverSet(_parent=parent)
                dat._data.update(process_tree(val, dat) for val in item)
                return dat
            return item

        return process_tree(data, self)

        return self._data.__repr__()

        return self._data.__len__()

        return self._data.__iter__()

        return self._data.__getitem__(key)

        return self._data == other

        return self._data != other

        return self._data < other

    def __gt__(self, other):
        return self._data > other

        return self._data | other

    def __ror__(self, other):

    @_save
        self._data.__setitem__(key, self._convert_mutables(value))

    @_save
        self._data.__delitem__(key)

    def deserialize(self):
        """Deserializes this mutable into its corresponding non-Saver type."""
        return deserialize(self)

class _SaverList(_SaverMutable, MutableSequence):
    """
    A list that saves itself to an Attribute when updated.
    """

        self._data = kwargs.pop("_class", list)()

    @_save
    def __iadd__(self, otherlist):
        self._data = self._data.__add__(otherlist)
        return self._data

    def __add__(self, otherlist):
        return list(self._data) + otherlist

    @_save
    def insert(self, index, value):
        self._data.insert(index, self._convert_mutables(value))

        try:
            return list(self._data) == list(other)

        try:
            return list(self._data) != list(other)

    def index(self, value, *args):
        return self._data.index(value, *args)

    @_save
    def sort(self, *, key=None, reverse=False):
        self._data.sort(key=key, reverse=reverse)

    def copy(self):
        return self._data.copy()

class _SaverDict(_SaverMutable, MutableMapping):
    """
    A dict that stores changes to an Attribute when updated
    """

        self._data = kwargs.pop("_class", dict)()

    def has_key(self, key):
        return key in self._data

    @_save
        self._data.update(*args, **kwargs)

class _SaverDefaultDict(_SaverDict):
    """
    A defaultdict that stores changes to an attribute when updated
    """

    def __init__(self, factory, *args, **kwargs):
        self._data = defaultdict(factory)
        self.default_factory = factory

        if key not in self._data.keys():
            # detect the case of db.foo['a'] with no immediate assignment
            # (important: using `key in self._data` would be always True!)
            default_value = self._data[key]
            self.__setitem__(key, default_value)
        return self._data[key]

class _SaverSet(_SaverMutable, MutableSet):
    """
    A set that saves to an Attribute when updated
    """

        self._data = set()

    def __contains__(self, value):
        return self._data.__contains__(value)

    @_save
    def add(self, value):
        self._data.add(self._convert_mutables(value))

    @_save
    def discard(self, value):
        self._data.discard(value)

    @_save
    def update(self, *others):
        self._data.update(*others)

    @_save
    def remove(self, value):
        self._data.remove(value)

    @_save
    def pop(self):
        self._data.pop()

    @_save
        self._data.clear()

    @_save
    def intersection_update(self, *others):
        self._data.intersection_update(*others)

    @_save
    def difference_update(self, *others):
        self._data.difference_update(*others)

    @_save
    def symmetric_difference_update(self, *others):
        self._data.symmetric_difference(*others)

    def isdisjoint(self, other):
        return self._data.isdisjoint(other)

    def issubset(self, other):
        return self._data.issubset(other)

    def issuperset(self, other):
        return self._data.issuperset(other)

    def union(self, *others):
        return self._data.union(*others)

    def intersection(self, *others):
        return self._data.intersection(*others)

    def difference(self, *others):
        return self._data.difference(*others)

    def symmetric_difference(self, other):
        return self._data.symmetric_difference(other)

class _SaverOrderedDict(_SaverMutable, MutableMapping):
    """
    An ordereddict that can be saved and operated on.
    """

        self._data = OrderedDict()

class _SaverDeque(_SaverMutable):
    """
    A deque that can be saved and operated on.
    """

    def __init__(self, *args, maxlen=None, **kwargs):
        self._data = deque((), maxlen=maxlen)

    @_save
    def append(self, *args, **kwargs):
        self._data.append(*args, **kwargs)

    @_save
    def appendleft(self, *args, **kwargs):
        self._data.appendleft(*args, **kwargs)

    @_save

    @_save
    def extendleft(self, *args, **kwargs):
        self._data.extendleft(*args, **kwargs)

    # maxlen property
    def _getmaxlen(self):
        return self._data.maxlen

    def _setmaxlen(self, value):
        self._data.maxlen = value

    def _delmaxlen(self):
        del self._data.maxlen

    maxlen = property(_getmaxlen, _setmaxlen, _delmaxlen)

    @_save
    def pop(self, *args, **kwargs):
        return self._data.pop(*args, **kwargs)

    @_save
    def popleft(self, *args, **kwargs):
        return self._data.popleft(*args, **kwargs)

    @_save
    def reverse(self):
        self._data.reverse()

    @_save
    def rotate(self, *args):
        self._data.rotate(*args)

    @_save
    def remove(self, *args):
        self._data.remove(*args)

_DESERIALIZE_MAPPING = {
    _SaverList.__name__: list,
    _SaverDict.__name__: dict,
    _SaverSet.__name__: set,
    _SaverOrderedDict.__name__: OrderedDict,
    _SaverDeque.__name__: deque,
    _SaverDefaultDict.__name__: defaultdict,
}

def deserialize(obj):
    """
    Make sure to *fully* decouple a structure from the database, by turning all _Saver*-mutables
    inside it back into their normal Python forms.

    """

    def _iter(obj):
        # breakpoint()
        typ = type(obj)
        tname = typ.__name__
        if tname in ("_SaverDict", "dict"):
            return {_iter(key): _iter(val) for key, val in obj.items()}
        elif tname in ("_SaverOrderedDict", "OrderedDict"):
            return OrderedDict([(_iter(key), _iter(val)) for key, val in obj.items()])
        elif tname in ("_SaverDefaultDict", "defaultdict"):
            return defaultdict(
                obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()}
            )
        elif tname in ("_SaverDeque", deque):
            return deque((_iter(val) for val in obj), maxlen=obj.maxlen)
        elif tname in _DESERIALIZE_MAPPING:
            return _DESERIALIZE_MAPPING[tname](_iter(val) for val in obj)
        elif is_iter(obj):
            return typ(_iter(val) for val in obj)
        return obj

    return _iter(obj)

#
# serialization helpers

def pack_dbobj(item):
    """
    Check and convert django database objects to an internal representation.

    Args:
        item (any): A database entity to pack

    Returns:
        packed (any or tuple): Either returns the original input item
            or the packing tuple `("__packed_dbobj__", key, creation_time, id)`.

    """
    _init_globals()
    obj = item
    natural_key = _FROM_MODEL_MAP[
        hasattr(obj, "id")
        and hasattr(obj, "db_date_created")
        and hasattr(obj, "__dbclass__")
        and obj.__dbclass__.__name__.lower()
    ]
    # build the internal representation as a tuple
    #  ("__packed_dbobj__", key, creation_time, id)
    return (
        natural_key
        and ("__packed_dbobj__", natural_key, _TO_DATESTRING(obj), _GA(obj, "id"))
        or item
    )

def unpack_dbobj(item):
    """
    Check and convert internal representations back to Django database
    models.

    Args:
        item (packed_dbobj): The fact that item is a packed dbobj
            should be checked before this call.

    Returns:
        unpacked (any): Either the original input or converts the
            internal store back to a database representation (its
            typeclass is returned if applicable).

    """
    try:
        obj = item[3] and _TO_MODEL_MAP[item[1]].objects.get(id=item[3])
    except ObjectDoesNotExist:
        if hasattr(item, "pk"):
            # this happens if item is already an obj
    if item[1] in _IGNORE_DATETIME_MODELS:
        # if we are replacing models we ignore the datatime
        return obj
    else:
        # even if we got back a match, check the sanity of the date (some
        # databases may 're-use' the id)
        return _TO_DATESTRING(obj) == item[2] and obj or None

def pack_session(item):
    """
    Handle the safe serializion of Sessions objects (these contain
    hidden references to database objects (accounts, puppets) so they
    can't be safely serialized).

    Args:
        item (Session)): This item must have all properties of a session
            before entering this call.

    Returns:
        packed (tuple or None): A session-packed tuple on the form
            `(__packed_session__, sessid, conn_time)`. If this sessid
            does not match a session in the Session handler, None is returned.

    """
    session = evennia.SESSION_HANDLER.get(item.sessid)
    if session and session.conn_time == item.conn_time:
        # we require connection times to be identical for the Session
        # to be accepted as actually being a session (sessids gets
        # reused all the time).
        return (
            item.conn_time
            and item.sessid
            and ("__packed_session__", _GA(item, "sessid"), _GA(item, "conn_time"))
        )

def unpack_session(item):
    """
    Check and convert internal representations back to Sessions.

    Args:
        item (packed_session): The fact that item is a packed session

    Returns:
            internal store back to a Session. If Session no longer
            exists, None will be returned.
    """
    session = evennia.SESSION_HANDLER.get(item[1])
    if session and session.conn_time == item[2]:
        # to be accepted as the same as the one stored (sessids gets
        return session

#
# Access methods

def to_pickle(data):
    """
    This prepares data on arbitrary form to be pickled. It handles any
    nested structure and returns data on a form that is safe to pickle
    (including having converted any database models to their internal
    representation).  We also convert any Saver*-type objects back to
    their normal representations, they are not pickle-safe.

    Args:
        data (any): Data to pickle.

    Returns:
        data (any): Pickled data.

    """

    def process_item(item):
        """Recursive processor and identification of data"""

        dtype = type(item)

        if dtype in (str, int, float, bool, bytes, SafeString):
        elif dtype == tuple:
            return tuple(process_item(val) for val in item)
        elif dtype in (list, _SaverList):
            return [process_item(val) for val in item]
        elif dtype in (dict, _SaverDict):
            return dict((process_item(key), process_item(val)) for key, val in item.items())
        elif dtype in (defaultdict, _SaverDefaultDict):
                item.default_factory,
                ((process_item(key), process_item(val)) for key, val in item.items()),
            )
        elif dtype in (deque, _SaverDeque):
            return deque((process_item(val) for val in item), maxlen=item.maxlen)
        elif dtype in (set, _SaverSet):
            return set(process_item(val) for val in item)
        elif dtype in (OrderedDict, _SaverOrderedDict):
            return OrderedDict((process_item(key), process_item(val)) for key, val in item.items())
            return deque(process_item(val) for val in item)

        # not one of the base types
        if hasattr(item, "__serialize_dbobjs__"):
            # Allows custom serialization of any dbobjects embedded in
            # the item that Evennia will otherwise not find (these would
            # otherwise lead to an error). Use the dbserialize helper from
            # this method.
            try:
                item.__serialize_dbobjs__()
            except TypeError as err:
                # we catch typerrors so we can handle both classes (requiring
                # classmethods) and instances
                pass

        if hasattr(item, "__iter__"):
            try:
                # we try to conserve the iterable class, if not convert to dict
                try:
                    return item.__class__(
                        (process_item(key), process_item(val)) for key, val in item.items()
                    )
                except (AttributeError, TypeError):
                    return {process_item(key): process_item(val) for key, val in item.items()}
                # we try to conserve the iterable class, if not convert to list
                try:
                    return item.__class__([process_item(val) for val in item])
                    return [process_item(val) for val in item]
        elif hasattr(item, "sessid") and hasattr(item, "conn_time"):
            return pack_session(item)
        try:
            return pack_dbobj(item)
            logger.log_err(f"The object {item} of type {type(item)} could not be stored.")
            raise

    return process_item(data)

# @transaction.autocommit
def from_pickle(data, db_obj=None):
    """
    This should be fed a just de-pickled data object. It will be converted back
    to a form that may contain database objects again. Note that if a database
    object was removed (or changed in-place) in the database, None will be
    returned.

    Args:
        data (any): Pickled data to unpickle.
        db_obj (Atribute, any): This is the model instance (normally
            an Attribute) that _Saver*-type iterables (_SaverList etc)
            will save to when they update. It must have a 'value' property
            that saves assigned data to the database. Skip if not
            serializing onto a given object.  If db_obj is given, this
            function will convert lists, dicts and sets to their
            _SaverList, _SaverDict and _SaverSet counterparts.

    Returns:
        data (any): Unpickled data.

    """

        elif _IS_PACKED_DBOBJ(item):
            # this must be checked before tuple
            return unpack_dbobj(item)
        elif _IS_PACKED_SESSION(item):
            return unpack_session(item)
        elif dtype == dict:
        elif dtype == defaultdict:
            )
        elif dtype == set:
        elif dtype == OrderedDict:
        elif dtype == deque:
        elif hasattr(item, "__iter__"):
            try:
                try:
                    )
                try:
                    # we try to conserve the iterable class if
                    # it accepts an iterator
                    return item.__class__(process_item(val) for val in item)

        if hasattr(item, "__deserialize_dbobjs__"):
            # this allows the object to custom-deserialize any embedded dbobjs
            # that we previously serialized with __serialize_dbobjs__.
            # use the dbunserialize helper in this module.
            try:
                item.__deserialize_dbobjs__()
            except (TypeError, UnpicklingError):
                # handle recoveries both of classes (requiring classmethods
                # or instances. Unpickling errors can happen when re-loading the
                # data from cache (because the hidden entity was already
                # deserialized and stored back on the object, unpickling it
                # again fails). TODO: Maybe one could avoid this retry in a
                # more graceful way?
                pass

        return item

    def process_tree(item, parent):
        """Recursive processor, building a parent-tree from iterable data"""
            return tuple(process_tree(val, item) for val in item)
        elif dtype == list:
            dat = _SaverList(_parent=parent)
            dat._data.extend(process_tree(val, dat) for val in item)
            return dat
            dat = _SaverDict(_parent=parent)
            dat._data.update(
                (process_item(key), process_tree(val, dat)) for key, val in item.items()
            )
            return dat
            dat = _SaverDefaultDict(item.default_factory, _parent=parent)
            )
            return dat
            dat = _SaverSet(_parent=parent)
            dat._data.update(set(process_tree(val, dat) for val in item))
            return dat
            dat = _SaverOrderedDict(_parent=parent)
            )
            return dat
            dat = _SaverDeque(_parent=parent, maxlen=item.maxlen)
            dat._data.extend(process_item(val) for val in item)
            return dat
            try:
                try:
                    dat = _SaverDict(_parent=parent, _class=item.__class__)
                    dat._data.update(
                        (process_item(key), process_tree(val, dat)) for key, val in item.items()
                    )
                    return dat
                    dat = _SaverDict(_parent=parent)
                    )
                    return dat
                try:
                    # we try to conserve the iterable class if it
                    # accepts an iterator
                    dat = _SaverList(_parent=parent, _class=item.__class__)
                    dat._data.extend(process_tree(val, dat) for val in item)
                    return dat
                    dat = _SaverList(_parent=parent)
                    return dat

            try:
                pass

    if db_obj:
        # convert lists, dicts and sets to their Saved* counterparts. It
        # is only relevant if the "root" is an iterable of the right type.
        dtype = type(data)
        if dtype in (str, int, float, bool, bytes, SafeString, tuple):
            return process_item(data)
            dat = _SaverList(_db_obj=db_obj)
            dat._data.extend(process_tree(val, dat) for val in data)
            return dat
            dat = _SaverDict(_db_obj=db_obj)
                (process_item(key), process_tree(val, dat)) for key, val in data.items()
            )
            return dat
            dat = _SaverDefaultDict(data.default_factory, _db_obj=db_obj)
            )
            return dat
            dat = _SaverSet(_db_obj=db_obj)
            dat._data.update(process_tree(val, dat) for val in data)
            return dat
            dat = _SaverOrderedDict(_db_obj=db_obj)
            )
            return dat
            dat = _SaverDeque(_db_obj=db_obj, maxlen=data.maxlen)
            dat._data.extend(process_item(val) for val in data)
            return dat
        elif hasattr(data, "__iter__"):
            try:
                try:
                    dat = _SaverDict(_db_obj=db_obj, _class=data.__class__)
                        (process_item(key), process_tree(val, dat)) for key, val in data.items()
                    )
                    return dat
                    dat = _SaverDict(_db_obj=db_obj)
                    )
                    return dat
                try:
                    dat = _SaverList(_db_obj=db_obj, _class=data.__class__)
                    dat._data.extend(process_tree(val, dat) for val in data)
                    return dat

                    dat = _SaverList(_db_obj=db_obj)
                    return dat

def do_pickle(data):
    """Perform pickle to string"""
    try:
        return dumps(data, protocol=PICKLE_PROTOCOL)
        logger.log_err(f"Could not pickle data for storage: {data}")
        raise

def do_unpickle(data):
    """Retrieve pickle from pickled string"""
    try:
        return loads(to_bytes(data))
        logger.log_err(f"Could not unpickle data from storage: {data}")
        raise

def dbserialize(data):
    """Serialize to pickled form in one step"""
    return do_pickle(to_pickle(data))

def dbunserialize(data, db_obj=None):
    """Un-serialize in one step. See from_pickle for help db_obj."""
    return from_pickle(do_unpickle(data), db_obj=db_obj)

"""
EvEditor (Evennia Line Editor)

This implements an advanced line editor for editing longer texts in-game. The
editor mimics the command mechanisms of the "VI" editor (a famous line-by-line
editor) as far as reasonable.

Features of the editor:

- undo/redo.
- edit/replace on any line of the buffer.
- search&replace text anywhere in buffer.
- formatting of buffer, or selection, to certain width + indentations.
- allow to echo the input or not, depending on your client.
- in-built help

To use the editor, just import EvEditor from this module and initialize it:

```python

# set up an editor to edit the caller's 'desc' Attribute
def _loadfunc(caller):
    return caller.db.desc

def _savefunc(caller, buffer):
    caller.db.desc = buffer.strip()

def _quitfunc(caller):
    caller.msg("Custom quit message")

# start the editor
EvEditor(caller, loadfunc=None, savefunc=None, quitfunc=None, key="",
         persistent=True, code=False)
```

The editor can also be used to format Python code and be made to
survive a reload. See the `EvEditor` class for more details.

"""
import re

from evennia.utils import dedent, fill, is_iter, justify, logger, to_str, utils

# we use cmdhandler instead of evennia.syscmdkeys to
# avoid some cases of loading before evennia init'd

_RE_GROUP = re.compile(r"\".*?\"|\'.*?\'|\S*")
# use NAWS in the future?

#
# texts
#

_HELP_TEXT = _(
    """
 <txt>  - any non-command is appended to the end of the buffer.
 :  <l> - view buffer or only line(s) <l>
 :: <l> - raw-view buffer or only line(s) <l>
 :::    - escape - enter ':' as the only character on the line.
 :h     - this help.

 :w     - save the buffer (don't quit)
 :wq    - save buffer and quit
 :q     - quit (will be asked to save if buffer was changed)
 :q!    - quit without saving, no questions asked

 :u     - (undo) step backwards in undo history
 :uu    - (redo) step forward in undo history
 :UU    - reset all changes back to initial state

 :dd <l>     - delete last line or line(s) <l>
 :dw <l> <w> - delete word or regex <w> in entire buffer or on line <l>
 :DD         - clear entire buffer

 :y  <l>        - yank (copy) line(s) <l> to the copy buffer
 :x  <l>        - cut line(s) <l> and store it in the copy buffer
 :p  <l>        - put (paste) previously copied line(s) directly after <l>
 :i  <l> <txt>  - insert new text <txt> at line <l>. Old line will move down
 :r  <l> <txt>  - replace line <l> with text <txt>
 :I  <l> <txt>  - insert text at the beginning of line <l>
 :A  <l> <txt>  - append text after the end of line <l>

 :s <l> <w> <txt> - search/replace word or regex <w> in buffer or on line <l>

 :j <l> <w> - justify buffer or line <l>. <w> is f, c, l or r. Default f (full)
 :f <l>     - flood-fill entire buffer or line <l>: Equivalent to :j left
 :fi <l>    - indent entire buffer or line <l>
 :fd <l>    - de-indent entire buffer or line <l>

 :echo - turn echoing of the input on/off (helpful for some clients)
"""
)

_HELP_LEGEND = _(
    """
    Legend:
    <l>   - line number, like '5' or range, like '3:7'.
    <w>   - a single word, or multiple words with quotes around them.
    <txt> - longer string, usually not needing quotes.
"""
)

_HELP_CODE = _(
    """
 :!    - Execute code buffer without saving
 :<    - Decrease the level of automatic indentation for the next lines
 :>    - Increase the level of automatic indentation for the next lines
 :=    - Switch automatic indentation on/off
""".lstrip(
        "\n"
    )
)

_ERROR_LOADFUNC = _(
    """
{error}

|rBuffer load function error. Could not load initial data.|n
"""
)

_ERROR_SAVEFUNC = _(
    """
{error}

|rSave function returned an error. Buffer not saved.|n
"""
)

_ERROR_NO_SAVEFUNC = _("|rNo save function defined. Buffer cannot be saved.|n")

_MSG_SAVE_NO_CHANGE = _("No changes need saving")
_DEFAULT_NO_QUITFUNC = _("Exited editor.")

_ERROR_QUITFUNC = _(
    """
{error}

|rQuit function gave an error. Skipping.|n
"""
)

_ERROR_PERSISTENT_SAVING = _(
    """
{error}

|rThe editor state could not be saved for persistent mode. Switching
to non-persistent mode (which means the editor session won't survive
an eventual server reload - so save often!)|n
"""
)

_TRACE_PERSISTENT_SAVING = _(
    "EvEditor persistent-mode error. Commonly, this is because one or "
    "more of the EvEditor callbacks could not be pickled, for example "
    "because it's a class method or is defined inside another function."
)

_MSG_NO_UNDO = _("Nothing to undo.")
_MSG_NO_REDO = _("Nothing to redo.")
_MSG_UNDO = _("Undid one step.")
_MSG_REDO = _("Redid one step.")

#
# Handle yes/no quit question
#

class CmdSaveYesNo(_COMMAND_DEFAULT_CLASS):
    """
    Save the editor state on quit. This catches
    nomatches (defaults to Yes), and avoid saves only if
    command was given specifically as "no" or "n".
    """

    aliases = _CMD_NOINPUT
    help_cateogory = "LineEditor"

        """
        Implement the yes/no choice.

        """
        # this is only called from inside the lineeditor
        # so caller.ndb._lineditor must be set.

        self.caller.cmdset.remove(SaveYesNoCmdSet)
        if self.raw_string.strip().lower() in ("no", "n"):
            # answered no
            self.caller.msg(self.caller.ndb._eveditor.quit())
        else:
            # answered yes (default)
            self.caller.ndb._eveditor.save_buffer()
            self.caller.ndb._eveditor.quit()

class SaveYesNoCmdSet(CmdSet):
    """
    Stores the yesno question

    """

    key = "quitsave_yesno"

        """at cmdset creation"""
        self.add(CmdSaveYesNo())

#
# Editor commands
#

class CmdEditorBase(_COMMAND_DEFAULT_CLASS):
    """
    Base parent for editor commands
    """

    help_entry = "LineEditor"

    editor = None

        """
        Handles pre-parsing. Editor commands are on the form

        ::

            :cmd [li] [w] [txt]

        Where all arguments are optional.

        - `li`  - line number (int), starting from 1. This could also
              be a range given as <l>:<l>.
        - `w`  - word(s) (string), could be encased in quotes.
        - `txt` - extra text (string), could be encased in quotes.

        """

        editor = self.caller.ndb._eveditor
        if not editor:
            # this will completely replace the editor
            _load_editor(self.caller)
            editor = self.caller.ndb._eveditor
        self.editor = editor

        linebuffer = self.editor.get_buffer().split("\n")

        nlines = len(linebuffer)

        # The regular expression will split the line by whitespaces,
        # stripping extra whitespaces, except if the text is
        # surrounded by single- or double quotes, in which case they
        # will be kept together and extra whitespace preserved. You
        # can input quotes on the line by alternating single and
        # double quotes.
        arglist = [part for part in _RE_GROUP.findall(self.args) if part]
        temp = []
        for arg in arglist:
            # we want to clean the quotes, but only one type,
            # in case we are nesting.
            if arg.startswith('"'):
                arg.strip('"')
            elif arg.startswith("'"):
                arg.strip("'")
            temp.append(arg)
        arglist = temp

        # A dumb split, without grouping quotes
        words = self.args.split()

        # current line number
        cline = nlines - 1

        # the first argument could also be a range of line numbers, on the
        # form <lstart>:<lend>. Either of the ends could be missing, to
        # mean start/end of buffer respectively.

        lstart, lend = cline, cline + 1
        linerange = False
        if arglist and arglist[0].count(":") == 1:
            part1, part2 = arglist[0].split(":")
            if part1 and part1.isdigit():
                lstart = min(max(0, int(part1)) - 1, nlines)
                linerange = True
            if part2 and part2.isdigit():
                lend = min(lstart + 1, int(part2)) + 1
        elif arglist and arglist[0].isdigit():
            lstart = min(max(0, int(arglist[0]) - 1), nlines)
            lend = lstart + 1
            linerange = True
        if linerange:
            arglist = arglist[1:]

        # nicer output formatting of the line range.
        lstr = (
            "line %i" % (lstart + 1)
            if not linerange or lstart + 1 == lend
            else "lines %i-%i" % (lstart + 1, lend)
        )

        # arg1 and arg2 is whatever arguments. Line numbers or -ranges are
        # never included here.
        args = " ".join(arglist)
        arg1, arg2 = "", ""
        if len(arglist) > 1:
            arg1, arg2 = arglist[0], " ".join(arglist[1:])
        else:
            arg1 = " ".join(arglist)

        # store for use in func()

        self.linebuffer = linebuffer
        self.nlines = nlines
        self.cline = cline
        self.lstart = lstart
        self.lend = lend
        self.linerange = linerange
        self.lstr = lstr
        self.words = words
        self.arg1 = arg1
        self.arg2 = arg2

    """
    Load persistent editor from storage.

    """
    saved_options = caller.attributes.get("_eveditor_saved")
    saved_buffer, saved_undo = caller.attributes.get("_eveditor_buffer_temp", (None, None))
    unsaved = caller.attributes.get("_eveditor_unsaved", False)
    indent = caller.attributes.get("_eveditor_indent", 0)
    if saved_options:
        eveditor = EvEditor(caller, **saved_options[0])
        if saved_buffer:
            # we have to re-save the buffer data so we can handle subsequent restarts
            caller.attributes.add("_eveditor_buffer_temp", (saved_buffer, saved_undo))
            setattr(eveditor, "_buffer", saved_buffer)
            setattr(eveditor, "_undo_buffer", saved_undo)
            setattr(eveditor, "_undo_pos", len(saved_undo) - 1)
            setattr(eveditor, "_unsaved", unsaved)
            setattr(eveditor, "_indent", indent)
        for key, value in saved_options[1].items():
            setattr(eveditor, key, value)
    else:
        # something went wrong. Cleanup.
        caller.cmdset.remove(EvEditorCmdSet)

class CmdLineInput(CmdEditorBase):
    """
    No command match - Inputs line of text into buffer.

    """

        """
        Adds the line without any formatting changes.

        If the editor handles code, it might add automatic
        indentation.
        """
        editor = caller.ndb._eveditor
        buf = editor.get_buffer()

        # add a line of text to buffer
        line = self.raw_string.strip("\r\n")
        if editor._codefunc and editor._indent >= 0:
            # if automatic indentation is active, add spaces
            line = editor.deduce_indent(line, buf)
        buf = line if not buf else buf + "\n%s" % line
        self.editor.update_buffer(buf)
        if self.editor._echo_mode:
            # need to do it here or we will be off one line
            cline = len(self.editor.get_buffer().split("\n"))
            if editor._codefunc:
                # display the current level of identation
                indent = editor._indent
                if indent < 0:
                    indent = "off"

                self.caller.msg("|b%02i|||n (|g%s|n) %s" % (cline, indent, raw(line)))
            else:
                self.caller.msg("|b%02i|||n %s" % (cline, raw(self.args)))

class CmdEditorGroup(CmdEditorBase):
    """
    Commands for the editor
    """

    key = ":editor_command_group"
    aliases = [
        ":",
        "::",
        ":::",
        ":h",
        ":w",
        ":wq",
        ":q",
        ":q!",
        ":u",
        ":uu",
        ":UU",
        ":dd",
        ":dw",
        ":DD",
        ":y",
        ":x",
        ":p",
        ":i",
        ":j",
        ":r",
        ":I",
        ":A",
        ":s",
        ":S",
        ":f",
        ":fi",
        ":fd",
        ":echo",
        ":!",
        ":<",
        ":>",
        ":=",
    ]

        """
        This command handles all the in-editor :-style commands. Since
        each command is small and very limited, this makes for a more
        efficient presentation.

        """

        linebuffer = self.linebuffer
        lstart, lend = self.lstart, self.lend
        cmd = self.cmdstring
        echo_mode = self.editor._echo_mode

        if cmd == ":":
            # Echo buffer
            if self.linerange:
                buf = linebuffer[lstart:lend]
                editor.display_buffer(buf=buf, offset=lstart)
            else:
                editor.display_buffer()
        elif cmd == "::":
            # Echo buffer without the line numbers and syntax parsing
                editor.display_buffer(buf=buf, offset=lstart, linenums=False, options={"raw": True})
            else:
                editor.display_buffer(linenums=False, options={"raw": True})
        elif cmd == ":::":
            # Insert single colon alone on a line
            editor.update_buffer([":"] if lstart == 0 else linebuffer + [":"])
            if echo_mode:
                caller.msg(_("Single ':' added to buffer."))
        elif cmd == ":h":
            # help entry
            editor.display_help()
        elif cmd == ":w":
            # save without quitting
            editor.save_buffer()
        elif cmd == ":wq":
            # save and quit
            editor.quit()
        elif cmd == ":q":
            # quit. If not saved, will ask
            if self.editor._unsaved:
                caller.cmdset.add(SaveYesNoCmdSet)
                caller.msg(_("Save before quitting?") + " |lcyes|lt[Y]|le/|lcno|ltN|le")
            else:
                editor.quit()
        elif cmd == ":q!":
            # force quit, not checking saving
        elif cmd == ":u":
            # undo
            editor.update_undo(-1)
        elif cmd == ":uu":
            # redo
            editor.update_undo(1)
        elif cmd == ":UU":
            # reset buffer
            editor.update_buffer(editor._pristine_buffer)
            caller.msg(_("Reverted all changes to the buffer back to original state."))
        elif cmd == ":dd":
            # :dd <l> - delete line <l>
            buf = linebuffer[:lstart] + linebuffer[lend:]
            editor.update_buffer(buf)
            caller.msg(_("Deleted {string}.").format(string=self.lstr))
        elif cmd == ":dw":
            # :dw <w> - delete word in entire buffer
            # :dw <l> <w> delete word only on line(s) <l>
            if not self.arg1:
                caller.msg(_("You must give a search word to delete."))
            else:
                if not self.linerange:
                    lstart = 0
                    lend = self.cline + 1
                        _("Removed {arg1} for lines {l1}-{l2}.").format(
                            arg1=self.arg1, l1=lstart + 1, l2=lend + 1
                        )
                    )
                else:
                        _("Removed {arg1} for {line}.").format(arg1=self.arg1, line=self.lstr)
                    )
                sarea = "\n".join(linebuffer[lstart:lend])
                sarea = re.sub(r"%s" % self.arg1.strip("'").strip('"'), "", sarea, re.MULTILINE)
                buf = linebuffer[:lstart] + sarea.split("\n") + linebuffer[lend:]
                editor.update_buffer(buf)
        elif cmd == ":DD":
            # clear buffer
            editor.update_buffer("")

            # Reset indentation level to 0
                if editor._indent >= 0:
                    editor._indent = 0
                    if editor._persistent:
                        caller.attributes.add("_eveditor_indent", 0)
            caller.msg(_("Cleared {nlines} lines from buffer.").format(nlines=self.nlines))
        elif cmd == ":y":
            # :y <l> - yank line(s) to copy buffer
            cbuf = linebuffer[lstart:lend]
            editor._copy_buffer = cbuf
            caller.msg(_("{line}, {cbuf} yanked.").format(line=self.lstr.capitalize(), cbuf=cbuf))
        elif cmd == ":x":
            # :x <l> - cut line to copy buffer
            caller.msg(_("{line}, {cbuf} cut.").format(line=self.lstr.capitalize(), cbuf=cbuf))
        elif cmd == ":p":
            # :p <l> paste line(s) from copy buffer
            if not editor._copy_buffer:
                caller.msg(_("Copy buffer is empty."))
            else:
                buf = linebuffer[:lstart] + editor._copy_buffer + linebuffer[lstart:]
                    _("Pasted buffer {cbuf} to {line}.").format(
                        cbuf=editor._copy_buffer, line=self.lstr
                    )
                )
        elif cmd == ":i":
            # :i <l> <txt> - insert new line
            new_lines = self.args.split("\n")
            if not new_lines:
                caller.msg(_("You need to enter a new line and where to insert it."))
            else:
                buf = linebuffer[:lstart] + new_lines + linebuffer[lstart:]
                    _("Inserted {num} new line(s) at {line}.").format(
                        num=len(new_lines), line=self.lstr
                    )
                )
        elif cmd == ":r":
            # :r <l> <txt> - replace lines
                caller.msg(_("You need to enter a replacement string."))
            else:
                buf = linebuffer[:lstart] + new_lines + linebuffer[lend:]
                    _("Replaced {num} line(s) at {line}.").format(
                    )
                )
        elif cmd == ":I":
            # :I <l> <txt> - insert text at beginning of line(s) <l>
            if not self.raw_string and not editor._codefunc:
                caller.msg(_("You need to enter text to insert."))
            else:
                buf = (
                    linebuffer[:lstart]
                    + ["%s%s" % (self.args, line) for line in linebuffer[lstart:lend]]
                    + linebuffer[lend:]
                )
                caller.msg(_("Inserted text at beginning of {line}.").format(line=self.lstr))
        elif cmd == ":A":
            # :A <l> <txt> - append text after end of line(s)
                caller.msg(_("You need to enter text to append."))
            else:
                buf = (
                    + ["%s%s" % (line, self.args) for line in linebuffer[lstart:lend]]
                )
                caller.msg(_("Appended text to end of {line}.").format(line=self.lstr))
        elif cmd == ":s":
            # :s <li> <w> <txt> - search and replace words
            # in entire buffer or on certain lines
            if not self.arg1 or not self.arg2:
                caller.msg(_("You must give a search word and something to replace it with."))
            else:
                    lstart = 0
                        _("Search-replaced {arg1} -> {arg2} for lines {l1}-{l2}.").format(
                            arg1=self.arg1, arg2=self.arg2, l1=lstart + 1, l2=lend
                        )
                    )
                else:
                        _("Search-replaced {arg1} -> {arg2} for {line}.").format(
                            arg1=self.arg1, arg2=self.arg2, line=self.lstr
                        )
                    )

                regex = r"%s|^%s(?=\s)|(?<=\s)%s(?=\s)|^%s$|(?<=\s)%s$"
                regarg = self.arg1.strip("'").strip('"')
                if " " in regarg:
                    regarg = regarg.replace(" ", " +")
                sarea = re.sub(
                    regex % (regarg, regarg, regarg, regarg, regarg),
                    self.arg2.strip("'").strip('"'),
                    sarea,
                    re.MULTILINE,
                )
        elif cmd == ":f":
            # :f <l> flood-fill buffer or <l> lines of buffer.
            width = _DEFAULT_WIDTH
            if not self.linerange:
                lstart = 0
                lend = self.cline + 1
                caller.msg(_("Flood filled lines {l1}-{l2}.").format(l1=lstart + 1, l2=lend))
            else:
                caller.msg(_("Flood filled {line}.").format(line=self.lstr))
            fbuf = "\n".join(linebuffer[lstart:lend])
            fbuf = fill(fbuf, width=width)
            buf = linebuffer[:lstart] + fbuf.split("\n") + linebuffer[lend:]
        elif cmd == ":j":
            # :f <l> <w>  justify buffer of <l> with <w> as align (one of
            # f(ull), c(enter), r(ight) or l(left). Default is full.
            align_map = {
                "full": "f",
                "f": "f",
                "center": "c",
                "c": "c",
                "right": "r",
                "r": "r",
                "left": "l",
                "l": "l",
            }
            align_name = {"f": "Full", "c": "Center", "l": "Left", "r": "Right"}
            if self.arg1 and self.arg1.lower() not in align_map:
                    _("Valid justifications are")
                    + " [f]ull (default), [c]enter, [r]right or [l]eft"
                )
                return
            align = align_map[self.arg1.lower()] if self.arg1 else "f"
                lstart = 0
                    _("{align}-justified lines {l1}-{l2}.").format(
                        align=align_name[align], l1=lstart + 1, l2=lend
                    )
                )
            else:
                    _("{align}-justified {line}.").format(align=align_name[align], line=self.lstr)
                )
            jbuf = "\n".join(linebuffer[lstart:lend])
            jbuf = justify(jbuf, width=width, align=align)
            buf = linebuffer[:lstart] + jbuf.split("\n") + linebuffer[lend:]
        elif cmd == ":fi":
            # :fi <l> indent buffer or lines <l> of buffer.
            indent = " " * 4
                lstart = 0
                caller.msg(_("Indented lines {l1}-{l2}.").format(l1=lstart + 1, l2=lend))
            else:
                caller.msg(_("Indented {line}.").format(line=self.lstr))
            fbuf = [indent + line for line in linebuffer[lstart:lend]]
            buf = linebuffer[:lstart] + fbuf + linebuffer[lend:]
        elif cmd == ":fd":
                lstart = 0
                    _("Removed left margin (dedented) lines {l1}-{l2}.").format(
                        l1=lstart + 1, l2=lend
                    )
                )
            else:
                caller.msg(_("Removed left margin (dedented) {line}.").format(line=self.lstr))
            fbuf = dedent(fbuf)
        elif cmd == ":echo":
            # set echoing on/off
            editor._echo_mode = not editor._echo_mode
            caller.msg(_("Echo mode set to {mode}").format(mode=editor._echo_mode))
        elif cmd == ":!":
                editor._codefunc(caller, editor._buffer)
            else:
                caller.msg(_("This command is only available in code editor mode."))
        elif cmd == ":<":
            # :<
                editor.decrease_indent()
                if indent >= 0:
                        _("Decreased indentation: new indentation is {indent}.").format(
                            indent=indent
                        )
                    )
                else:
                    caller.msg(_("|rManual indentation is OFF.|n Use := to turn it on."))
            else:
        elif cmd == ":>":
            # :>
                editor.increase_indent()
                        _("Increased indentation: new indentation is {indent}.").format(
                        )
                    )
                else:
            else:
        elif cmd == ":=":
            # :=
                editor.swap_autoindent()
                    caller.msg(_("Auto-indentation turned on."))
                else:
                    caller.msg(_("Auto-indentation turned off."))
            else:

class EvEditorCmdSet(CmdSet):
    """CmdSet for the editor commands"""

    key = "editorcmdset"

        self.add(CmdLineInput())
        self.add(CmdEditorGroup())

#
# Main Editor object
#

class EvEditor:
    """
    This defines a line editor object. It creates all relevant commands
    and tracks the current state of the buffer. It also cleans up after
    itself.

    """

        self,
        caller,
        loadfunc=None,
        savefunc=None,
        quitfunc=None,
        key="",
        codefunc=False,
    ):
        """
        Launches a full in-game line editor, mimicking the functionality of VIM.

        Args:
            caller (Object): Who is using the editor.
            loadfunc (callable, optional): This will be called as
                `loadfunc(caller)` when the editor is first started. Its
                return will be used as the editor's starting buffer.
            savefunc (callable, optional): This will be called as
                `savefunc(caller, buffer)` when the save-command is given and
                is used to actually determine where/how result is saved.
                It should return `True` if save was successful and also
                handle any feedback to the user.
            quitfunc (callable, optional): This will optionally be
                called as `quitfunc(caller)` when the editor is
                exited. If defined, it should handle all wanted feedback
                to the user.
            quitfunc_args (tuple, optional): Optional tuple of arguments to
                supply to `quitfunc`.
            key (str, optional): An optional key for naming this
                session and make it unique from other editing sessions.
            persistent (bool, optional): Make the editor survive a reboot. Note
                that if this is set, all callables must be possible to pickle
            codefunc (bool, optional): If given, will run the editor in code mode.
                This will be called as `codefunc(caller, buf)`.

        Notes:
            In persistent mode, all the input callables (savefunc etc)
            must be possible to be *pickled*, this excludes e.g.
            callables that are class methods or functions defined
            dynamically or as part of another function. In
            non-persistent mode no such restrictions exist.

        """
        self._caller = caller
        self._caller.ndb._eveditor = self
        self._buffer = ""
        self._unsaved = False
        self._persistent = persistent
        self._indent = 0

        if loadfunc:
            self._loadfunc = loadfunc
        else:
            self._loadfunc = lambda caller: self._buffer
        self.load_buffer()
        if savefunc:
            self._savefunc = savefunc
        else:
            self._savefunc = lambda caller, buffer: caller.msg(_ERROR_NO_SAVEFUNC)
        if quitfunc:
            self._quitfunc = quitfunc
        else:
            self._quitfunc = lambda caller: caller.msg(_DEFAULT_NO_QUITFUNC)
        self._codefunc = codefunc

        # store the original version
        self._pristine_buffer = self._buffer
        self._sep = "-"

        # undo operation buffer
        self._undo_buffer = [self._buffer]
        self._undo_pos = 0
        self._undo_max = 20

        # copy buffer
        self._copy_buffer = []

            # save in tuple {kwargs, other options}
            try:
                caller.attributes.add(
                    "_eveditor_saved",
                    (
                        dict(
                            loadfunc=loadfunc,
                            savefunc=savefunc,
                            quitfunc=quitfunc,
                            codefunc=codefunc,
                            key=key,
                            persistent=persistent,
                        ),
                        dict(_pristine_buffer=self._pristine_buffer, _sep=self._sep),
                    ),
                )
                caller.attributes.add("_eveditor_buffer_temp", (self._buffer, self._undo_buffer))
                caller.attributes.add("_eveditor_unsaved", False)
                caller.attributes.add("_eveditor_indent", 0)
                caller.msg(_ERROR_PERSISTENT_SAVING.format(error=err))
                logger.log_trace(_TRACE_PERSISTENT_SAVING)
                persistent = False

        # Create the commands we need
        caller.cmdset.add(EvEditorCmdSet, persistent=persistent)

        # echo inserted text back to caller
        self._echo_mode = True

        # show the buffer ui
        self.display_buffer()

    def load_buffer(self):
        """
        Load the buffer using the load function hook.

        """
        try:
            self._buffer = self._loadfunc(self._caller)
            if not isinstance(self._buffer, str):
                self._caller.msg(
                    f"|rBuffer is of type |w{type(self._buffer)})|r. "
                    "Continuing, it is converted to a string "
                    "(and will be saved as such)!|n"
                )
                self._buffer = to_str(self._buffer)

            self._caller.msg(_ERROR_LOADFUNC.format(error=e))

    def get_buffer(self):
        """
        Return:
            buffer (str): The current buffer.

        """
        return self._buffer

    def update_buffer(self, buf):
        """
        This should be called when the buffer has been changed
        somehow.  It will handle unsaved flag and undo updating.

        Args:
            buf (str): The text to update the buffer with.

        """
        if is_iter(buf):
            buf = "\n".join(buf)

        if buf != self._buffer:
            self._buffer = buf
            self.update_undo()
            self._unsaved = True
            if self._persistent:
                self._caller.attributes.add(
                    "_eveditor_buffer_temp", (self._buffer, self._undo_buffer)
                )
                self._caller.attributes.add("_eveditor_unsaved", True)
                self._caller.attributes.add("_eveditor_indent", self._indent)

    def quit(self):
        """
        Cleanly exit the editor.

        """
        try:
            self._quitfunc(self._caller)
            self._caller.msg(_ERROR_QUITFUNC.format(error=e))
        self._caller.nattributes.remove("_eveditor")
        self._caller.attributes.remove("_eveditor_buffer_temp")
        self._caller.attributes.remove("_eveditor_saved")
        self._caller.attributes.remove("_eveditor_unsaved")
        self._caller.attributes.remove("_eveditor_indent")
        self._caller.cmdset.remove(EvEditorCmdSet)

    def save_buffer(self):
        """
        Saves the content of the buffer.

        """
        if self._unsaved or self._codefunc:
            # always save code - this allows us to tie execution to
            # saving if we want.
            try:
                if self._savefunc(self._caller, self._buffer):
                    # Save codes should return a true value to indicate
                    # save worked. The saving function is responsible for
                    # any status messages.
                    self._unsaved = False
                self._caller.msg(_ERROR_SAVEFUNC.format(error=e))
        else:
            self._caller.msg(_MSG_SAVE_NO_CHANGE)

    def update_undo(self, step=None):
        """
        This updates the undo position.

        Args:
            step (int, optional): The amount of steps
                to progress the undo position to. This
                may be a negative value for undo and
                a positive value for redo.

        """
        if step and step < 0:
            # undo
            if self._undo_pos <= 0:
                self._caller.msg(_MSG_NO_UNDO)
            else:
                self._undo_pos = max(0, self._undo_pos + step)
                self._buffer = self._undo_buffer[self._undo_pos]
                self._caller.msg(_MSG_UNDO)
        elif step and step > 0:
            # redo
            if self._undo_pos >= len(self._undo_buffer) - 1 or self._undo_pos + 1 >= self._undo_max:
                self._caller.msg(_MSG_NO_REDO)
            else:
                self._undo_pos = min(
                    self._undo_pos + step, min(len(self._undo_buffer), self._undo_max) - 1
                )
                self._caller.msg(_MSG_REDO)
        if not self._undo_buffer or (
            self._undo_buffer and self._buffer != self._undo_buffer[self._undo_pos]
        ):
            # save undo state
            self._undo_buffer = self._undo_buffer[: self._undo_pos + 1] + [self._buffer]
            self._undo_pos = len(self._undo_buffer) - 1

    def display_buffer(self, buf=None, offset=0, linenums=True, options={"raw": False}):
        """
        This displays the line editor buffer, or selected parts of it.

        Args:
            buf (str, optional): The buffer or part of buffer to display.
            offset (int, optional): If `buf` is set and is not the full buffer,
                `offset` should define the actual starting line number, to
                get the linenum display right.
            linenums (bool, optional): Show line numbers in buffer.
            options: raw (bool, optional): Tell protocol to not parse
                formatting information.

        """
        if buf is None:
            buf = self._buffer

        lines = buf.split("\n")
        nlines = len(lines)
        nwords = len(buf.split())
        nchars = len(buf)

        sep = self._sep
        header = (
            "|n"
            + sep * 10
            + _("Line Editor [{name}]").format(name=self._key)
            + sep * (_DEFAULT_WIDTH - 24 - len(self._key))
        )
        footer = (
            "|n"
            + sep * 10
            + "[l:%02i w:%03i c:%04i]" % (nlines, nwords, nchars)
            + sep * 12
            + _("(:h for help)")
            + sep * (_DEFAULT_WIDTH - 54)
        )
        if linenums:
            main = "\n".join(
                "|b%02i|||n %s" % (iline + 1 + offset, raw(line))
                for iline, line in enumerate(lines)
            )
        else:
            main = "\n".join([raw(line) for line in lines])
        string = "%s\n%s\n%s" % (header, main, footer)
        self._caller.msg(string, options=options)

    def display_help(self):
        """
        Shows the help entry for the editor.

        """
        string = self._sep * _DEFAULT_WIDTH + _HELP_TEXT
        if self._codefunc:
            string += _HELP_CODE
        string += _HELP_LEGEND + self._sep * _DEFAULT_WIDTH
        self._caller.msg(string)

    def deduce_indent(self, line, buffer):
        """
        Try to deduce the level of indentation of the given line.

        """
        keywords = {
            "elif ": ["if "],
            "else:": ["if ", "try"],
            "except": ["try:"],
            "finally:": ["try:"],
        }
        opening_tags = ("if ", "try:", "for ", "while ")

        # If the line begins by one of the given keywords
        indent = self._indent
        if any(line.startswith(kw) for kw in keywords.keys()):
            # Get the keyword and matching begin tags
            keyword = [kw for kw in keywords if line.startswith(kw)][0]
            begin_tags = keywords[keyword]
            for oline in reversed(buffer.splitlines()):
                if any(oline.lstrip(" ").startswith(tag) for tag in begin_tags):
                    # This line begins with a begin tag, takes the identation
                    indent = (len(oline) - len(oline.lstrip(" "))) / 4
                    break

            self._indent = indent + 1
        elif any(line.startswith(kw) for kw in opening_tags):

        line = " " * 4 * indent + line
        return line

    def decrease_indent(self):
        """Decrease automatic indentation by 1 level."""
        if self._codefunc and self._indent > 0:
            self._indent -= 1

    def increase_indent(self):
        """Increase automatic indentation by 1 level."""
        if self._codefunc and self._indent >= 0:
            self._indent += 1

    def swap_autoindent(self):
        """Swap automatic indentation on or off."""
            if self._indent >= 0:
                self._indent = -1
            else:
                self._indent = 0

# coding=utf-8
"""
EvForm - a way to create advanced ASCII forms

This is intended for creating advanced ASCII game forms, such as a large pretty character sheet or
info document.

The system works on the basis of a readin template that is given in a separate Python file imported
into the handler. This file contains some optional settings and a string mapping out the form. The
template has markers in it to denounce fields to fill. The markers map the absolute size of the
field and will be filled with an `evtable.EvCell` object when displaying the form.

Example of input file `testform.py`:

```python
FORMCHAR = "x"
TABLECHAR = "c"

FORM = '''
.------------------------------------------------.
|                                                |
|  Name: xxxxx1xxxxx    Player: xxxxxxx2xxxxxxx  |
|        xxxxxxxxxxx                             |
|                                                |
 >----------------------------------------------<
|                                                |
| Desc:  xxxxxxxxxxx    STR: x4x    DEX: x5x     |
|        xxxxx3xxxxx    INT: x6x    STA: x7x     |
|        xxxxxxxxxxx    LUC: x8x    MAG: x9x     |
|                                                |
|          |                                     |
| cccccccc | ccccccccccccccccccccccccccccccccccc |
| cccAcccc | ccccccccccccccccccccccccccccccccccc |
| cccccccc | cccccccccccccccccBccccccccccccccccc |
|          |                                     |
|                                           v&   |
-------------------------------------------------
'''
```

The first line of the `FORM` string is ignored if empty. The forms and table markers must mark out
complete, unbroken rectangles, each containing one embedded single-character identifier (so the
smallest element possible is a 3-character wide form). The identifier can be any character except
for the `FORM_CHAR` and `TABLE_CHAR` and some of the common ASCII-art elements, like space, `_` `|`
`*` etc (see `INVALID_FORMCHARS` in this module). Form Rectangles can have any size, but must be
separated from each other by at least one other character's width.

The form can also replace literal markers not abiding by these rules. For example, the `v&` in the
bottom right corner could be such literal marker. If a literal-mapping for 'v&' is provided, all
occurrences of this marker will be replaced. This will happen *before* any other parsing, so in
principle this could be used to inject new fields/tables into the form dynamically.  This literal
mapping does not consider width, but it will affect to total width of the form, so make sure what
you inject does not break things.  Using literal markers is the only way to inject 1 or 2-character
replacements.

Usage

```python
from evennia import EvForm, EvTable

# create a new form from the template - using the python path
form = EvForm("path.to.testform")

# alteratively, you can supply the template as a dict:

form = EvForm({"FORM": "....", "TABLECHAR": "c", "FORMCHAR": "x"})

# EvForm can also take a dictionary instead of a filepath, as long
# as the dict contains the keys FORMCHAR, TABLECHAR and FORM
# form = EvForm(form=form_dict)

# add data to each tagged form cell
form.map(cells={1: "Tom the Bouncer",
                2: "Griatch",
                3: "A sturdy fellow",
                4: 12,
                5: 10,
                6:  5,
                7: 18,
                8: 10,
                9:  3})
# create the EvTables
tableA = EvTable("HP","MV","MP",
                           table=[["**"], ["*****"], ["***"]],
                           border="incols")
tableB = EvTable("Skill", "Value", "Exp",
                           table=[["Shooting", "Herbalism", "Smithing"],
                                  [12,14,9],["550/1200", "990/1400", "205/900"]],
# map 'literal' replacents (here, a version string)
custom_mapping = {"v&", "v2"}

# add the tables to the proper ids in the form
form.map(tables={"A": tableA,
                 "B": tableB})

print(form)
```

This produces the following result:

::

    .------------------------------------------------.
    |                                                |
    |  Name: Tom the        Player: Griatch          |
    |        Bouncer                                 |
    |                                                |
     >----------------------------------------------<
    |                                                |
    | Desc:  A sturdy       STR: 12     DEX: 10      |
    |        fellow         INT: 5      STA: 18      |
    |                       LUC: 10     MAG: 3       |
    |                                                |
    |          |                                     |
    | HP|MV|MP | Skill      |Value      |Exp         |
    | ~~+~~+~~ | ~~~~~~~~~~~+~~~~~~~~~~~+~~~~~~~~~~~ |
    | **|**|** | Shooting   |12         |550/1200    |
    |   |**|*  | Herbalism  |14         |990/1400    |
    |   |* |   | Smithing   |9          |205/900     |
    |          |                                     |
    |                                           v2   |
     ------------------------------------------------

The marked forms have been replaced with EvCells of text and with EvTables. The literal marker `v&`
was replaced with `v2`.

If you change the form layout on disk, you can use `form.reload()` to re-read it from disk without
creating a new form.

If you want to update the data of an existing form, you can use `form.map()` with the changes - the
mappings will be updated, keeping the things you want. You can also update the template itself this
way, by supplying it as a dict.

Each component (except literal mappings) is restrained to the width and height specified by the
template, so it will resize to fit (or crop text if the area is too small for it). If you try to fit
a table into an area it cannot fit into (when including its borders and at least one line of text),
the form will raise an error.

----

"""

import re

from evennia.utils.evtable import EvCell, EvTable
from evennia.utils.utils import all_from_module, is_iter, to_str

# non-valid form-identifying characters (which can thus be
# used as separators between forms without being detected
# as an identifier). These should be listed in regex form.
INVALID_FORMCHARS = r"\s\/\|\\\*\_\-\#\<\>\~\^\:\;\.\,"
# if there is an ansi-escape (||) we have to replace this with ||| to make sure
# to properly escape down the line
_ANSI_ESCAPE = re.compile(r"\|\|")

class EvForm:
    """
    This object is instantiated with a text file and parses
    it for rectangular form fields. It can then be fed a
    mapping so as to populate the fields with fixed-width
    EvCell or Tables.

    """

    # cell option defaults
    cell_options = {
        "pad_left": 0,
        "pad_right": 0,
        "pad_top": 0,
        "pad_bottom": 0,
        "align": "l",
        "valign": "t",
        "enforce_size": True,
    }

    # table option defaults
    table_options = {
    }

    def __init__(self, data=None, cells=None, tables=None, literals=None, **kwargs):
        """
        Initiate the form

            data (str or dict): Path to template file or a dict with
                "formchar", "tablechar" and "form" keys (not case sensitive, so FORM etc
                also works, to stay compatible with the in-file names). While "form/FORM"
                is required, if FORMCHAR/TABLECHAR are not given, they will default to
                'x' and 'c' respectively.
            cells (dict): A dictionary mapping  `{id: str}`
            tables (dict): A dictionary mapping  `{id: EvTable}`.
            literals (dict): A dictionary mapping `{id: str}`. Will be replaced
                after width of form is calculated, but before cells/tables are mapped.
                All occurrences of the identifier on the form will be replaced. Note
                that there is no length-restriction on the remap, you are responsible
                for not breaking the form.

        Notes:
            Other kwargs are fed as options to the EvCells and EvTables
            (see `evtable.EvCell` and `evtable.EvTable` for more info).

        """
        self.indata = data  # storing here so we can reload later in case of a filename
        self.options = self._parse_inkwargs(**kwargs)

        self.cells_mapping = (
            dict((to_str(key), value) for key, value in cells.items()) if cells else {}
        )
        self.tables_mapping = (
            dict((to_str(key), value) for key, value in tables.items()) if tables else {}
        )
        self.literals_mapping = (
            dict((to_str(key), to_str(value)) for key, value in literals.items())
            if literals
            else {}
        )

        # work arrays
        self.literal_form = ""
        self.mapping = {}
        self.matrix = []
        self.form = []

        # will parse and build the form

    def _parse_indata(self):
        """
        Parse and validate the `self.indata` property. We do this in order to be able to
        re-load the evform module if indata is a filename and catch any on-file changes.

        Returns:
            dict: The data dict parsed/generated from the in-data.

        """
        data = self.indata

        default_formchar = "x"
        default_tablechar = "c"

        if isinstance(data, str):
            # a module path - read all variables from it
            data = all_from_module(data)

        if isinstance(data, dict):
            data = {
                "form": str(data.get("form", data.get("FORM", None))),
                "formchar": str(data.get("formchar", data.get("FORMCHAR", default_formchar))),
                "tablechar": str(data.get("tablechar", data.get("TABLECHAR", default_tablechar))),
            }
        else:
            raise RuntimeError(f"EvForm invalid input: {data}.")

        if not data or data["form"] is None:
            raise RuntimeError("Evform data must specify a valid 'form' or 'FORM'.")

        # handle empty or multi-character form/tablechars (not supported)
        data["formchar"] = data["formchar"][0] if data["formchar"] else default_formchar
        data["tablechar"] = data["tablechar"][0] if data["tablechar"] else default_tablechar
        if re.match(rf"[{INVALID_FORMCHARS}]", data["formchar"]):
            raise RuntimeError(f"Invalid formchar: {data['formchar']}")
        if re.match(rf"[{INVALID_FORMCHARS}]", data["tablechar"]):
            raise RuntimeError(f"Invalid tablechar: {data['tablechar']}")

        return data

    def _parse_inkwargs(self, **kwargs):
        """
        Validate incoming kwargs that will be passed on to become cell/table options.

            any: Kwargs to process.

        Returns:
            dict: A validated/cleaned kwarg to use for options.

        """
        if "filename" in kwargs:
                "EvForm's 'filename' kwarg was renamed to 'data' and can now accept both "
                "a python path and a dict with 'FORMCHAR', 'TABLECHAR' and 'FORM' keys."
            )
        if "form" in kwargs:
                "EvForms's 'form' kwarg was renamed to 'data' and can now accept both "
                "a python path and a dict detailing the form."
            )

        # clean cell kwarg options (these cannot be overridden on the cell but must be controlled
        # by the evform itself)
        kwargs.pop("enforce_size", None)
        kwargs.pop("width", None)
        kwargs.pop("height", None)

        return kwargs

    def _do_literal_mapping(self):
        """
        Do literal replacement in the EvForm.

        """
        literal_form = copy(self.data["form"])

        for key, repl in self.literals_mapping.items():
            literal_form = literal_form.replace(key, repl)
        return literal_form

    def _parse_to_matrix(self):
        """
        Forces all lines to be as long as the longest line, filling with whitespace.

        Args:
            lines (list): list of `ANSIString`s

        Returns:
            (list): list of `ANSIString`s of
            same length as the longest input line

        """
        matrix = EvForm._to_ansi(self.literal_form.split("\n"))

        maxl = max(len(line) for line in matrix)
        matrix = [line + " " * (maxl - len(line)) for line in matrix]
        if matrix and not matrix[0].strip():
            # the first line is normally empty, we strip it.
            matrix = matrix[1:]
        return matrix

    def _to_ansi(obj, regexable=False):
        "convert anything to ANSIString"

        if isinstance(obj, ANSIString):
            return obj
        elif isinstance(obj, str):
            # since ansi will be parsed twice (here and in the normal ansi send), we have to
            # escape ansi twice.
            obj = ansi_raw(obj)

        if isinstance(obj, dict):
                (key, EvForm._to_ansi(value, regexable=regexable)) for key, value in obj.items()
            )
        # regular _to_ansi (from EvTable)
            return [EvForm._to_ansi(o) for o in obj]
        else:
            return ANSIString(obj, regexable=regexable)

    def _rectangles_to_mapping(self):
        """
        Parse a form for rectangular formfields identified by formchar/tablechar enclosing an
        identifier.

        """
        formchar = self.data["formchar"]
        tablechar = self.data["tablechar"]
        matrix = self.matrix

        cell_options = copy(self.cell_options)
        cell_options.update(self.options)

        table_options = copy(self.table_options)
        table_options.update(self.options)

        nmatrix = len(matrix)

        mapping = {}

        def _get_rectangles(char):
            """Find all identified rectangles marked with given char"""
            rects = []
            coords = {}
            regex = re.compile(rf"{char}+([^{INVALID_FORMCHARS}{char}]+){char}+")

            # find the start/width of rectangles for each line
            for iy, line in enumerate(EvForm._to_ansi(matrix, regexable=True)):
                ix0 = 0
                while True:
                    match = regex.search(line, ix0)
                    if match:
                        # get the width of the rectangle directly from the match
                        coords[match.group(1)] = [iy, match.start(), match.end()]
                        ix0 = match.end()
                    else:
                        break

            for key, (iy, leftix, rightix) in coords.items():
                # scan up to find top of rectangle
                dy_up = 0
                if iy > 0:
                    for i in range(1, iy):
                        if all(matrix[iy - i][ix] == char for ix in range(leftix, rightix)):
                            dy_up += 1
                        else:
                            break
                # find bottom edge of rectangle
                dy_down = 0
                if iy < nmatrix - 1:
                    for i in range(1, nmatrix - iy - 1):
                        if all(matrix[iy + i][ix] == char for ix in range(leftix, rightix)):
                            dy_down += 1
                        else:
                            break

                #  we have our rectangle. Calculate size
                iyup = iy - dy_up
                iydown = iy + dy_down
                width = rightix - leftix
                height = abs(iyup - iydown) + 1

                # store (key, y, x, width, height) of triangle
                rects.append((key, iyup, leftix, width, height))

            return rects

        # Map EvCells into form rectangles
        for key, y, x, width, height in _get_rectangles(formchar):
            # get data to populate cell
            data = self.cells_mapping.get(key, "")
            if isinstance(data, EvCell):
                # mapping already provides the cell. We need to override some
                # of the cell's options to make it work in the evform rectangle.
                # We retain the align/valign since this may be interesting to
                # play with within the rectangle.
                cell = data
                custom_align = cell.align
                custom_valign = cell.valign
                cell.reformat(
                    width=width,
                    height=height,
                    **{**cell_options, **{"align": custom_align, "valign": custom_valign}},
                )
            else:
                # generating cell on the fly
                cell = EvCell(data, width=width, height=height, **cell_options)

            mapping[key] = (y, x, width, height, cell)

        # Map EvTables into form rectangles
        for key, y, x, width, height in _get_rectangles(tablechar):
            # get EvTable from mapping
            table = self.tables_mapping.get(key, None)

            if table:
                table.reformat(width=width, height=height, **table_options)
            else:
                table = EvTable(width=width, height=height, **table_options)

            mapping[key] = (y, x, width, height, table)

        return mapping

    def _build_form(self):
        """
        Insert cell/table contents into form at given locations to create
        the final result.

        """
        form = copy(self.matrix)
        mapping = self.mapping

        for key, (y, x, width, height, cell_or_table) in mapping.items():
            # rect is a list of <height> lines, each <width> wide
            rect = cell_or_table.get()
            for il, rectline in enumerate(rect):
                formline = form[y + il]
                # insert new content, replacing old
                form[y + il] = formline[:x] + rectline + formline[x + width :]

        return form

        """
        Creates the form from a filename or data structure.

        Args:
            data (str or dict): Can be used to update an existing form using
                the same cells/tables provided on initialization or using `.map()`.

        Notes:
            Kwargs are passed through to Cel creation.

        """
        self.data = self._parse_indata()

        # Map any literals into the string
        self.literal_form = self._do_literal_mapping()
        # Create raw form matrix, indexable with (y, x) coords
        self.matrix = self._parse_to_matrix()
        # parse and identify all rectangles in the form
        self.mapping = self._rectangles_to_mapping()
        # combine mapping with form template into a final result
        self.form = self._build_form()

    def map(self, cells=None, tables=None, data=None, literals=None, **kwargs):
        """
        Add mapping for form. This allows for updating an existing
        evform.

        Args:
            cells (dict): A dictionary of {identifier:celltext}. These
                will be appended to the existing mappings.
            tables (dict): A dictionary of {identifier:table}. Will
                be appended to the existing mapping.
            data (str or dict): A path to a evform module or a dict with
                the needed "FORM", "TABLE/FORMCHAR" keys. Will replace
                the originally initialized form.
            literals

            These will be appended to the existing cell/table options.

        Notes:
            kwargs will be forwarded to tables/cells. See
            `evtable.EvCell` and `evtable.EvTable` for info.

        """
        if data:
            # storing so ._parse_indata will find it during reload
            self.indata = data

        new_cells = dict((to_str(key), value) for key, value in cells.items()) if cells else {}
        self.cells_mapping.update(new_cells)
        new_tables = dict((to_str(key), value) for key, value in tables.items()) if tables else {}
        self.tables_mapping.update(new_tables)
        new_literals = (
            else {}
        )
        self.literals_mapping.update(new_literals)

        self.options.update(self._parse_inkwargs(**kwargs))

        # parse and build the form

        "Prints the form"
        return str(ANSIString("\n").join([line for line in self.form]))

"""
EvMenu

This implements a full menu system for Evennia.

To start the menu, just import the EvMenu class from this module.
Example usage:

```python

    from evennia.utils.evmenu import EvMenu

    EvMenu(caller, menu_module_path,
         startnode="node1",
         cmdset_mergetype="Replace", cmdset_priority=1,
         auto_quit=True, cmd_on_exit="look", persistent=True)
```

Where `caller` is the Object to use the menu on - it will get a new
cmdset while using the Menu. The menu_module_path is the python path
to a python module containing function definitions.  By adjusting the
keyword options of the Menu() initialization call you can start the
menu at different places in the menu definition file, adjust if the
menu command should overload the normal commands or not, etc.

The `persistent` keyword will make the menu survive a server reboot.
It is `False` by default. Note that if using persistent mode, every
node and callback in the menu must be possible to be *pickled*, this
excludes e.g. callables that are class methods or functions defined
dynamically or as part of another function. In non-persistent mode
no such restrictions exist.

The menu is defined in a module (this can be the same module as the
command definition too) with function definitions:

```python

    def node1(caller):
        # (this is the start node if called like above)
        # code

    def node_with_other_name(caller, input_string):
        # code

    def another_node(caller, input_string, **kwargs):
        # code
```

Where caller is the object using the menu and input_string is the
command entered by the user on the *previous* node (the command
entered to get to this node). The node function code will only be
executed once per node-visit and the system will accept nodes with
both one or two arguments interchangeably. It also accepts nodes
that takes `**kwargs`.

The menu tree itself is available on the caller as
`caller.ndb._evmenu`. This makes it a convenient place to store
temporary state variables between nodes, since this NAttribute is
deleted when the menu is exited.

The return values must be given in the above order, but each can be
returned as None as well. If the options are returned as None, the
menu is immediately exited and the default "look" command is called.

- `text` (str, tuple or None): Text shown at this node. If a tuple, the
   second element in the tuple holds either a string or a dict. If a string,
   this is the help text to show when `auto_help` is active for the menu and
   the user presses `h`. If a dict, this is a mapping of `'help topic': 'help text'` to
   show in that menu. This can be used to show information without having to
   switch to another node.
- `options` (tuple, dict or None): If `None`, this exits the menu.
  If a single dict, this is a single-option node. If a tuple,
  it should be a tuple of option dictionaries. Option dicts have the following keys:

  - `key` (str or tuple, optional): What to enter to choose this option.
    If a tuple, it must be a tuple of strings, where the first string is the
    key which will be shown to the user and the others are aliases.
    If unset, the options' number will be used. The special key `_default`
    marks this option as the default fallback when no other option matches
    the user input. There can only be one `_default` option per node. It
    will not be displayed in the list.
  - `desc` (str, optional): This describes what choosing the option will do.
  - `goto` (str, tuple or callable): If string, should be the name of node to go to
    when this option is selected. If a callable, it has the signature
    `callable(caller[,raw_input][,**kwargs])`. If a tuple, the first element
    is the callable and the second is a dict with the `**kwargs` to pass to
    the callable. Those kwargs will also be passed into the next node if possible.
    Such a callable should return either a str or a (str, dict), where the
    string is the name of the next node to go to and the dict is the new,
    (possibly modified) kwarg to pass into the next node. If the callable returns
    None or the empty string, the current node will be revisited.

If `key` is not given, the option will automatically be identified by
its number 1..N.

Example:

```python

    # in menu_module.py

        text = ("This is a node text",
                "This is help text for this node")
        options = ({"key": "testing",
                    "desc": "Select this to go to node 2",
                    "goto": ("node2", {"foo": "bar"}),
                   {"desc": "Go to node 3.",
                    "goto": "node3"})

    def callback1(caller):
        # this is called when choosing the "testing" option in node1
        # (before going to node2). If it returned a string, say 'node3',
        # then the next node would be node3 instead of node2 as specified
        # by the normal 'goto' option key above.
        caller.msg("Callback called!")

    def node2(caller, **kwargs):
        text = '''
            This is node 2. It only allows you to go back
            to the original node1. This extra indent will
            be stripped. We don't include a help text but
            here are the variables passed to us: {}
            '''.format(kwargs)
        options = {"goto": "node1"}

    def node3(caller):
        text = "This ends the menu since there are no options."
        return text, None

```

When starting this menu with  `Menu(caller, "path.to.menu_module")`,
the first node will look something like this:

::

    This is a node text
    ______________________________________

    testing: Select this to go to node 2
    2: Go to node 3

Where you can both enter "testing" and "1" to select the first option.
If the client supports MXP, they may also mouse-click on "testing" to
do the same. When making this selection, a function "callback1" in the
same Using `help` will show the help text, otherwise a list of
available commands while in menu mode.

The menu tree is exited either by using the in-menu quit command or by
reaching a node without any options.

For a menu demo, import `CmdTestMenu` from this module and add it to
your default cmdset. Run it with this module, like `testmenu evennia.utils.evmenu`.

## Menu generation from template string

In evmenu.py is a helper function `parse_menu_template` that parses a
template-string and outputs a menu-tree dictionary suitable to pass into
EvMenu:
::

    menutree = evmenu.parse_menu_template(caller, menu_template, goto_callables)
    EvMenu(caller, menutree)

For maximum flexibility you can inject normally-created nodes in the menu tree
before passing it to EvMenu. If that's not needed, you can also create a menu
in one step with:

```python

    evmenu.template2menu(caller, menu_template, goto_callables)

```

The `goto_callables` is a mapping `{"funcname": callable, ...}`, where each
callable must be a module-global function on the form
`funcname(caller, raw_string, **kwargs)` (like any goto-callable). The
`menu_template` is a multi-line string on the following form:
::

    ## node start

    This is the text of the start node.
    The text area can have multiple lines, line breaks etc.

    Each option below is one of these forms
        key: desc -> gotostr_or_func
        key: gotostr_or_func
        >: gotostr_or_func
        > glob/regex: gotostr_or_func

    ## options

        # comments are only allowed from beginning of line.
        # Indenting is not necessary, but good for readability

        1: Option number 1 -> node1
        2: Option number 2 -> node2
        next: This steps next -> go_back()
        # the -> can be ignored if there is no desc
        back: go_back(from_node=start)
        abort: abort

    ## node node1

    Text for Node1. Enter a message!
    <return> to go back.

    ## options

        # Beginning the option-line with >
        # allows to perform different actions depending on
        # what is inserted.

        # this catches everything starting with foo
        > foo*: handle_foo_message()

        # regex are also allowed (this catches number inputs)
        > [0-9]+?: handle_numbers()

        # this catches the empty return
        >: start

        # this catches everything else
        > *: handle_message(from_node=node1)

    ## node node2

    Text for Node2. Just go back.

    ## options

        >: start

    ## node abort

    This exits the menu since there is no `## options` section.

Each menu node is defined by a `# node <name>` containing the text of the node,
followed by `## options` Also `## NODE` and `## OPTIONS` work. No python code
logics is allowed in the template, this code is not evaluated but parsed. More
advanced dynamic usage requires a full node-function (which can be added to the
generated dict, as said).

Adding `(..)` to a goto treats it as a callable and it must then be included in
the `goto_callable` mapping. Only named keywords (or no args at all) are
allowed, these will be added to the `**kwargs` going into the callable. Quoting
strings is only needed if wanting to pass strippable spaces, otherwise the
key:values will be converted to strings/numbers with literal_eval before passed
into the callable.

The \\> option takes a glob or regex to perform different actions depending
on user input. Make sure to sort these in increasing order of generality since
they will be tested in sequence.

----

"""

import re
from fnmatch import fnmatch
from inspect import getfullargspec, isfunction
from math import ceil

# i18n

from evennia.utils.evtable import EvColumn, EvTable
    crop,
    dedent,
    is_iter,
    m_len,
    pad,
    to_str,
)

# read from protocol NAWS later?

# Return messages

_ERR_NOT_IMPLEMENTED = _(
    "Menu node '{nodename}' is either not implemented or caused an error. "
    "Make another choice or try 'q' to abort."
)
_ERR_GENERAL = _("Error in menu node '{nodename}'.")
_ERR_NO_OPTION_DESC = _("No description.")
_HELP_FULL = _("Commands: <menu option>, help, quit")
_HELP_NO_QUIT = _("Commands: <menu option>, help")
_HELP_NO_OPTIONS = _("Commands: help, quit")
_HELP_NO_OPTIONS_NO_QUIT = _("Commands: help")
_HELP_NO_OPTION_MATCH = _("Choose an option or try 'help'.")

_ERROR_PERSISTENT_SAVING = """
{error}

|rThe menu state could not be saved for persistent mode. Switching
to non-persistent mode (which means the menu session won't survive
an eventual server reload).|n
"""

_TRACE_PERSISTENT_SAVING = (
    "EvMenu persistent-mode error. Commonly, this is because one or "
)

class EvMenuError(RuntimeError):
    """
    Error raised by menu when facing internal errors.

    """

    pass

class EvMenuGotoAbortMessage(RuntimeError):
    """
    This can be raised by a goto-callable to abort the goto flow.  The message
    stored with the executable will be sent to the caller who will remain on
    the current node. This can be used to pass single-line returns without
    re-running the entire node with text and options.

    Example:
        raise EvMenuGotoMessage("That makes no sense.")

    """

#
# Menu command and command set
#

class CmdEvMenuNode(Command):
    """
    Command to handle all user input targeted at the menu while the menu is active.

    """

    aliases = [_CMD_NOMATCH]
    help_category = "Menu"
    auto_help_display_key = "<menu commands>"

        return "Menu commands are explained within the menu."

    def _update_aliases(self, menu):
        """Add aliases to make sure to override defaults if we defined we want it."""

        new_aliases = [_CMD_NOMATCH]
        if menu.auto_quit and "quit" not in self.aliases:
            new_aliases.extend(["q", "quit"])
        if menu.auto_look and "look" not in self.aliases:
            new_aliases.extend(["l", "look"])
        if menu.auto_help and "help" not in self.aliases:
            new_aliases.extend(["h", "help"])
        if len(new_aliases) > 1:
            self.set_aliases(new_aliases)

        self.msg(f"aliases: {self.aliases}")

        """
        Implement all menu commands.
        """

        def _restore(caller):
            # check if there is a saved menu available.
            # this will re-start a completely new evmenu call.
            saved_options = caller.attributes.get("_menutree_saved")
            if saved_options:
                startnode_tuple = caller.attributes.get("_menutree_saved_startnode")
                try:
                    startnode, startnode_input = startnode_tuple
                except ValueError:  # old form of startnode store
                    startnode, startnode_input = startnode_tuple, ""
                if startnode:
                    saved_options[2]["startnode"] = startnode
                    saved_options[2]["startnode_input"] = startnode_input
                MenuClass = saved_options[0]
                # this will create a completely new menu call
                menu = MenuClass(caller, *saved_options[1], **saved_options[2])
                # self._update_aliases(menu)

        # we store Session on the menu since this can be hard to
        # get in multisession environments if caller is an Account.
        menu = caller.ndb._evmenu
        if not menu:
            if _restore(caller):
                return
            orig_caller = caller
            caller = caller.account if inherits_from(caller, evennia.DefaultObject) else None
            menu = caller.ndb._evmenu if caller else None
            if not menu:
                if caller and _restore(caller):
                    return
                caller = self.session
                menu = caller.ndb._evmenu
                if not menu:
                    # can't restore from a session
                    err = "Menu object not found as %s.ndb._evmenu!" % orig_caller
                    orig_caller.msg(
                        err
                    )  # don't give the session as a kwarg here, direct to original
                    raise EvMenuError(err)

        # self._update_aliases(menu)

        # we must do this after the caller with the menu has been correctly identified since it
        # can be either Account, Object or Session (in the latter case this info will be
        # superfluous).
        caller.ndb._evmenu._session = self.session
        # we have a menu, use it.
        menu.parse_input(self.raw_string)

class EvMenuCmdSet(CmdSet):
    """
    The Menu cmdset replaces the current cmdset.

    """

    key = "menu_cmdset"
    no_channels = False

        """
        Called when creating the set.
        """
        self.add(CmdEvMenuNode())

#
# Menu main class
#

class EvMenu:
    """
    This object represents an operational menu. It is initialized from
    a menufile.py instruction.

    """

    # convenient helpers for easy overloading
    node_border_char = "_"

        self,
        caller,
        menudata,
        startnode="start",
        cmdset_mergetype="Replace",
        cmdset_priority=1,
        auto_look=True,
        auto_help=True,
        startnode_input="",
        debug=False,
        **kwargs,
    ):
        """
        Initialize the menu tree and start the caller onto the first node.

        Args:
            caller (Object, Account or Session): The user of the menu.
            menudata (str, module or dict): The full or relative path to the module
                holding the menu tree data. All global functions in this module
                whose name doesn't start with '_ ' will be parsed as menu nodes.
                Also the module itself is accepted as input. Finally, a dictionary
                menu tree can be given directly. This must then be a mapping
                `{"nodekey":callable,...}` where `callable` must be called as
                and return the data expected of a menu node. This allows for
                dynamic menu creation.
            startnode (str, optional): The starting node name in the menufile.
            cmdset_mergetype (str, optional): 'Replace' (default) means the menu
                commands will be exclusive - no other normal commands will
                be usable while the user is in the menu. 'Union' means the
                menu commands will be integrated with the existing commands
                (it will merge with `merge_priority`), if so, make sure that
                the menu's command names don't collide with existing commands
                in an unexpected way. Also the CMD_NOMATCH and CMD_NOINPUT will
                be overloaded by the menu cmdset. Other cmdser mergetypes
                has little purpose for the menu.
            cmdset_priority (int, optional): The merge priority for the
                menu command set. The default (1) is usually enough for most
                types of menus.
            auto_quit (bool, optional): Allow user to use "q", "quit" or
                "exit" to leave the menu at any point. Recommended during
                development!
            auto_look (bool, optional): Automatically make "looK" or "l" to
                re-show the last node. Turning this off means you have to handle
                re-showing nodes yourself, but may be useful if you need to
                use "l" for some other purpose.
            auto_help (bool, optional): Automatically make "help" or "h" show
                the current help entry for the node. If turned off, eventual
                help must be handled manually, but it may be useful if you
                need 'h' for some other purpose, for example.
            cmd_on_exit (callable, str or None, optional): When exiting the menu
                (either by reaching a node with no options or by using the
                in-built quit command (activated with `allow_quit`), this
                callback function or command string will be executed.
                The callback function takes two parameters, the caller then the
                EvMenu object. This is called after cleanup is complete.
                Set to None to not call any command.
            persistent (bool, optional): Make the Menu persistent (i.e. it will
                survive a reload. This will make the Menu cmdset persistent. Use
                with caution - if your menu is buggy you may end up in a state
                you can't get out of! Also note that persistent mode requires
                that all formatters, menu nodes and callables are possible to
                *pickle*. When the server is reloaded, the latest node shown will be completely
                re-run with the same input arguments - so be careful if you are counting
                up some persistent counter or similar - the counter may be run twice if
                reload happens on the node that does that. Note that if `debug` is True,
                this setting is ignored and assumed to be False.
            startnode_input (str or (str, dict), optional): Send an input text to `startnode` as if
                a user input text from a fictional previous node. If including the dict, this will
                be passed as **kwargs to that node. When the server reloads,
                the latest visited node will be re-run as `node(caller, raw_string, **kwargs)`.
            session (Session, optional): This is useful when calling EvMenu from an account
                in multisession mode > 2. Note that this session only really relevant
                for the very first display of the first node - after that, EvMenu itself
                will keep the session updated from the command input. So a persistent
                menu will *not* be using this same session anymore after a reload.
            debug (bool, optional): If set, the 'menudebug' command will be made available
                by default in all nodes of the menu. This will print out the current state of
                the menu. Deactivate for production use! When the debug flag is active, the
                `persistent` flag is deactivated.
            **kwargs: All kwargs will become initialization variables on `caller.ndb._evmenu`,
                to be available at run.

        Raises:
            EvMenuError: If the start/end node is not found in menu tree.

        Notes:
            While running, the menu is stored on the caller as `caller.ndb._evmenu`. Also
            the current Session (from the Command, so this is still valid in multisession
            environments) is available through `caller.ndb._evmenu._session`. The `_evmenu`
            property is a good one for storing intermediary data on between nodes since it
            will be automatically deleted when the menu closes.

            In persistent mode, all nodes, formatters and callbacks in the menu must be
            possible to be *pickled*, this excludes e.g. callables that are class methods
            or functions defined dynamically or as part of another function. In

        """
        self._startnode = startnode
        self._menutree = self._parse_menudata(menudata)
        self._persistent = persistent if not debug else False
        self._quitting = False

        if startnode not in self._menutree:
            raise EvMenuError("Start node '%s' not in menu tree!" % startnode)

        # public variables made available to the command

        # track EvMenu kwargs
        self.auto_quit = auto_quit
        self.auto_look = auto_look
        self.auto_help = auto_help
        self.debug_mode = debug
        self._session = session
        if isinstance(cmd_on_exit, str):
            # At this point menu._session will have been replaced by the
            # menu command to the actual session calling.
            self.cmd_on_exit = lambda caller, menu: caller.execute_cmd(
                cmd_on_exit, session=menu._session
            )
        elif callable(cmd_on_exit):
            self.cmd_on_exit = cmd_on_exit
        else:
            self.cmd_on_exit = None
        # current menu state
        self.default = None
        self.nodetext = None
        self.helptext = None
        self.nodename = None
        self.node_kwargs = {}

        # used for testing
        self.test_options = {}
        self.test_nodetext = ""

        # assign kwargs as initialization vars on ourselves.
        reserved_clash = set(
            (
                "_startnode",
                "_menutree",
                "_session",
                "_persistent",
                "cmd_on_exit",
                "default",
                "nodetext",
                "helptext",
                "options",
                "cmdset_mergetype",
                "auto_quit",
            )
        ).intersection(set(kwargs.keys()))
        if reserved_clash:
                f"One or more of the EvMenu `**kwargs` ({list(reserved_clash)}) "
                "is reserved by EvMenu for internal use."
            )
        for key, val in kwargs.items():
            setattr(self, key, val)

        if self.caller.ndb._evmenu:
            # an evmenu already exists - we try to close it cleanly. Note that this will
            # not fire the previous menu's end node.
            try:
                self.caller.ndb._evmenu.close_menu()
                pass

        # store ourself on the object
        self.caller.ndb._evmenu = self

        # TODO DEPRECATED - for backwards-compatibility. Use `.ndb._evmenu` instead
        self.caller.ndb._menutree = self

            # save the menu to the database
            calldict = {
                "startnode": startnode,
                "cmdset_mergetype": cmdset_mergetype,
                "cmdset_priority": cmdset_priority,
                "auto_quit": auto_quit,
                "auto_look": auto_look,
                "auto_help": auto_help,
                "cmd_on_exit": cmd_on_exit,
                "persistent": persistent,
            }
            calldict.update(kwargs)
            try:
                caller.attributes.add("_menutree_saved", (self.__class__, (menudata,), calldict))
                caller.attributes.add("_menutree_saved_startnode", (startnode, startnode_input))
                self.msg(_ERROR_PERSISTENT_SAVING.format(error=err))

        # set up the menu command on the caller
        menu_cmdset = EvMenuCmdSet()
        menu_cmdset.mergetype = str(cmdset_mergetype).lower().capitalize() or "Replace"
        menu_cmdset.priority = int(cmdset_priority)
        self.caller.cmdset.add(menu_cmdset, persistent=persistent)

        reserved_startnode_kwargs = set(("nodename", "raw_string"))
        startnode_kwargs = {}
        if isinstance(startnode_input, (tuple, list)) and len(startnode_input) > 1:
            startnode_input, startnode_kwargs = startnode_input[:2]
            if not isinstance(startnode_kwargs, dict):
                raise EvMenuError("startnode_input must be either a str or a tuple (str, dict).")
            clashing_kwargs = reserved_startnode_kwargs.intersection(set(startnode_kwargs.keys()))
            if clashing_kwargs:
                    f"Evmenu startnode_inputs includes kwargs {tuple(clashing_kwargs)} that "
                    "clashes with EvMenu's internal usage."
                )

        # start the menu
        self.goto(self._startnode, startnode_input, **startnode_kwargs)

    def _parse_menudata(self, menudata):
        """
        Parse a menufile for node functions and store in dictionary
        map. Alternatively, accept a pre-made mapping dictionary of
        node functions.

        Args:
            menudata (str, module or dict): The python.path to the menufile,
                or the python module itself. If a dict, this should be a
                mapping nodename:callable, where the callable must match
                the criteria for a menu node.

        Returns:
            menutree (dict): A {nodekey: func}

        """
        if isinstance(menudata, dict):
            # This is assumed to be a pre-loaded menu tree.
            return menudata
        else:
            # a python path of a module
            module = mod_import(menudata)
                (key, func)
                for key, func in module.__dict__.items()
                if isfunction(func) and not key.startswith("_")
            )

    def _format_node(self, nodetext, optionlist):
        """
        Format the node text + option section

        Args:
            nodetext (str): The node text
            optionlist (list): List of (key, desc) pairs.

        Returns:
            string (str): The options section, including
                all needed spaces.

        Notes:
            This will adjust the columns of the options, first to use
            a maxiumum of 4 rows (expanding in columns), then gradually
            growing to make use of the screen space.

        """

        # handle the node text
        nodetext = self.nodetext_formatter(nodetext)

        # handle the options
        optionstext = self.options_formatter(optionlist)

        # format the entire node
        return self.node_formatter(nodetext, optionstext)

    def _safe_call(self, callback, raw_string, **kwargs):
        """
        Call a node-like callable, with a variable number of raw_string, *args, **kwargs, all of
        which should work also if not present (only `caller` is always required). Return its result.

        """
        try:
            try:
                nargs = len(getfullargspec(callback).args)
                raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback))
            supports_kwargs = bool(getfullargspec(callback).varkw)
            if nargs <= 0:

            if supports_kwargs:
                if nargs > 1:
                    ret = callback(self.caller, raw_string, **kwargs)
                    # callback accepting raw_string, **kwargs
                else:
                    # callback accepting **kwargs
                    ret = callback(self.caller, **kwargs)
            elif nargs > 1:
                # callback accepting raw_string
                ret = callback(self.caller, raw_string)
            else:
                # normal callback, only the caller as arg
                ret = callback(self.caller)
        except EvMenuError:
            errmsg = _ERR_GENERAL.format(nodename=callback)
            self.msg(errmsg)
            raise

        return ret

    def _execute_node(self, nodename, raw_string, **kwargs):
        """
        Execute a node (-function) and get its returns.

        Args:
            nodename (str): Name of node.
            raw_string (str): The raw default string entered on the
                previous node (only used if the node accepts it as an
                argument)
            kwargs (any, optional): Optional kwargs for the node.

        Returns:
            nodetext, options (tuple): The node text (a string or a
                tuple and the options tuple, if any.

        """
        try:
            node = self._menutree[nodename]
            self.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename))
            raise EvMenuError
        try:
            kwargs["_current_nodename"] = nodename
            ret = self._safe_call(node, raw_string, **kwargs)
            if isinstance(ret, (tuple, list)) and len(ret) > 1:
                nodetext, options = ret[:2]
            else:
                nodetext, options = ret, None
            self.msg(_ERR_GENERAL.format(nodename=nodename))
            raise

        # store options to make them easier to test
        self.test_nodetext = nodetext
        self.test_options = options

        return nodetext, options

    def _extract_goto(self, nodename, option_dict):
        """
        Helper: Get callables and their eventual kwargs.

        Args:
            nodename (str): The current node name (used for error reporting).
            option_dict (dict): The seleted option's dict.

        Returns:
            goto (str, callable or None): The goto directive in the option.
            goto_kwargs (dict): Kwargs for `goto` if the former is callable, otherwise empty.

        """
        goto_kwargs = {}
        goto = option_dict.get("goto", None)
        if goto and isinstance(goto, (tuple, list)):
            if len(goto) > 1:
                goto, goto_kwargs = goto[:2]  # ignore any extra arguments
                if not hasattr(goto_kwargs, "__getitem__"):
                    #  not a dict-like structure
                    raise EvMenuError(
                        "EvMenu node {}: goto kwargs is not a dict: {}".format(
                            nodename, goto_kwargs
                        )
                    )
            else:
                goto = goto[0]
        return goto, goto_kwargs

    def goto(self, nodename_or_callable, raw_string, **kwargs):
        """
        Run a node by name, optionally dynamically generating that name first.

        Args:
            nodename_or_callable (str or callable): Name of node or a callable
                to be called as `function(caller, raw_string, **kwargs)` or
                `function(caller, **kwargs)`. This callable must return the node-name (str)
                pointing to the next node.
                argument)
            **kwargs: Extra arguments to goto callables.

        """

        inp_nodename = nodename_or_callable
        if callable(nodename_or_callable):
            # run the "goto" callable to get the next node to go to
            nodename = self._safe_call(nodename_or_callable, raw_string, **kwargs)
            if isinstance(nodename, (tuple, list)):
                if not len(nodename) > 1 or not isinstance(nodename[1], dict):
                        "{}: goto callable must return str or (str, dict)".format(inp_nodename)
                    )
                nodename, kwargs = nodename[:2]
            if not nodename:
                # no nodename return. Re-run current node
                nodename = self.nodename
        elif nodename_or_callable is None:
            # repeat current node
            nodename = self.nodename
        else:
            # the nodename given directly
            nodename = nodename_or_callable

        # one way or another, we have the nodename as a string now

        try:
            # execute the found nodename, make use of the returns.
            nodetext, options = self._execute_node(nodename, raw_string, **kwargs)
            return

        if self._persistent:
            self.caller.attributes.add(
                "_menutree_saved_startnode", (nodename, (raw_string, kwargs))
            )

        # validation of the node return values

        # if the nodetext is a list/tuple, the second set is the help text.
        # helptext can also be a dict, which allows for tooltip command-text (key-value) or
        # ((key,aliases)-value) pairs.

        # make sure helptext is defined
        helptext = ""
        if is_iter(nodetext):
            nodetext, *helptext = nodetext
            helptext = helptext[0] if helptext else ""

            if isinstance(helptext, dict):
                # handle both (key-value) and (key, aliases)-value pairs
                _help_text = {}
                for topic_keys, help_entry in helptext.items():
                    for topic_key in make_iter(topic_keys):
                        _help_text[topic_key.strip().lower()] = help_entry
                helptext = _help_text

        nodetext = "" if nodetext is None else str(nodetext)

        # handle the helptext
            self.helptext = self.helptext_formatter(helptext)
        elif options:
            self.helptext = _HELP_FULL if self.auto_quit else _HELP_NO_QUIT
        else:
            self.helptext = _HELP_NO_OPTIONS if self.auto_quit else _HELP_NO_OPTIONS_NO_QUIT

        # store the current node's data in the menu state

        self.nodename = nodename
        self.node_kwargs = kwargs
        self.options = {}
        display_options = []  # options will be displayed in this order

        if options:
            options = [options] if isinstance(options, dict) else options
            for inum, dic in enumerate(options):
                # homogenize the options dict
                keys = make_iter(dic.get("key"))
                desc = dic.get("desc", dic.get("text", None))

                if "_default" in keys:
                    keys = [key for key in keys if key != "_default"]
                    goto, goto_kwargs = self._extract_goto(nodename, dic)
                    self.default = (goto, goto_kwargs)
                else:
                    # use the key (only) if set, otherwise use the running number
                    keys = list(make_iter(dic.get("key", str(inum + 1).strip())))

                if keys:
                    display_options.append((keys[0], desc))
                    for key in keys:
                        self.options[strip_ansi(key).strip().lower()] = (goto, goto_kwargs)

        # format the text
        self.nodetext = self._format_node(nodetext, display_options)

        # display self.nodetext to the user
        self.display_nodetext()

        # close menu if we have no more options to process
        if not options:
            self.close_menu()

        """
        Shutdown menu; occurs when reaching the end node or using the quit command.
        """
        if not self._quitting:
            # avoid multiple calls from different sources
            self._quitting = True
            self.caller.cmdset.remove(EvMenuCmdSet)
            del self.caller.ndb._evmenu
            del self.caller.ndb._menutree  # TODO Deprecated
                self.caller.attributes.remove("_menutree_saved")
                self.caller.attributes.remove("_menutree_saved_startnode")
            if self.cmd_on_exit is not None:
                self.cmd_on_exit(self.caller, self)
            # special for template-generated menues
            del self.caller.db._evmenu_template_contents

    def print_debug_info(self, arg):
        """
        Messages the caller with the current menu state, for debug purposes.

        Args:
            arg (str): Arg to debug instruction, either nothing, 'full' or the name
                of a property to inspect.

        """
        all_props = inspect.getmembers(self)
        all_methods = [name for name, _ in inspect.getmembers(self, predicate=inspect.ismethod)]
        all_builtins = [name for name, _ in inspect.getmembers(self, predicate=inspect.isbuiltin)]
        props = {
            prop: value
            for prop, value in all_props
            if prop not in all_methods and prop not in all_builtins and not prop.endswith("__")
        }

        local = {
            key: var
            for key, var in locals().items()
            if key not in all_props and not key.endswith("__")
        }

        if arg:
            if arg in props:
                debugtxt = " |y* {}:|n\n{}".format(arg, props[arg])
            elif arg in local:
                debugtxt = " |y* {}:|n\n{}".format(arg, local[arg])
            elif arg == "full":
                debugtxt = (
                    "|yMENU DEBUG full ... |n\n"
                    + "\n".join(
                        "|y *|n {}: {}".format(key, val) for key, val in sorted(props.items())
                    )
                    + "\n |yLOCAL VARS:|n\n"
                        "|y *|n {}: {}".format(key, val) for key, val in sorted(local.items())
                    )
                    + "\n |y... END MENU DEBUG|n"
                )
            else:
                debugtxt = "|yUsage: menudebug full|<name of property>|n"
        else:
            debugtxt = (
                "|yMENU DEBUG properties ... |n\n"
                + "\n".join(
                    "|y *|n {}: {}".format(key, crop(to_str(val, force_string=True), width=50))
                    for key, val in sorted(props.items())
                )
                + "\n |yLOCAL VARS:|n\n"
                    for key, val in sorted(local.items())
                )
                + "\n |y... END MENU DEBUG|n"
            )
        self.msg(debugtxt)

    def msg(self, txt):
        """
        This is a central point for sending return texts to the caller. It
        allows for a central point to add custom messaging when creating custom
        EvMenu overrides.

        Args:
            txt (str): The text to send.

        Notes:
            By default this will send to the same session provided to EvMenu
            (if `session` kwarg was provided to `EvMenu.__init__`). It will
            also send it with a `type=menu` for the benefit of OOB/webclient.

        """
        self.caller.msg(text=(txt, {"type": "menu"}), session=self._session)

    def parse_input(self, raw_string):
        """
        Parses the incoming string from the menu user. This is the entry-point for all input
        into the menu.

        Args:
            raw_string (str): The incoming, unmodified string
                from the user.
        Notes:
            This method is expected to parse input and use the result
            to relay execution to the relevant methods of the menu. It
            should also report errors directly to the user.

        """
        # this is the input cmd given to the menu
        cmd = strip_ansi(raw_string.strip().lower())

        try:
            if self.options and cmd in self.options:
                # we chose one of the available options; this
                # will take precedence over the default commands
                goto_node, goto_kwargs = self.options[cmd]
                self.goto(goto_node, raw_string, **(goto_kwargs or {}))
            elif self.auto_look and cmd in ("look", "l"):
                self.display_nodetext()
            elif self.auto_help and isinstance(self.helptext, dict) and cmd in self.helptext:
                self.display_tooltip(cmd)
            elif self.auto_help and cmd in ("help", "h"):
                self.display_helptext()
            elif self.auto_quit and cmd in ("quit", "q", "exit"):
                self.close_menu()
            elif self.debug_mode and cmd.startswith("menudebug"):
                self.print_debug_info(cmd[9:].strip())
            elif self.default:
                goto_node, goto_kwargs = self.default
            else:
                self.msg(_HELP_NO_OPTION_MATCH)
        except EvMenuGotoAbortMessage as err:
            # custom interrupt from inside a goto callable - print the message and
            # stay on the current node.
            self.msg(str(err))

    def display_nodetext(self):
        self.msg(self.nodetext)

        self.msg(self.helptext)

    def display_tooltip(self, cmd):
        self.msg(self.helptext.get(cmd))

    # formatters - override in a child class

        """

        Args:
            nodetext (str): The full node text (the text describing the node).

        Returns:
            nodetext (str): The formatted node text.

        """
        return dedent(nodetext.strip("\n"), baseline_index=0).rstrip()

        """
        Format the node's help text

        Args:
            helptext (str): The unformatted help text for the node.

        Returns:
            helptext (str): The formatted help text.

        """
        return dedent(helptext.strip("\n"), baseline_index=0).rstrip()

        """
        Formats the option block.

        Args:
            optionlist (list): List of (key, description) tuples for every
                option related to this node.

        Returns:
            options (str): The formatted option display.

        """
        if not optionlist:
            return ""

        # column separation distance
        colsep = 4

        nlist = len(optionlist)

        # get the widest option line in the table.
        table_width_max = -1
        table = []
            if key or desc:
                desc_string = f": {desc}" if desc else ""
                table_width_max = max(
                    table_width_max,
                    max(m_len(p) for p in key.split("\n"))
                    + max(m_len(p) for p in desc_string.split("\n"))
                    + colsep,
                )
                raw_key = strip_ansi(key)
                if raw_key != key:
                    # already decorations in key definition
                    table.append(f" |lc{raw_key}|lt{key}|le{desc_string}")
                else:
                    # add a default white color to key
                    table.append(f" |lc{raw_key}|lt|w{key}|n|le{desc_string}")
        ncols = _MAX_TEXT_WIDTH // table_width_max  # number of columns

        if ncols < 0:
            # no visible options at all
            return ""

        ncols = 1 if ncols == 0 else ncols

        # minimum number of rows in a column
        min_rows = 4

        # split the items into columns
        split = max(min_rows, ceil(len(table) / ncols))
        max_end = len(table)
        cols_list = []
        for icol in range(ncols):
            start = icol * split
            end = min(start + split, max_end)
            cols_list.append(EvColumn(*table[start:end]))

        return str(EvTable(table=cols_list, border="none"))

        """

        Args:

        Returns:

        """
        sep = self.node_border_char

        if self._session:
            screen_width = self._session.protocol_flags.get("SCREENWIDTH", {0: _MAX_TEXT_WIDTH})[0]
        else:
            screen_width = _MAX_TEXT_WIDTH

        nodetext_width_max = max(m_len(line) for line in nodetext.split("\n"))
        options_width_max = max(m_len(line) for line in optionstext.split("\n"))
        total_width = min(screen_width, max(options_width_max, nodetext_width_max))
        separator1 = sep * total_width + "\n\n" if nodetext_width_max else ""
        separator2 = "\n" + sep * total_width + "\n\n" if total_width else ""
        return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext

#
# List node (decorator turning a node into a list with
#   look/edit/add functionality for the elements)
#

def list_node(option_generator, select=None, pagesize=10):
    """
    Decorator for making an EvMenu node into a multi-page list node. Will add new options,
    prepending those options added in the node.

    Args:
        option_generator (callable or list): A list of strings indicating the options, or a callable
            that is called as option_generator(caller) to produce such a list.
        select (callable or str, optional): Node to redirect a selection to. Its `**kwargs` will
            contain the `available_choices` list and `selection` will hold one of the elements in
            that list.  If a callable, it will be called as
            `select(caller, menuchoice, **kwargs)` where menuchoice is the chosen option as a
            string and `available_choices` is a kwarg mapping the option keys to the choices
            offered by the option_generator. The callable whould return the name of the target node
            to goto after this selection (or None to repeat the list-node). Note that if this is not
            given, the decorated node must itself provide a way to continue from the node!
        pagesize (int): How many options to show per page.

    Example:

        ```python
        def select(caller, selection, available_choices=None, **kwargs):
            '''
            This will be called by all auto-generated options except any 'extra_options'
            you return from the node (those you need to handle normally).

            Args:
                caller (Object or Account): User of the menu.
                selection (str): What caller chose in the menu
                available_choices (list): The keys of elements available on the *current listing
                    page*.
                **kwargs: Kwargs passed on from the node.
            Returns:
                tuple, str or None: A tuple (nextnodename, **kwargs) or just nextnodename.  Return
                    `None` to go back to the listnode.
            '''

            # (do something with `selection` here)

            return "nextnode", **kwargs

        @list_node(['foo', 'bar'], select)
        def node_index(caller):
            text = "describing the list"

            # optional extra options in addition to the list-options
            extra_options = []

            return text, extra_options

        ```

    Notes:
        All normal `goto` callables returned from the decorated nodes
        will, if they accept `**kwargs`, get a new kwarg 'available_choices'
        injected. These are the ordered list of named options (descs) visible
        on the current node page.

    """

    def decorator(func):
        def _select_parser(caller, raw_string, **kwargs):
            """
            Parse the select action

            """
            available_choices = kwargs.pop("available_choices", [])

            try:
                index = int(raw_string.strip()) - 1
                selection = available_choices[index]
                caller.msg(_("|rInvalid choice.|n"))
            else:
                if callable(select):
                    try:
                        if bool(getfullargspec(select).varkw):
                            return select(
                                caller, selection, available_choices=available_choices, **kwargs
                            )
                        else:
                            return select(caller, selection, **kwargs)
                        logger.log_trace(
                            "Error in EvMenu.list_node decorator:\n  "
                            f"select-callable: {select}\n  with args: ({caller}"
                            f"{selection}, {available_choices}, {kwargs}) raised "
                            "exception."
                        )
                elif select:
                    # we assume a string was given, we inject the result into the kwargs
                    # to pass on to the next node
                    kwargs["selection"] = selection
                    return str(select), kwargs
            # this means the previous node will be re-run with these same kwargs

        def _list_node(caller, raw_string, **kwargs):
            option_list = (
                option_generator(caller) if callable(option_generator) else option_generator
            )

            npages = 0
            page_index = 0
            page = []
            options = []

            if option_list:
                nall_options = len(option_list)
                pages = [
                    option_list[ind : ind + pagesize] for ind in range(0, nall_options, pagesize)
                ]
                npages = len(pages)

                page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0)))
                page = pages[page_index]

            text = ""
            extra_text = None

            # dynamic, multi-page option list. Each selection leads to the `select`
            # callback being called with a result from the available choices
            options.extend(
                [
                    {"desc": opt, "goto": (_select_parser, {"available_choices": page, **kwargs})}
                    for opt in page
                ]
            )

            if npages > 1:
                # if the goto callable returns None, the same node is rerun, and
                # kwargs not used by the callable are passed on to the node. This
                # allows us to call ourselves over and over, using different kwargs.
                    {
                        "key": (_("|Wcurrent|n"), "c"),
                        "desc": "|W({}/{})|n".format(page_index + 1, npages),
                        "goto": (lambda caller: None, {"optionpage_index": page_index}),
                    }
                )
                if page_index > 0:
                    options.append(
                        {
                            "key": (_("|wp|Wrevious page|n"), "p"),
                            "goto": (lambda caller: None, {"optionpage_index": page_index - 1}),
                        }
                    )
                if page_index < npages - 1:
                        {
                            "key": (_("|wn|Wext page|n"), "n"),
                            "goto": (lambda caller: None, {"optionpage_index": page_index + 1}),
                        }
                    )

            # add data from the decorated node

            decorated_options = []
            supports_kwargs = bool(getfullargspec(func).varkw)
            try:
                if supports_kwargs:
                    text, decorated_options = func(caller, raw_string, **kwargs)
                else:
                    text, decorated_options = func(caller, raw_string)
                try:
                    if supports_kwargs:
                        text, decorated_options = func(caller, **kwargs)
                    else:
                        text, decorated_options = func(caller)
                    raise
            else:
                if isinstance(decorated_options, dict):
                    decorated_options = [decorated_options]
                else:
                    decorated_options = make_iter(decorated_options)

            if isinstance(decorated_options, dict):
                decorated_options = [decorated_options]
            for eopt in decorated_options:
                cback = ("goto" in eopt and "goto") or None
                if cback:
                    signature = eopt[cback]
                    if callable(signature):
                        # callable with no kwargs defined
                        eopt[cback] = (signature, {"available_choices": page})
                    elif is_iter(signature):
                        if len(signature) > 1 and isinstance(signature[1], dict):
                            signature[1]["available_choices"] = page
                            eopt[cback] = signature
                        elif signature:
                            # a callable alone in a tuple (i.e. no previous kwargs)
                            eopt[cback] = (signature[0], {"available_choices": page})
                        else:
                            # malformed input.
                            logger.log_err(
                                "EvMenu @list_node decorator found "
                                "malformed option to decorate: {}".format(eopt)
                            )
                extra_options.append(eopt)

            options.extend(extra_options)
            text = text + "\n\n" + extra_text if extra_text else text

        return _list_node

# -------------------------------------------------------------------------------------------------
#
# Simple input shortcuts
#

class CmdGetInput(Command):
    """
    Enter your data and press return.
    """

        """This is called when user enters anything."""
        try:
            getinput = caller.ndb._getinput
            if not getinput and inherits_from(caller, evennia.DefaultObject):
                getinput = caller.account.ndb._getinput
                if getinput:
                    caller = caller.account
            callback = getinput._callback

            caller.ndb._getinput._session = self.session
            prompt = caller.ndb._getinput._prompt
            args = caller.ndb._getinput._args
            kwargs = caller.ndb._getinput._kwargs
            result = self.raw_string.rstrip()  # we strip the ending line break caused by sending

            ok = not callback(caller, prompt, result, *args, **kwargs)
            if ok:
                # only clear the state if the callback does not return
                # anything
                del caller.ndb._getinput
                caller.cmdset.remove(InputCmdSet)
            # make sure to clean up cmdset if something goes wrong
            caller.msg("|rError in get_input. Choice not confirmed (report to admin)|n")
            logger.log_trace("Error in get_input")
            caller.cmdset.remove(InputCmdSet)

class InputCmdSet(CmdSet):
    """
    This stores the input command
    """

    key = "input_cmdset"

        """called once at creation"""
        self.add(CmdGetInput())

class _Prompt:
    """Dummy holder"""

    pass

def get_input(caller, prompt, callback, session=None, *args, **kwargs):
    """
    This is a helper function for easily request input from the caller.

    Args:
        caller (Account or Object): The entity being asked the question. This
            should usually be an object controlled by a user.
        prompt (str): This text will be shown to the user, in order to let them
            know their input is needed.
        callback (callable): A function that will be called
            when the user enters a reply. It must take three arguments: the
            `caller`, the `prompt` text and the `result` of the input given by
            the user. If the callback doesn't return anything or return False,
            the input prompt will be cleaned up and exited. If returning True,
            the prompt will remain and continue to accept input.
        session (Session, optional): This allows to specify the
            session to send the prompt to. It's usually only needed if `caller`
            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.getinput._session`.
        *args (any): Extra arguments to pass to `callback`.  To utilise `*args`
            (and `**kwargs`), a value for the `session` argument must also be
            provided.
        **kwargs (any): Extra kwargs to pass to `callback`.

    Raises:
        RuntimeError: If the given callback is not callable.

    Notes:
        The result value sent to the callback is raw and not processed in any
        way. This means that you will get the ending line return character from
        most types of client inputs. So make sure to strip that before doing a
        comparison.

        When the prompt is running, a temporary object `caller.ndb._getinput`
        is stored; this will be removed when the prompt finishes.

        If you need the specific Session of the caller (which may not be easy
        to get if caller is an account in higher multisession modes), then it
        is available in the callback through `caller.ndb._getinput._session`.
        This is why the `session` is required as input.

        It's not recommended to 'chain' `get_input` into a sequence of
        questions. This will result in the caller stacking ever more instances
        of InputCmdSets. While they will all be cleared on concluding the
        get_input chain, EvMenu should be considered for anything beyond a
        single question.

    """
    if not callable(callback):
        raise RuntimeError("get_input: input callback is not callable.")
    caller.ndb._getinput = _Prompt()
    caller.ndb._getinput._callback = callback
    caller.ndb._getinput._prompt = prompt
    caller.ndb._getinput._session = session
    caller.ndb._getinput._args = args
    caller.ndb._getinput._kwargs = kwargs
    caller.cmdset.add(InputCmdSet, persistent=False)
    caller.msg(prompt, session=session)

class CmdYesNoQuestion(Command):
    """
    Handle a prompt for yes or no. Press [return] for the default choice.

    """

    aliases = [_CMD_NOMATCH, "yes", "no", "y", "n", "a", "abort"]
    arg_regex = r"^$"

    def _clean(self, caller):
        del caller.ndb._yes_no_question
        if not caller.cmdset.has(YesNoQuestionCmdSet) and inherits_from(
            caller, evennia.DefaultObject
        ):
            caller.account.cmdset.remove(YesNoQuestionCmdSet)
        else:
            caller.cmdset.remove(YesNoQuestionCmdSet)

        try:
            yes_no_question = caller.ndb._yes_no_question
            if not yes_no_question and inherits_from(caller, evennia.DefaultObject):
                yes_no_question = caller.account.ndb._yes_no_question
                caller = caller.account

            if not yes_no_question:
                self._clean(caller)
                return

            inp = self.cmdname

            if inp == _CMD_NOINPUT:
                raw = self.raw_cmdname.strip()
                if not raw:
                    # use default
                    inp = yes_no_question.default
                else:
                    inp = raw

            if inp in ("a", "abort") and yes_no_question.allow_abort:
                caller.msg(_("Aborted."))
                return

            caller.ndb._yes_no_question.session = self.session

            args = yes_no_question.args
            kwargs = yes_no_question.kwargs
            kwargs["caller_session"] = self.session

            if inp in ("yes", "y"):
                yes_no_question.yes_callable(caller, *args, **kwargs)
            elif inp in ("no", "n"):
                yes_no_question.no_callable(caller, *args, **kwargs)
            else:
                # invalid input. Resend prompt without cleaning
                caller.msg(yes_no_question.prompt, session=self.session)
                return

            # cleanup
            self._clean(caller)
            caller.msg(_("|rError in ask_yes_no. Choice not confirmed (report to admin)|n"))
            logger.log_trace("Error in ask_yes_no")
            raise

class YesNoQuestionCmdSet(CmdSet):
    """
    """

    key = "yes_no_question_cmdset"

        self.add(CmdYesNoQuestion())

def ask_yes_no(
    caller,
    prompt="Yes or No {options}?",
    yes_action="Yes",
    no_action="No",
    default=None,
    allow_abort=False,
    *args,
    **kwargs,
):
    """
    A helper function for asking a simple yes/no question. This will cause
    the system to pause and wait for input from the player.

    Args:
        caller (Object): The entity being asked.
        prompt (str): The yes/no question to ask. This takes an optional formatting
            marker `{options}` which will be filled with 'Y/N', '[Y]/N' or
            'Y/[N]' depending on the setting of `default`. If `allow_abort` is set,
            then the 'A(bort)' option will also be available.
        yes_action (callable or str): If a callable, this will be called
            with `(caller, *args, **kwargs)` when the Yes-choice is made.
            If a string, this string will be echoed back to the caller.
        no_action (callable or str): If a callable, this will be called
            with `(caller, *args, **kwargs)` when the No-choice is made.
        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 (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).
            callbacks) through `caller.ndb._yes_no_question.session`.
        *args: Additional arguments passed on into callables.
        **kwargs: Additional keyword args passed on into callables.

    Raises:
        RuntimeError, FooError: If default and `allow_abort` clashes.

    Example:
        ::

            # just returning strings
            ask_yes_no(caller, "Are you happy {options}?",
                       "you answered yes", "you answered no")
            # trigger callables
            ask_yes_no(caller, "Are you sad {options}?",
                       _callable_yes, _callable_no, allow_abort=True)

    """

    def _callable_yes_txt(caller, *args, **kwargs):
        yes_txt = kwargs["yes_txt"]
        session = kwargs["caller_session"]
        caller.msg(yes_txt, session=session)

    def _callable_no_txt(caller, *args, **kwargs):
        no_txt = kwargs["no_txt"]
        caller.msg(no_txt, session=session)

    if not callable(yes_action):
        kwargs["yes_txt"] = str(yes_action)
        yes_action = _callable_yes_txt

    if not callable(no_action):
        kwargs["no_txt"] = str(no_action)
        no_action = _callable_no_txt

    # prepare the prompt with options
    options = "Y/N"
    abort_txt = "/Abort" if allow_abort else ""
    if default:
        default = default.lower()
        if default == "y":
            options = "[Y]/N"
        elif default == "n":
            options = "Y/[N]"
        elif default == "a":
            allow_abort = True
            abort_txt = "/[A]bort"
    options += abort_txt
    prompt = prompt.format(options=options)

    caller.ndb._yes_no_question = _Prompt()
    caller.ndb._yes_no_question.prompt = prompt
    caller.ndb._yes_no_question.session = session
    caller.ndb._yes_no_question.default = default
    caller.ndb._yes_no_question.allow_abort = allow_abort
    caller.ndb._yes_no_question.yes_callable = yes_action
    caller.ndb._yes_no_question.no_callable = no_action
    caller.ndb._yes_no_question.args = args
    caller.ndb._yes_no_question.kwargs = kwargs

    caller.cmdset.add(YesNoQuestionCmdSet)

#
# Menu generation from menu template string
#

_RE_NODE = re.compile(r"##\s*?NODE\s+?(?P<nodename>\S[\S\s]*?)$", re.I + re.M)
_RE_OPTIONS_SEP = re.compile(r"##\s*?OPTIONS\s*?$", re.I + re.M)
_RE_CALLABLE = re.compile(r"\S+?\(\)", re.I + re.M)
_RE_CALLABLE = re.compile(r"(?P<funcname>\S+?)(?:\((?P<kwargs>[\S\s]+?)\)|\(\))", re.I + re.M)

_OPTION_INPUT_MARKER = ">"
_OPTION_ALIAS_MARKER = ";"
_OPTION_SEP_MARKER = ":"
_OPTION_CALL_MARKER = "->"
_OPTION_COMMENT_START = "#"

# Input/option/goto handler functions that allows for dynamically generated
# nodes read from the menu template.

def _process_callable(caller, goto, goto_callables, raw_string, current_nodename, kwargs):
    """
    Central helper for parsing a goto-callable (`funcname(**kwargs)`) out of
    the right-hand-side of the template options and map this to an actual
    callable registered with the template generator. This involves parsing the
    func-name and running literal-eval on its kwargs.

    """
    match = _RE_CALLABLE.match(goto)
    if match:
        gotofunc = match.group("funcname")
        gotokwargs = match.group("kwargs") or ""
        if gotofunc in goto_callables:
            for kwarg in gotokwargs.split(","):
                if kwarg and "=" in kwarg:
                    key, value = [part.strip() for part in kwarg.split("=", 1)]
                    if key in (
                        "evmenu_goto",
                        "evmenu_gotomap",
                        "_current_nodename",
                        "evmenu_current_nodename",
                        "evmenu_goto_callables",
                    ):
                        raise RuntimeError(
                            f"EvMenu template error: goto-callable '{goto}' uses a "
                            f"kwarg ({kwarg}) that is reserved for the EvMenu templating "
                            "system. Rename the kwarg."
                        )
                    try:
                        key = literal_eval(key)
                        pass
                    try:
                        value = literal_eval(value)
                        pass
                    kwargs[key] = value

                goto = goto_callables[gotofunc](caller, raw_string, **kwargs)
    if goto is None:
        return goto, {"generated_nodename": current_nodename}
    return goto, {"generated_nodename": goto}

def _generated_goto_func(caller, raw_string, **kwargs):
    """
    This rerouter handles normal direct goto func call matches.

    key : ... -> goto_callable(**kwargs)

    """
    goto = kwargs["evmenu_goto"]
    goto_callables = kwargs["evmenu_goto_callables"]
    current_nodename = kwargs["evmenu_current_nodename"]
    return _process_callable(caller, goto, goto_callables, raw_string, current_nodename, kwargs)

def _generated_input_goto_func(caller, raw_string, **kwargs):
    """
    This goto-func acts as a rerouter for >-type line parsing (by acting as the
    _default option). The patterns discovered in the menu maps to different
    *actual* goto-funcs. We map to those here.

    >pattern: ... -> goto_callable

    """
    gotomap = kwargs["evmenu_gotomap"]
    raw_string = raw_string.strip("\n")  # strip is necessary to catch empty return

    # start with glob patterns
    for pattern, goto in gotomap.items():
        if fnmatch(raw_string.lower(), pattern):
            return _process_callable(
                caller, goto, goto_callables, raw_string, current_nodename, kwargs
            )
    # no glob pattern match; try regex
        if pattern and re.match(pattern, raw_string.lower(), flags=re.I + re.M):
            )
    # no match, show error
    raise EvMenuGotoAbortMessage(_HELP_NO_OPTION_MATCH)

def _generated_node(caller, raw_string, **kwargs):
    """
    Every node in the templated menu will be this node, but with dynamically
    changing text/options. It must be a global function like this because
    otherwise we could not make the templated-menu persistent.

    """
    text, options = caller.db._evmenu_template_contents[kwargs["_current_nodename"]]

def parse_menu_template(caller, menu_template, goto_callables=None):
    """
    Parse menu-template string. The main function of the EvMenu templating system.

    Args:
        caller (Object or Account): Entity using the menu.
        menu_template (str): Menu described using the templating format.
        goto_callables (dict, optional): Mapping between call-names and callables
            on the form `callable(caller, raw_string, **kwargs)`. These are what is
            available to use in the `menu_template` string.

    Returns:
        dict: A `{"node": nodefunc}` menutree suitable to pass into EvMenu.

    """

    def _validate_kwarg(goto, kwarg):
        """
        Validate goto-callable kwarg is on correct form.
        """
        if "=" not in kwarg:
                f"EvMenu template error: goto-callable '{goto}' has a "
                f"non-kwarg argument ({kwarg}). All callables in the "
                "template must have only keyword-arguments, or no "
                "args at all."
            )
        key, _ = [part.strip() for part in kwarg.split("=", 1)]
        if key in (
            "evmenu_goto",
            "evmenu_gotomap",
            "_current_nodename",
            "evmenu_current_nodename",
            "evmenu_goto_callables",
        ):
                f"EvMenu template error: goto-callable '{goto}' uses a "
                f"kwarg ({kwarg}) that is reserved for the EvMenu templating "
                "system. Rename the kwarg."
            )

    def _parse_options(nodename, optiontxt, goto_callables):
        """
        Parse option section into option dict.

        """
        optiontxt = optiontxt[0].strip() if optiontxt else ""
        optionlist = [optline.strip() for optline in optiontxt.split("\n")]
        inputparsemap = {}

        for inum, optline in enumerate(optionlist):
            if optline.startswith(_OPTION_COMMENT_START) or _OPTION_SEP_MARKER not in optline:
                # skip comments or invalid syntax
                continue
            key = ""
            desc = ""
            pattern = None

            key, goto = [part.strip() for part in optline.split(_OPTION_SEP_MARKER, 1)]

            # desc -> goto
            if _OPTION_CALL_MARKER in goto:
                desc, goto = [part.strip() for part in goto.split(_OPTION_CALL_MARKER, 1)]

            # validate callable
            match = _RE_CALLABLE.match(goto)
            if match:
                kwargs = match.group("kwargs")
                if kwargs:
                    for kwarg in kwargs.split(","):
                        _validate_kwarg(goto, kwarg)

            # parse key [;aliases|pattern]
            key = [part.strip() for part in key.split(_OPTION_ALIAS_MARKER)]
            if not key:
                # fall back to this being the Nth option
                key = [f"{inum + 1}"]
            main_key = key[0]

            if main_key.startswith(_OPTION_INPUT_MARKER):
                # if we have a pattern, build the arguments for _default later
                pattern = main_key[len(_OPTION_INPUT_MARKER) :].strip()
                inputparsemap[pattern] = goto
            else:
                # a regular goto string/callable target
                option = {
                    "key": key,
                    "goto": (
                        _generated_goto_func,
                        {
                            "evmenu_goto": goto,
                            "evmenu_current_nodename": nodename,
                            "evmenu_goto_callables": goto_callables,
                        },
                    ),
                }
                if desc:
                    option["desc"] = desc
                options.append(option)

        if inputparsemap:
            # if this exists we must create a _default entry too
                {
                    "key": "_default",
                    "goto": (
                        _generated_input_goto_func,
                        {
                            "evmenu_gotomap": inputparsemap,
                        },
                    ),
                }
            )

    def _parse(caller, menu_template, goto_callables):
        """
        Parse the menu string format into a node tree.

        """
        nodetree = {}
        splits = _RE_NODE.split(menu_template)
        splits = splits[1:] if splits else []

        # from evennia import set_trace;set_trace(term_size=(140,120))
        content_map = {}
        for node_ind in range(0, len(splits), 2):
            nodename, nodetxt = splits[node_ind], splits[node_ind + 1]
            text, *optiontxt = _RE_OPTIONS_SEP.split(nodetxt, maxsplit=2)
            options = _parse_options(nodename, optiontxt, goto_callables)
            content_map[nodename] = (text, options)
            nodetree[nodename] = _generated_node
        caller.db._evmenu_template_contents = content_map

        return nodetree

    return _parse(caller, menu_template, goto_callables)

def template2menu(
    caller,
    menu_template,
    goto_callables=None,
    startnode="start",
    **kwargs,
):
    """
    Helper function to generate and start an EvMenu based on a menu template
    string. This will internall call `parse_menu_template` and run a default
    EvMenu with its results.

    Args:
        caller (Object or Account): The entity using the menu.
        menu_template (str): The menu-template string describing the content
            and structure of the menu. It can also be the python-path to, or a module
            containing a `MENU_TEMPLATE` global variable with the template.
        goto_callables (dict, optional): Mapping of callable-names to
            module-global objects to reference by name in the menu-template.
            Must be on the form `callable(caller, raw_string, **kwargs)`.
        startnode (str, optional): The name of the startnode, if not 'start'.
        persistent (bool, optional): If the generated menu should be persistent.
        **kwargs: All kwargs will be passed into EvMenu.

    Returns:
        EvMenu: The generated EvMenu.

    """
    goto_callables = goto_callables or {}
    menu_tree = parse_menu_template(caller, menu_template, goto_callables)
    return EvMenu(
        caller,
        menu_tree,
        **kwargs,
    )

"""
EvMore - pager mechanism

This is a pager for displaying long texts and allows stepping up and down in
the text (the name comes from the traditional 'more' unix command).

To use, simply pass the text through the EvMore object:

```python

    from evennia.utils.evmore import EvMore

    text = some_long_text_output()
    EvMore(caller, text, always_page=False, session=None, justify_kwargs=None, **kwargs)
```

One can also use the convenience function `msg` from this module to avoid
having to set up the `EvMenu` object manually:

```python

    from evennia.utils import evmore

    evmore.msg(caller, text, always_page=False, session=None, justify_kwargs=None, **kwargs)
```

The `always_page` argument  decides if the pager is used also if the text is not long
enough to need to scroll, `session` is used to determine which session to relay
to and `justify_kwargs` are kwargs to pass to utils.utils.justify in order to
change the formatting of the text. The remaining `**kwargs` will be passed on to
the `caller.msg()` construct every time the page is updated.

----

"""

from evennia.utils.utils import dedent, inherits_from, justify, make_iter

# we need to use NAWS for this
_SCREEN_WIDTH = settings.CLIENT_DEFAULT_WIDTH
_SCREEN_HEIGHT = settings.CLIENT_DEFAULT_HEIGHT

_EVTABLE = None

_LBR = ANSIString("\n")

# text

_DISPLAY = """{text}
|n(|wPage|n [{pageno}/{pagemax}] |wn|next|n || |wp|nrevious || |wt|nop || |we|nnd || |wq|nuit)"""

class CmdMore(Command):
    """
    Manipulate the text paging. Catch no-input with aliases.
    """

    aliases = ["quit", "q", "abort", "a", "next", "n", "previous", "p", "top", "t", "end", "e"]
    auto_help = False

        """
        Implement the command
        """
        more = self.caller.ndb._more
        if not more and inherits_from(self.caller, evennia.DefaultObject):
            more = self.caller.account.ndb._more
        if not more:
            self.caller.msg("Error in loading the pager. Contact an admin.")
            return

        if cmd in ("abort", "a", "q"):
            more.page_quit()
        elif cmd in ("previous", "p"):
            more.page_back()
        elif cmd in ("top", "t", "look", "l"):
            more.page_top()
        elif cmd in ("end", "e"):
            more.page_end()
        else:
            # return or n, next
            more.page_next()

class CmdMoreExit(Command):
    """
    Any non-more command will exit the pager.

    """

        """
        Exit pager and re-fire the failed command.

        """
        more.page_quit()

        # re-fire the command (in new cmdset)
        self.caller.execute_cmd(self.raw_string)

class CmdSetMore(CmdSet):
    """
    Stores the more command
    """

    key = "more_commands"
    priority = 110

        self.add(CmdMore())
        self.add(CmdMoreExit())

# resources for handling queryset inputs
def queryset_maxsize(qs):
    return qs.count()

class EvMore(object):
    """
    The main pager object
    """

        self,
        caller,
        inp,
        always_page=False,
        justify=False,
        justify_kwargs=None,
        exit_on_lastpage=False,
        exit_cmd=None,
        page_formatter=str,
        **kwargs,
    ):
        """
        Initialization of the EvMore pager.

        Args:
            caller (Object or Account): Entity reading the text.
            inp (str, EvTable, Paginator or iterator): The text or data to put under paging.

                - If a string, paginage normally. If this text contains
                  one or more `\\\\f` format symbol, automatic pagination and justification
                  are force-disabled and page-breaks will only happen after each `\\\\f`.
                - If `EvTable`, the EvTable will be paginated with the same
                  setting on each page if it is too long. The table
                  decorations will be considered in the size of the page.
                - Otherwise `inp` is converted to an iterator, where each step is
                  expected to be a line in the final display. Each line
                  will be run through `iter_callable`.

            always_page (bool, optional): If `False`, the
                pager will only kick in if `inp` is too big
                to fit the screen.
            session (Session, optional): If given, this session will be used
                to determine the screen width and will receive all output.
            justify (bool, optional): If set, auto-justify long lines. This must be turned
                off for fixed-width or formatted output, like tables. It's force-disabled
                if `inp` is an EvTable.
            justify_kwargs (dict, optional): Keywords for the justify function. Used only
                if `justify` is True. If this is not set, default arguments will be used.
            exit_on_lastpage (bool, optional): If reaching the last page without the
                page being completely filled, exit pager immediately. If unset,
                another move forward is required to exit. If set, the pager
                exit message will not be shown.
            exit_cmd (str, optional): If given, this command-string will be executed on
                the caller when the more page exits. Note that this will be using whatever
                cmdset the user had *before* the evmore pager was activated (so none of
                the evmore commands will be available when this is run).
            kwargs (any, optional): These will be passed on to the `caller.msg` method. Notably,
                one can pass additional outputfuncs this way. There is one special kwarg:
                - text_kwargs - extra kwargs to pass with the text outputfunc, e.g.
                  `text_kwargs={"type": "help"} would result to each page being sent
                  to `msg` as `text=(pagetxt, {"type": "help"})`.

        Examples:

            ```python
            super_long_text = " ... "
            EvMore(caller, super_long_text)
            ```
            Paginator
            ```python
            from django.core.paginator import Paginator
            query = ObjectDB.objects.all()
            pages = Paginator(query, 10)  # 10 objs per page
            EvMore(caller, pages)
            ```
            Every page an EvTable
            ```python
            from evennia import EvTable
            def _to_evtable(page):
                table = ... # convert page to a table
                return EvTable(*headers, table=table, ...)
            EvMore(caller, pages, page_formatter=_to_evtable)
            ```

        """
        self._always_page = always_page

            # if not supplied, use the first session to
            # determine screen size
            sessions = caller.sessions.get()
            if not sessions:
                return
            session = sessions[0]

        self._justify = justify
        self._justify_kwargs = justify_kwargs
        self.exit_on_lastpage = exit_on_lastpage
        self.exit_cmd = exit_cmd
        self._exit_msg = _("|xExited pager.|n")

        self._text_kwargs = kwargs.pop("text_kwargs", {})

        self._kwargs = kwargs

        self._pages = []
        self._npos = 0

        self._npages = 1
        self._paginator = self.paginator_index
        self._page_formatter = str

        # set up individual pages for different sessions
        height = max(4, session.protocol_flags.get("SCREENHEIGHT", {0: _SCREEN_HEIGHT})[0] - 4)
        self.width = session.protocol_flags.get("SCREENWIDTH", {0: _SCREEN_WIDTH})[0]
        # always limit number of chars to 10 000 per page
        self.height = min(10000 // max(1, self.width), height)

        # does initial parsing of input
        self.init_pages(inp)

        # kick things into gear

    # EvMore functional methods

    def display(self, show_footer=True):
        """
        Pretty-print the page.
        """
        pos = 0
        text = "[no content]"
        if self._npages > 0:
            pos = self._npos
            text = self.page_formatter(self.paginator(pos))
        if show_footer:
            page = _DISPLAY.format(text=text, pageno=pos + 1, pagemax=self._npages)
        else:
            page = text
        # check to make sure our session is still valid
        sessions = self._caller.sessions.get()
        if not sessions:
            self.page_quit()
            return
        # this must be an 'is' check, not an == check
        if not any(ses for ses in sessions if self._session is ses):
            self._session = sessions[0]
        text_outputfunc = (page, (), self._text_kwargs)
        self._caller.msg(text=text_outputfunc, session=self._session, **self._kwargs)

    def page_top(self):
        """
        Display the top page
        """

    def page_end(self):
        """
        Display the bottom page.
        """
        self._npos = self._npages - 1

    def page_next(self):
        """
        Scroll the text to the next page. Quit if already at the end
        of the page.
        """
        if self._npos >= self._npages - 1:
            # exit if we are already at the end
        else:
            self._npos += 1
            if self.exit_on_lastpage and self._npos >= (self._npages - 1):
                self.display(show_footer=False)
                self.page_quit(quiet=True)
            else:
                self.display()

    def page_back(self):
        """
        Scroll the text back up, at the most to the top.
        """
        self._npos = max(0, self._npos - 1)

    def page_quit(self, quiet=False):
        """
        Quit the pager
        """
        del self._caller.ndb._more
            self._caller.msg(text=self._exit_msg, **self._kwargs)
        self._caller.cmdset.remove(CmdSetMore)
        if self.exit_cmd:
            self._caller.execute_cmd(self.exit_cmd, session=self._session)

        """
        Starts the pagination
        """
        if self._npages <= 1 and not self._always_page:
            # no need for paging; just pass-through.
            self.display(show_footer=False)
        else:
            # go into paging mode
            # first pass on the msg kwargs
            self._caller.ndb._more = self
            self._caller.cmdset.add(CmdSetMore)

            # goto top of the text
            self.page_top()

    # default paginators - responsible for extracting a specific page number

    def paginator_index(self, pageno):
        """Paginate to specific, known index"""
        return self._data[pageno]

    def paginator_slice(self, pageno):
        """
        Paginate by slice. This is done with an eye on memory efficiency (usually for
        querysets); to avoid fetching all objects at the same time.

        """
        return self._data[pageno * self.height : pageno * self.height + self.height]

    def paginator_django(self, pageno):
        """
        Paginate using the django queryset Paginator API. Note that his is indexed from 1.
        """
        return self._data.page(pageno + 1)

    # default helpers to set up particular input types

    def init_evtable(self, table):
        """The input is an EvTable."""
        if table.height:
            # enforced height of each paged table, plus space for evmore extras
            self.height = table.height - 4

        # convert table to string
        text = str(table)
        self._justify = False
        self._justify_kwargs = None  # enforce
        self.init_str(text)

    def init_queryset(self, qs):
        """The input is a queryset"""
        nsize = qs.count()  # we assume each will be a line
        self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1)
        self._data = qs

    def init_django_paginator(self, pages):
        """
        The input is a django Paginator object.
        """
        self._npages = pages.num_pages
        self._data = pages

    def init_iterable(self, inp):
        """The input is something other than a string - convert to iterable of strings"""
        inp = make_iter(inp)
        nsize = len(inp)
        self._data = inp

    def init_f_str(self, text):
        """
        The input contains `\\f` markers. We use `\\f` to indicate the user wants to
        enforce their line breaks on their own. If so, we do no automatic
        line-breaking/justification at all.

        Args:
            text (str): The string to format with f-markers.

        """
        self._data = text.split("\f")
        self._npages = len(self._data)

    def init_str(self, text):
        """The input is a string"""

        if self._justify:
            # we must break very long lines into multiple ones. Note that this
            # will also remove spurious whitespace.
            justify_kwargs = self._justify_kwargs or {}
            width = self._justify_kwargs.get("width", self.width)
            justify_kwargs["width"] = width
            justify_kwargs["align"] = self._justify_kwargs.get("align", "l")
            justify_kwargs["indent"] = self._justify_kwargs.get("indent", 0)

            lines = []
            for line in text.split("\n"):
                if len(line) > width:
                    lines.extend(justify(line, **justify_kwargs).split("\n"))
                else:
                    lines.append(line)
        else:
            # no justification. Simple division by line
            lines = text.split("\n")

        # note: If joining on ANSIString here, we will parse out || escapes into | too early,
        # meaning the protocol will later parse into color; better to leave things and only parse
        # once.
        self._data = [
            "\n".join(lines[i : i + self.height]) for i in range(0, len(lines), self.height)
        ]

    # Hooks for customizing input handling and formatting (override in a child class)

        """
        Initialize the pagination. By default, will analyze input type to determine
        how pagination automatically.

        Args:
            inp (any): Incoming data to be paginated. By default, handles pagination of
                strings, querysets, django.Paginator, EvTables and any iterables with strings.

        Notes:
            If overridden, this method must perform the following  actions:

            - read and re-store `self._data` (the incoming data set) if needed for pagination to
              work.
            - set `self._npages` to the total number of pages. Default is 1.
            - set `self._paginator` to a callable that will take a page number 1...N and return
              the data to display on that page (not any decorations or next/prev buttons). If only
              wanting to change the paginator, override `self.paginator` instead.
            - set `self._page_formatter` to a callable that will receive the page from
              `self._paginator` and format it with one element per line. Default is `str`. Or
              override `self.page_formatter` directly instead.

            By default, helper methods are called that perform these actions
            depending on supported inputs.

        """
        if inherits_from(inp, "evennia.utils.evtable.EvTable"):
            # an EvTable
            self.init_evtable(inp)
            self._paginator = self.paginator_index
        elif isinstance(inp, QuerySet):
            # a queryset
            self.init_queryset(inp)
            self._paginator = self.paginator_slice
        elif isinstance(inp, Paginator):
            self.init_django_paginator(inp)
            self._paginator = self.paginator_django
        elif not isinstance(inp, str):
            # anything else not a str
            self.init_iterable(inp)
        elif "\f" in inp:
            # string with \f line-break markers in it
            self.init_f_str(inp)
        else:
            # a string
            self.init_str(inp)

    def paginator(self, pageno):
        """
        Paginator. The data operated upon is in `self._data`.

        Args:
            pageno (int): The page number to view, from 0...N-1
        Returns:
            str: The page to display (without any decorations, those are added
                by EvMore).

        """
        return self._paginator(pageno)

        """
        Page formatter. Every page passes through this method. Override
        it to customize behavior per-page. A common use is to generate a new
        EvTable for every page (this is more efficient than to generate one huge
        EvTable across many pages and feed it into EvMore all at once).

        Args:
            page (any): A piece of data representing one page to display. This must

        Returns:
            str: A ready-formatted page to display. Extra footer with help about
                switching to the next/prev page will be added automatically

        """
        return self._page_formatter(page)

# helper function

def msg(
    caller,
    text="",
    always_page=False,
    justify=False,
    justify_kwargs=None,
    exit_on_lastpage=True,
    **kwargs,
):
    """
    EvMore-supported version of msg, mimicking the normal msg method.

    """
    EvMore(
        caller,
        text,
        always_page=always_page,
        justify=justify,
        justify_kwargs=justify_kwargs,
        exit_on_lastpage=exit_on_lastpage,
        **kwargs,
    )

msg.__doc__ += dedent(EvMore.__init__.__doc__)

"""
This is an advanced ASCII table creator. It was inspired by Prettytable
(https://code.google.com/p/prettytable/) but shares no code and is considerably
more advanced, supporting auto-balancing of incomplete tables and ANSI colors among
other things.

```python
  from evennia.utils import evtable

  table = evtable.EvTable("Heading1", "Heading2",
                  table=[[1,2,3],[4,5,6],[7,8,9]], border="cells")
  table.add_column("This is long data", "This is even longer data")
  table.add_row("This is a single row")
  print table
```

Result:

::

    +----------------------+----------+---+--------------------------+
    |       Heading1       | Heading2 |   |                          |
    +~~~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~~+~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~+
    |           1          |     4    | 7 |     This is long data    |
    |           2          |     5    | 8 | This is even longer data |
    |           3          |     6    | 9 |                          |
    | This is a single row |          |   |                          |

As seen, the table will automatically expand with empty cells to make
the table symmetric. Tables can be restricted to a given width:

```python
  table.reformat(width=50, align="l")
```

(We could just have added these keywords to the table creation call)

This yields the following result:

::

    +-----------+------------+-----------+-----------+
    | Heading1  | Heading2   |           |           |
    +~~~~~~~~~~~+~~~~~~~~~~~~+~~~~~~~~~~~+~~~~~~~~~~~+
    | 1         | 4          | 7         | This is   |
    |           |            |           | long data |
    |           |            |           | This is   |
    | 2         | 5          | 8         | even      |
    |           |            |           | longer    |
    |           |            |           | data      |
    | 3         | 6          | 9         |           |
    | This is a |            |           |           |
    |  single   |            |           |           |
    | row       |            |           |           |

Table-columns can be individually formatted. Note that if an
individual column is set with a specific width, table auto-balancing
will not affect this column (this may lead to the full table being too
wide, so be careful mixing fixed-width columns with auto- balancing).
Here we change the width and alignment of the column at index 3
(Python starts from 0):

```python

table.reformat_column(3, width=30, align="r")
print table
```

::

    +-----------+-------+-----+-----------------------------+---------+
    | Heading1  | Headi |     |                             |         |
    |           | ng2   |     |                             |         |
    +~~~~~~~~~~~+~~~~~~~+~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~+
    | 1         | 4     | 7   |           This is long data | Test1   |
    | 2         | 5     | 8   |    This is even longer data | Test3   |
    | 3         | 6     | 9   |                             | Test4   |
    | This is a |       |     |                             |         |
    |  single   |       |     |                             |         |
    | row       |       |     |                             |         |

When adding new rows/columns their data can have its own alignments
(left/center/right, top/center/bottom).

If the height is restricted, cells will be restricted from expanding
vertically. This will lead to text contents being cropped. Each cell
can only shrink to a minimum width and height of 1.

`EvTable` is intended to be used with `ANSIString` for supporting ANSI-coloured
string types.

When a cell is auto-wrapped across multiple lines, ANSI-reset sequences will be
put at the end of each wrapped line. This means that the colour of a wrapped
cell will not "bleed", but it also means that eventual colour outside the table
will not transfer "across" a table, you need to re-set the color to have it
appear on both sides of the table string.

----

"""

from textwrap import TextWrapper

from evennia.utils.utils import display_len as d_len
from evennia.utils.utils import is_iter, justify

def _to_ansi(obj):
    """
    convert to ANSIString.

    Args:
        obj (str): Convert incoming text to
            be ANSI aware ANSIStrings.
    """
    if is_iter(obj):
        return [_to_ansi(o) for o in obj]
    else:
        return ANSIString(obj)

_whitespace = "\t\n\x0b\x0c\r "

class ANSITextWrapper(TextWrapper):
    """
    This is a wrapper work class for handling strings with ANSI tags
    in it.  It overloads the standard library `TextWrapper` class and
    is used internally in `EvTable` and has no public methods.

    """

    def _munge_whitespace(self, text):
        """_munge_whitespace(text : string) -> string

        Munge whitespace in text: expand tabs and convert all other
        whitespace characters to spaces.  Eg. " foo\tbar\n\nbaz"
        becomes " foo    bar  baz".
        """

    # TODO: Ignore expand_tabs/replace_whitespace until ANSIString handles them.
    # - don't remove this code. /Griatch
    #        if self.expand_tabs:
    #            text = text.expandtabs()
    #        if self.replace_whitespace:
    #            if isinstance(text, str):
    #                text = text.translate(self.whitespace_trans)
    #        return text

    def _split(self, text):
        """_split(text : string) -> [string]

        Split the text to wrap into indivisible chunks.  Chunks are
        not quite the same as words; see _wrap_chunks() for full
        details.  As an example, the text
          Look, goof-ball -- use the -b option!
        breaks into the following chunks:
          'Look,', ' ', 'goof-', 'ball', ' ', '--', ' ',
          'use', ' ', 'the', ' ', '-b', ' ', 'option!'
        if break_on_hyphens is True, or in:
          'Look,', ' ', 'goof-ball', ' ', '--', ' ',
          'use', ' ', 'the', ' ', '-b', ' ', option!'
        otherwise.
        """
        # NOTE-PYTHON3: The following code only roughly approximates what this
        #               function used to do. Regex splitting on ANSIStrings is
        #               dropping ANSI codes, so we're using ANSIString.split
        #               for the time being.
        #
        #               A less hackier solution would be appreciated.
        chunks = _to_ansi(text).split()

        chunks = [chunk + " " for chunk in chunks if chunk]  # remove empty chunks

        if len(chunks) > 1:
            chunks[-1] = chunks[-1][0:-1]

        return chunks

    def _wrap_chunks(self, chunks):
        """_wrap_chunks(chunks : [string]) -> [string]

        Wrap a sequence of text chunks and return a list of lines of
        length 'self.width' or less.  (If 'break_long_words' is false,
        some lines may be longer than this.)  Chunks correspond roughly
        to words and the whitespace between them: each chunk is
        indivisible (modulo 'break_long_words'), but a line break can
        come between any two chunks.  Chunks should not have internal
        whitespace; ie. a chunk is either all whitespace or a "word".
        Whitespace chunks will be removed from the beginning and end of
        lines, but apart from that whitespace is preserved.
        """
        lines = []
        if self.width <= 0:
            raise ValueError("invalid width %r (must be > 0)" % self.width)

        # Arrange in reverse order so items can be efficiently popped
        # from a stack of chucks.
        chunks.reverse()

        while chunks:
            # Start the list of chunks that will make up the current line.
            # cur_len is just the length of all the chunks in cur_line.
            cur_line = []
            cur_len = 0

            # Figure out which static string will prefix this line.
            if lines:
                indent = self.subsequent_indent
            else:
                indent = self.initial_indent

            # Maximum width for this line.
            width = self.width - d_len(indent)

            # First chunk on line is whitespace -- drop it, unless this
            # is the very beginning of the text (ie. no lines started yet).
            if self.drop_whitespace and chunks[-1].strip() == "" and lines:
                del chunks[-1]

            while chunks:
                ln = d_len(chunks[-1])

                # Can at least squeeze this chunk onto the current line.
                if cur_len + ln <= width:
                    cur_line.append(chunks.pop())
                    cur_len += ln

                # Nope, this line is full.
                else:
                    break

            # The current line is full, and the next chunk is too big to
            # fit on *any* line (not just this one).
            if chunks and d_len(chunks[-1]) > width:
                self._handle_long_word(chunks, cur_line, cur_len, width)

            # If the last chunk on this line is all whitespace, drop it.
            if self.drop_whitespace and cur_line and cur_line[-1].strip() == "":
                del cur_line[-1]

            # Convert current line back to a string and store it in list
            # of all lines (return value).
            if cur_line:
                ln = ""
                for w in cur_line:  # ANSI fix
                    ln += w  #
                lines.append(indent + ln)
        return lines

# -- Convenience interface ---------------------------------------------

def wrap(text, width=_DEFAULT_WIDTH, **kwargs):
    """
    Wrap a single paragraph of text, returning a list of wrapped lines.

    Reformat the single paragraph in 'text' so it fits in lines of no
    more than 'width' columns, and return a list of wrapped lines.  By
    default, tabs in 'text' are expanded with string.expandtabs(), and
    all other whitespace characters (including newline) are converted to

    Args:
        text (str): Text to wrap.
        width (int, optional): Width to wrap `text` to.

        See TextWrapper class for available keyword args to customize
        wrapping behaviour.

    """
    w = ANSITextWrapper(width=width, **kwargs)
    return w.wrap(text)

def fill(text, width=_DEFAULT_WIDTH, **kwargs):
    """Fill a single paragraph of text, returning a new string.

    Reformat the single paragraph in 'text' to fit in lines of no more
    than 'width' columns, and return a new string containing the entire
    wrapped paragraph.  As with wrap(), tabs are expanded and other
    whitespace characters converted to space.

    Args:
        text (str): Text to fill.
        width (int, optional): Width of fill area.

        filling behaviour.

    """
    return w.fill(text)

# EvCell class (see further down for the EvTable itself)

class EvCell:
    """
    Holds a single data cell for the table. A cell has a certain width
    and height and contains one or more lines of data. It can shrink
    and resize as needed.

    """

    def __init__(self, data, **kwargs):
        """
        Args:
            data (str): The un-padded data of the entry.

            width (int): Desired width of cell. It will pad
                to this size.
            height (int): Desired height of cell. it will pad
            pad_width (int): General padding width. This can be overruled
                by individual settings below.
            pad_left (int): Number of extra pad characters on the left.
            pad_right (int): Number of extra pad characters on the right.
            pad_top (int):  Number of extra pad lines top (will pad with `vpad_char`).
            pad_bottom (int): Number of extra pad lines bottom (will pad with `vpad_char`).
            pad_char (str)- pad character to use for padding. This is overruled
                by individual settings below (default `" "`).
            hpad_char (str): Pad character to use both for extra horizontal
                padding (default `" "`).
            vpad_char (str): Pad character to use for extra vertical padding
                and for vertical fill (default `" "`).
            fill_char (str): Character used to filling (expanding cells to
                desired size). This can be overruled by individual settings below.
            hfill_char (str): Character used for horizontal fill (default `" "`).
            vfill_char (str): Character used for vertical fill (default `" "`).
            align (str): Should be one of "l", "r", "c", "f" or "a" for left-, right-, center-,
                full-justified (with space between words) or absolute (keep as much original
                whitespace as possible). Default is left-aligned.
            valign (str): Should be one of "t", "b" or "c" for top-, bottom and center
                vertical alignment respectively. Default is centered.
            border_width (int): General border width. This is overruled
            border_left (int): Left border width.
            border_right (int): Right border width.
            border_top (int): Top border width.
            border_bottom (int): Bottom border width.
            border_char (str): This will use a single border char for all borders.
                  overruled by individual settings below.
            border_left_char (str): Char used for left border.
            border_right_char (str): Char used for right border.
            border_top_char (str): Char used for top border.
            border_bottom_char (str): Char user for bottom border.
            corner_char (str): Character used when two borders cross.  (default is "").
                This is overruled by individual settings below.
            corner_top_left_char (str): Char used for "nw" corner.
            corner_top_right_char (str):  Char used for "ne" corner.
            corner_bottom_left_char (str): Char used for "sw" corner.
            corner_bottom_right_char (str): Char used for "se" corner.
            crop_string (str): String to use when cropping sideways, default is `'[...]'`.
            crop (bool): Crop contentof cell rather than expand vertically, default=`False`.
            enforce_size (bool): If true, the width/height of the cell is
                strictly enforced and extra text will be cropped rather than the
                cell growing vertically.

        Raises:
            Exception: for impossible cell size requirements where the
                border width or height cannot fit, or the content is too
                small.

        """
        self.formatted = None
        padwidth = kwargs.get("pad_width", None)
        padwidth = int(padwidth) if padwidth is not None else None
        self.pad_left = int(kwargs.get("pad_left", padwidth if padwidth is not None else 1))
        self.pad_right = int(kwargs.get("pad_right", padwidth if padwidth is not None else 1))
        self.pad_top = int(kwargs.get("pad_top", padwidth if padwidth is not None else 0))
        self.pad_bottom = int(kwargs.get("pad_bottom", padwidth if padwidth is not None else 0))

        self.enforce_size = kwargs.get("enforce_size", False)

        # avoid multi-char pad_chars messing up counting
        pad_char = kwargs.get("pad_char", " ")
        pad_char = pad_char[0] if pad_char else " "
        hpad_char = kwargs.get("hpad_char", pad_char)
        self.hpad_char = hpad_char[0] if hpad_char else pad_char
        vpad_char = kwargs.get("vpad_char", pad_char)
        self.vpad_char = vpad_char[0] if vpad_char else pad_char

        fill_char = kwargs.get("fill_char", " ")
        fill_char = fill_char[0] if fill_char else " "
        hfill_char = kwargs.get("hfill_char", fill_char)
        self.hfill_char = hfill_char[0] if hfill_char else " "
        vfill_char = kwargs.get("vfill_char", fill_char)
        self.vfill_char = vfill_char[0] if vfill_char else " "

        self.crop_string = kwargs.get("crop_string", "[...]")

        # borders and corners
        borderwidth = kwargs.get("border_width", 0)
        self.border_left = kwargs.get("border_left", borderwidth)
        self.border_right = kwargs.get("border_right", borderwidth)
        self.border_top = kwargs.get("border_top", borderwidth)
        self.border_bottom = kwargs.get("border_bottom", borderwidth)

        borderchar = kwargs.get("border_char", None)
        self.border_left_char = kwargs.get("border_left_char", borderchar if borderchar else "|")
        self.border_right_char = kwargs.get(
            "border_right_char", borderchar if borderchar else self.border_left_char
        )
        self.border_top_char = kwargs.get("border_top_char", borderchar if borderchar else "-")
        self.border_bottom_char = kwargs.get(
            "border_bottom_char", borderchar if borderchar else self.border_top_char
        )

        corner_char = kwargs.get("corner_char", "+")
        self.corner_top_left_char = kwargs.get("corner_top_left_char", corner_char)
        self.corner_top_right_char = kwargs.get("corner_top_right_char", corner_char)
        self.corner_bottom_left_char = kwargs.get("corner_bottom_left_char", corner_char)
        self.corner_bottom_right_char = kwargs.get("corner_bottom_right_char", corner_char)

        # alignments
        self.align = kwargs.get("align", "l")
        self.valign = kwargs.get("valign", "c")

        self.data = self._split_lines(_to_ansi(data))
        self.raw_width = max(d_len(line) for line in self.data)
        self.raw_height = len(self.data)

        # this is extra trimming required for cels in the middle of a table only
        self.trim_horizontal = 0
        self.trim_vertical = 0

        # width/height is given without left/right or top/bottom padding
        if "width" in kwargs:
            width = kwargs.pop("width")
            self.width = (
                width - self.pad_left - self.pad_right - self.border_left - self.border_right
            )
            if self.width <= 0 < self.raw_width:
                raise Exception("Cell width too small - no space for data.")
        else:
            self.width = self.raw_width
        if "height" in kwargs:
            height = kwargs.pop("height")
            self.height = (
                height - self.pad_top - self.pad_bottom - self.border_top - self.border_bottom
            )
            if self.height <= 0 < self.raw_height:
                raise Exception("Cell height too small - no space for data.")
        else:
            self.height = self.raw_height

    def _reformat(self):
        """
        Apply all EvCells' formatting operations.

        """
        data = self._border(self._pad(self._valign(self._align(self._fit_width(self.data)))))

    def _split_lines(self, text):
        """
        Simply split by linebreaks

        Args:
            text (str): text to split.

        Returns:
            split (list): split text.
        """
        return text.split("\n")

    def _fit_width(self, data):
        """
        Split too-long lines to fit the desired width of the Cell.

        Args:
            data (str): Text to adjust to the cell's width.

        Returns:
            adjusted data (str): The adjusted text.

        Notes:
            This also updates `raw_width`.

        """
        width = self.width
        adjusted_data = []
        for line in data:
            if 0 < width < d_len(line):
                # replace_whitespace=False, expand_tabs=False is a
                # fix for ANSIString not supporting expand_tabs/translate
                adjusted_data.extend(
                    [
                        ANSIString(part + ANSIString("|n"))
                        for part in wrap(line, width=width, drop_whitespace=False)
                    ]
                )
            else:
                adjusted_data.append(line)
        if self.enforce_size:
            # don't allow too high cells
            excess = len(adjusted_data) - self.height
            if excess > 0:
                # too many lines. Crop and mark last line with crop_string
                crop_string = self.crop_string
                adjusted_data = adjusted_data[:-excess]
                adjusted_data_length = len(adjusted_data[-1])
                crop_string_length = len(crop_string)
                if adjusted_data_length >= crop_string_length:
                    # replace with data[...]
                    # (note that if adjusted data is shorter than the crop-string,
                    # we skip the crop-string and just pass the cropped data.)
                    adjusted_data[-1] = adjusted_data[-1][:-crop_string_length] + crop_string

            elif excess < 0:
                # too few lines. Fill to height.
                adjusted_data.extend(["" for _ in range(excess)])

        return adjusted_data

    def _align(self, data):
        """
        Align list of rows of cell. Whitespace characters will be stripped
        if there is only one whitespace character - otherwise, it's assumed
        the caller may be trying some manual formatting in the text.

        Args:
            data (str): Text to align.

        Returns:
            text (str): Aligned result.

        """
        align = self.align
        hfill_char = self.hfill_char
        return [justify(line, width, align=align, fillchar=hfill_char) for line in data]

    def _valign(self, data):
        """
        Align cell vertically

        Args:

        Returns:
            text (str): Vertically aligned text.

        """
        valign = self.valign
        height = self.height
        cheight = len(data)
        excess = height - cheight
        padline = self.vfill_char * self.width

        if excess <= 0:
        # only care if we need to add new lines
        if valign == "t":
            return data + [padline for _ in range(excess)]
        elif valign == "b":
            return [padline for _ in range(excess)] + data
        else:  # center
            narrowside = [padline for _ in range(excess // 2)]
            widerside = narrowside + [padline]
            if excess % 2:
                # uneven padding
                if height % 2:
                    return widerside + data + narrowside
                else:
                    return narrowside + data + widerside
            else:
                # even padding, same on both sides
                return narrowside + data + narrowside

    def _pad(self, data):
        """
        Pad data with extra characters on all sides.

        Args:
            data (str): Text to pad.

        Returns:
            text (str): Padded text.

        """
        left = self.hpad_char * self.pad_left
        right = self.hpad_char * self.pad_right
        vfill = (self.width + self.pad_left + self.pad_right) * self.vpad_char
        top = [vfill for _ in range(self.pad_top)]
        bottom = [vfill for _ in range(self.pad_bottom)]
        return top + [left + line + right for line in data] + bottom

    def _border(self, data):
        """
        Add borders to the cell.

        Args:
            data (str): Text to surround with borders.

        Return:
            text (str): Text with borders.

        """

        left = self.border_left_char * self.border_left + ANSIString("|n")
        right = ANSIString("|n") + self.border_right_char * self.border_right

        cwidth = (
            self.width
            + self.pad_left
            + self.pad_right
            + max(0, self.border_left - 1)
            + max(0, self.border_right - 1)
        )

        vfill = self.corner_top_left_char if left else ""
        vfill += cwidth * self.border_top_char
        vfill += self.corner_top_right_char if right else ""
        top = [vfill for _ in range(self.border_top)]

        vfill = self.corner_bottom_left_char if left else ""
        vfill += cwidth * self.border_bottom_char
        vfill += self.corner_bottom_right_char if right else ""
        bottom = [vfill for _ in range(self.border_bottom)]

    def get_min_height(self):
        """
        Get the minimum possible height of cell, including at least
        one line for data.

        Returns:
            min_height (int): The mininum height of cell.

        """
        return self.pad_top + self.pad_bottom + self.border_bottom + self.border_top + 1

    def get_min_width(self):
        """
        Get the minimum possible width of cell, including at least one
        character-width for data.

        Returns:
            min_width (int): The minimum width of cell.

        """
        return self.pad_left + self.pad_right + self.border_left + self.border_right + 1

    def get_height(self):
        """
        Get natural height of cell, including padding.

        Returns:
            natural_height (int): Height of cell.

        """
        return len(self.formatted)  # if self.formatted else 0

    def get_width(self):
        """
        Get natural width of cell, including padding.

        Returns:
            natural_width (int): Width of cell.

        """
        return d_len(self.formatted[0])  # if self.formatted else 0

    def replace_data(self, data, **kwargs):
        """
        Replace cell data. This causes a full reformat of the cell.

        Args:
            data (str): Cell data.

        Notes:
            The available keyword arguments are the same as for
            `EvCell.__init__`.

        """
        self.reformat(**kwargs)

    def reformat(self, **kwargs):
        """
        Reformat the EvCell with new options

            The available keyword arguments are the same as for `EvCell.__init__`.

        Raises:
            Exception: If the cells cannot shrink enough to accomodate
                the options or the data given.

        """
        # keywords that require manipulation
        self.pad_left = int(
            kwargs.pop("pad_left", padwidth if padwidth is not None else self.pad_left)
        )
        self.pad_right = int(
            kwargs.pop("pad_right", padwidth if padwidth is not None else self.pad_right)
        )
        self.pad_top = int(
            kwargs.pop("pad_top", padwidth if padwidth is not None else self.pad_top)
        )
        self.pad_bottom = int(
            kwargs.pop("pad_bottom", padwidth if padwidth is not None else self.pad_bottom)
        )

        pad_char = kwargs.pop("pad_char", None)
        hpad_char = kwargs.pop("hpad_char", pad_char)
        self.hpad_char = hpad_char[0] if hpad_char else self.hpad_char
        vpad_char = kwargs.pop("vpad_char", pad_char)
        self.vpad_char = vpad_char[0] if vpad_char else self.vpad_char

        fillchar = kwargs.pop("fill_char", None)
        hfill_char = kwargs.pop("hfill_char", fillchar)
        self.hfill_char = hfill_char[0] if hfill_char else self.hfill_char
        vfill_char = kwargs.pop("vfill_char", fillchar)
        self.vfill_char = vfill_char[0] if vfill_char else self.vfill_char

        borderwidth = kwargs.get("border_width", None)
        self.border_left = kwargs.pop(
            "border_left", borderwidth if borderwidth is not None else self.border_left
        )
        self.border_right = kwargs.pop(
            "border_right", borderwidth if borderwidth is not None else self.border_right
        )
        self.border_top = kwargs.pop(
            "border_top", borderwidth if borderwidth is not None else self.border_top
        )
        self.border_bottom = kwargs.pop(
            "border_bottom", borderwidth if borderwidth is not None else self.border_bottom
        )

        self.border_left_char = kwargs.pop(
            "border_left_char", borderchar if borderchar else self.border_left_char
        )
        self.border_right_char = kwargs.pop(
            "border_right_char", borderchar if borderchar else self.border_right_char
        )
        self.border_top_char = kwargs.pop(
            "border_topchar", borderchar if borderchar else self.border_top_char
        )
        self.border_bottom_char = kwargs.pop(
            "border_bottom_char", borderchar if borderchar else self.border_bottom_char
        )

        corner_char = kwargs.get("corner_char", None)
        self.corner_top_left_char = kwargs.pop(
            "corner_top_left", corner_char if corner_char is not None else self.corner_top_left_char
        )
        self.corner_top_right_char = kwargs.pop(
            "corner_top_right",
            corner_char if corner_char is not None else self.corner_top_right_char,
        )
        self.corner_bottom_left_char = kwargs.pop(
            "corner_bottom_left",
            corner_char if corner_char is not None else self.corner_bottom_left_char,
        )
        self.corner_bottom_right_char = kwargs.pop(
            "corner_bottom_right",
            corner_char if corner_char is not None else self.corner_bottom_right_char,
        )

        # this is used by the table to adjust size of cells with borders in the middle
        # of the table
        self.trim_horizontal = kwargs.pop("trim_horizontal", self.trim_horizontal)
        self.trim_vertical = kwargs.pop("trim_vertical", self.trim_vertical)

        # fill all other properties
            setattr(self, key, value)

        # Handle sizes
                width
                - self.pad_left
                - self.pad_right
                - self.border_left
                - self.border_right
                + self.trim_horizontal
            )
            # if self.width <= 0 and self.raw_width > 0:
                raise Exception("Cell width too small, no room for data.")
                height
                - self.pad_top
                - self.pad_bottom
                - self.border_top
                - self.border_bottom
                + self.trim_vertical
            )
                raise Exception("Cell height too small, no room for data.")

        # reformat (to new sizes, padding, header and borders)
        self.formatted = self._reformat()

        """
        Get data, padded and aligned in the form of a list of lines.

        """
        if not self.formatted:
            self.formatted = self._reformat()
        return self.formatted

        return str(ANSIString("<EvCel %s>" % self.formatted))

        "returns cell contents on string form"
        return str(ANSIString("\n").join(self.formatted))

# EvColumn class

class EvColumn:
    """
    This class holds a list of Cells to represent a column of a table.
    It holds operations and settings that affect *all* cells in the
    column.

    Columns are not intended to be used stand-alone; they should be
    incorporated into an EvTable (like EvCells)

    """

        """
        Args:
            Text for each row in the column

            All `EvCell.__init_` keywords are available, these
            settings will be persistently applied to every Cell in the
            column.

        """
        self.options = kwargs  # column-specific options
        self.column = [EvCell(data, **kwargs) for data in args]

    def _balance(self, **kwargs):
        """
        Make sure to adjust the width of all cells so we form a
        coherent and lined-up column. Will enforce column-specific
        options to cells.

            Extra keywords to modify the column setting. Same keywords
            as in `EvCell.__init__`.

        """
        col = self.column
        # fixed options for the column will override those requested in the call!
        # this is particularly relevant to things like width/height, to avoid
        # fixed-widths columns from being auto-balanced
        kwargs.update(self.options)
        # use fixed width or adjust to the largest cell
        if "width" not in kwargs:
            [
                cell.reformat() for cell in col
            ]  # this is necessary to get initial widths of all cells
            kwargs["width"] = max(cell.get_width() for cell in col) if col else 0
        [cell.reformat(**kwargs) for cell in col]

    def add_rows(self, *args, **kwargs):
        """
        Add new cells to column. They will be inserted as
        a series of rows. It will inherit the options
        of the rest of the column's cells (use update to change
        options).

        Args:
            Texts for the new cells
            ypos (int, optional): Index position in table before which to insert the
                new column. Uses Python indexing, so to insert at the top,
                use `ypos=0`. If not given, data will be inserted at the end
                of the column.

            Available keywods as per `EvCell.__init__`.

        """
        # column-level options override those in kwargs
        options = {**kwargs, **self.options}

        ypos = kwargs.get("ypos", None)
        if ypos is None or ypos > len(self.column):
            # add to the end
            self.column.extend([EvCell(data, **options) for data in args])
        else:
            # insert cells before given index
            ypos = min(len(self.column) - 1, max(0, int(ypos)))
            new_cells = [EvCell(data, **options) for data in args]
            self.column = self.column[:ypos] + new_cells + self.column[ypos:]
        # self._balance(**kwargs)

        """
        Change the options for the column.

            Keywords as per `EvCell.__init__`.

        """
        self._balance(**kwargs)

    def reformat_cell(self, index, **kwargs):
        """
        reformat cell at given index, keeping column options if
        necessary.

        Args:
            index (int): Index location of the cell in the column,
                starting from 0 for the first row to Nrows-1.

        """
        # column-level options take precedence here
        self.column[index].reformat(**kwargs)

        return "<EvColumn\n  %s>" % "\n  ".join([repr(cell) for cell in self.column])

        return len(self.column)

        return iter(self.column)

    def __getitem__(self, index):
        return self.column[index]

    def __setitem__(self, index, value):
        self.column[index] = value

    def __delitem__(self, index):
        del self.column[index]

# Main Evtable class

class EvTable:
    """
    The table class holds a list of EvColumns, each consisting of EvCells so
    that the result is a 2D matrix.
    """

        """
        Args:
            Header texts for the table.

            table (list of lists or list of `EvColumns`, optional):
                This is used to build the table in a quick way.  If not
                given, the table will start out empty and `add_` methods
                need to be used to add rows/columns.
            header (bool, optional): `True`/`False` - turn off the
                header texts (`*args`) being treated as a header (such as
                not adding extra underlining)
            pad_width (int, optional): How much empty space to pad your cells with
                (default is 1)
            border (str, optional)): The border style to use. This is one of
                    - `None` - No border drawing at all.
                    - "table" - only a border around the whole table.
                    - "tablecols" - table and column borders. (default)
                    - "header" - only border under header.
                    - "cols" - only vertical borders.
                    - "incols" - vertical borders, no outer edges.
                    - "rows" - only borders between rows.
                    - "cells" - border around all cells.
            border_width (int, optional): Width of table borders, if border is active.
                Note that widths wider than 1 may give artifacts in the corners. Default is 1.
            corner_char (str, optional): Character to use in corners when border is active.
                Default is `+`.
            corner_top_left_char (str, optional): Character used for "nw" corner of table.
                Defaults to `corner_char`.
            corner_top_right_char (str, optional): Character used for "ne" corner of table.
            corner_bottom_left_char (str, optional): Character used for "sw" corner of table.
            corner_bottom_right_char (str, optional): Character used for "se" corner of table.
            pretty_corners (bool, optional): Use custom characters to
                make the table corners look "rounded". Uses UTF-8
                characters. Defaults to `False` for maximum compatibility with various displays
                that may occationally have issues with UTF-8 characters.
            header_line_char (str, optional): Character to use for underlining
                the header row (default is '~'). Requires `border` to not be `None`.
            width (int, optional): Fixed width of table. If not set,
                width is set by the total width of each column.  This will
                resize individual columns in the vertical direction to fit.
            height (int, optional): Fixed height of table. Defaults to being unset. Width is
                still given precedence. If given, table cells will crop text rather
                than expand vertically.
            evenwidth (bool, optional): Used with the `width` keyword. Adjusts columns to have as
                even width as possible. This often looks best also for mixed-length tables. Default
                is `False`.
            maxwidth (int, optional):  This will set a maximum width
                of the table while allowing it to be smaller. Only if it grows wider than this
                size will it be resized by expanding horizontally (or crop `height` is given).
                This keyword has no meaning if `width` is set.

        Raises:
            Exception: If given erroneous input or width settings for the data.

        Notes:
            Beyond those table-specific keywords, the non-overlapping keywords
            of `EvCell.__init__` are also available. These will be passed down
            to every cell in the table.

        """
        # at this point table is a 2D grid - a list of columns
        # x is the column position, y the row
        table = kwargs.pop("table", [])

        # header is a list of texts. We merge it to the table's top
        header = [_to_ansi(head) for head in args]
        self.header = header != []
        if self.header:
            if table:
                excess = len(header) - len(table)
                if excess > 0:
                    # header bigger than table
                    table.extend([] for _ in range(excess))
                elif excess < 0:
                    # too short header
                    header.extend(_to_ansi(["" for _ in range(abs(excess))]))
                for ix, heading in enumerate(header):
                    table[ix].insert(0, heading)
            else:
                table = [[heading] for heading in header]
        # even though we inserted the header, we can still turn off
        # header border underling etc. We only allow this if a header
        # was actually set
        self.header = kwargs.pop("header", self.header) if self.header else False
        hchar = kwargs.pop("header_line_char", "~")
        self.header_line_char = hchar[0] if hchar else "~"

        border = kwargs.pop("border", "tablecols")
        if border is None:
            border = "none"
        if border not in (
            "none",
            "table",
            "tablecols",
            "header",
            "incols",
            "cols",
            "rows",
            "cells",
        ):
            raise Exception("Unsupported border type: '%s'" % border)
        self.border = border

        # border settings are passed into Cell as well (so kwargs.get and not pop)
        self.border_width = kwargs.get("border_width", 1)
        self.corner_char = kwargs.get("corner_char", "+")
        pcorners = kwargs.pop("pretty_corners", False)
        self.corner_top_left_char = _to_ansi(
            kwargs.pop("corner_top_left_char", "." if pcorners else self.corner_char)
        )
        self.corner_top_right_char = _to_ansi(
            kwargs.pop("corner_top_right_char", "." if pcorners else self.corner_char)
        )
        self.corner_bottom_left_char = _to_ansi(
            kwargs.pop("corner_bottom_left_char", " " if pcorners else self.corner_char)
        )
        self.corner_bottom_right_char = _to_ansi(
            kwargs.pop("corner_bottom_right_char", " " if pcorners else self.corner_char)
        )

        self.width = kwargs.pop("width", None)
        self.height = kwargs.pop("height", None)
        self.evenwidth = kwargs.pop("evenwidth", False)
        self.maxwidth = kwargs.pop("maxwidth", None)
        if self.maxwidth and self.width and self.maxwidth < self.width:
            raise Exception("table maxwidth < table width!")
        # size in cell cols/rows
        self.ncols = len(table)
        self.nrows = max(len(col) for col in table) if table else 0
        # size in characters (gets set when _balance is called)
        self.nwidth = 0
        self.nheight = 0
        # save options
        self.options = kwargs

        # use the temporary table to generate the table on the fly, as a list of EvColumns
        self.table = []
        for col in table:
            if isinstance(col, EvColumn):
                self.add_column(col, **kwargs)
            elif isinstance(col, (list, tuple)):
                self.table.append(EvColumn(*col, **kwargs))
            else:
                    "EvTable 'table' kwarg must be a list of EvColumns or a list-of-lists of"
                    f" strings. Found {type(col)}."
                )

        # self.table = [EvColumn(*col, **kwargs) for col in table]

        # this is the actual working table
        self.worktable = None

        # balance the table
        # self._balance()

    def _cellborders(self, ix, iy, nx, ny, **kwargs):
        """
        Adds borders to the table by adjusting the input kwarg to
        instruct cells to build a border in the right positions.

        Args:
            ix (int): x index positions in table.
            iy (int): y index positions in table.
            nx (int): x size of table.
            ny (int): y size of table.

            Keywords as per `EvTable.__init__`.

        Returns:
            table (str): string with the correct borders.

        Notes:
            A copy of the kwarg is returned to the cell. This is method
            is called by self._borders.

        """

        ret = kwargs.copy()

        # handle the various border modes
        border = self.border
        header = self.header

        bwidth = self.border_width
        headchar = self.header_line_char

        def corners(ret):
            """Handle corners of table"""
            if ix == 0 and iy == 0:
                ret["corner_top_left_char"] = self.corner_top_left_char
            if ix == nx and iy == 0:
                ret["corner_top_right_char"] = self.corner_top_right_char
            if ix == 0 and iy == ny:
                ret["corner_bottom_left_char"] = self.corner_bottom_left_char
            if ix == nx and iy == ny:
                ret["corner_bottom_right_char"] = self.corner_bottom_right_char
            return ret

        def left_edge(ret):
            """add vertical border along left table edge"""
            if ix == 0:
                ret["border_left"] = bwidth
                # ret["trim_horizontal"] = bwidth
            return ret

        def top_edge(ret):
            """add border along top table edge"""
            if iy == 0:
                ret["border_top"] = bwidth
                # ret["trim_vertical"] = bwidth
            return ret

        def right_edge(ret):
            """add vertical border along right table edge"""
            if ix == nx:  # and 0 < iy < ny:
                ret["border_right"] = bwidth
                # ret["trim_horizontal"] = 0
            return ret

        def bottom_edge(ret):
            """add border along bottom table edge"""
            if iy == ny:
                ret["border_bottom"] = bwidth
            return ret

        def cols(ret):
            """Adding vertical borders inside the table"""
            if 0 <= ix < nx:
            return ret

        def rows(ret):
            """Adding horizontal borders inside the table"""
            if 0 <= iy < ny:
            return ret

        def head(ret):
            """Add header underline"""
            if iy == 0:
                # put different bottom line for header
                ret["border_bottom_char"] = headchar
            return ret

        # use the helper functions to define various
        # table "styles"

        if border in ("table", "tablecols", "cells"):
            ret = bottom_edge(right_edge(top_edge(left_edge(corners(ret)))))
        if border in ("cols", "tablecols", "cells"):
            ret = cols(right_edge(left_edge(ret)))
        if border in "incols":
            ret = cols(ret)
        if border in ("rows", "cells"):
            ret = rows(bottom_edge(top_edge(ret)))
        if header and border not in ("none", None):
            ret = head(ret)

        return ret

    def _borders(self):
        """
        Add borders to table. This is called from self._balance.
        """
        nx, ny = self.ncols - 1, self.nrows - 1
        options = self.options
        for ix, col in enumerate(self.worktable):
            for iy, cell in enumerate(col):
                col.reformat_cell(iy, **self._cellborders(ix, iy, nx, ny, **options))

    def _balance(self):
        """
        Balance the table. This means to make sure
        all cells on the same row have the same height,
        that all columns have the same number of rows
        and that the table fits within the given width.
        """

        # we make all modifications on a working copy of the
        # actual table. This allows us to add columns/rows
        # and re-balance over and over without issue.
        self.worktable = deepcopy(self.table)
        #        self._borders()
        #        return
        options = copy(self.options)

        # balance number of rows to make a rectangular table
        # column by column
        ncols = len(self.worktable)
        nrows = [len(col) for col in self.worktable]
        nrowmax = max(nrows) if nrows else 0
        for icol, nrow in enumerate(nrows):
            self.worktable[icol].reformat(**options)
            if nrow < nrowmax:
                # add more rows to too-short columns
                empty_rows = ["" for _ in range(nrowmax - nrow)]
                self.worktable[icol].add_rows(*empty_rows)
        self.ncols = ncols
        self.nrows = nrowmax

        # add borders - these add to the width/height, so we must do this before calculating
        # width/height
        self._borders()

        # equalize widths within each column
        cwidths = [max(cell.get_width() for cell in col) for col in self.worktable]

        if self.width or self.maxwidth and self.maxwidth < sum(cwidths):
            # we set a table width. Horizontal cells will be evenly distributed and
            # expand vertically as needed (unless self.height is set, see below)

            # use fixed width, or set to maxwidth
            width = self.width if self.width else self.maxwidth

            if ncols:
                # get minimum possible cell widths for each row
                cwidths_min = [max(cell.get_min_width() for cell in col) for col in self.worktable]
                cwmin = sum(cwidths_min)

                # get which cols have separately set widths - these should be locked
                # note that we need to remove cwidths_min for each lock to avoid counting
                # it twice (in cwmin and in locked_cols)
                locked_cols = {
                    icol: col.options["width"] - cwidths_min[icol]
                    for icol, col in enumerate(self.worktable)
                    if "width" in col.options
                }
                locked_width = sum(locked_cols.values())

                excess = width - cwmin - locked_width

                if len(locked_cols) >= ncols and excess:
                    # we can't adjust the width at all - all columns are locked
                    raise Exception(
                        "Cannot balance table to width %s - "
                        "all columns have a set, fixed width summing to %s!"
                        % (self.width, sum(cwidths))
                    )

                if excess < 0:
                    # the locked cols makes it impossible
                        "Cannot shrink table width to %s. "
                        "Minimum size (and/or fixed-width columns) "
                        "sets minimum at %s." % (self.width, cwmin + locked_width)
                    )

                if self.evenwidth:
                    # make each column of equal width
                    # use cwidths as a work-array to track weights
                    cwidths = copy(cwidths_min)
                    correction = 0
                    while correction < excess:
                        # flood-fill the minimum table starting with the smallest columns
                        ci = cwidths.index(min(cwidths))
                        if ci in locked_cols:
                            # locked column, make sure it's not picked again
                            cwidths[ci] += 9999
                            cwidths_min[ci] = locked_cols[ci]
                        else:
                            cwidths_min[ci] += 1
                            correction += 1
                    cwidths = cwidths_min
                else:
                    # make each column expand more proportional to their data size
                    # we use cwidth as a work-array to track weights
                        # fill wider columns first
                        ci = cwidths.index(max(cwidths))
                            cwidths[ci] -= 9999
                        else:
                            # give a just changed col less prio next run
                            cwidths[ci] -= 3

        # reformat worktable (for width align)
            try:
                col.reformat(width=cwidths[ix], **options)
                raise

        # equalize heights for each row (we must do this here, since it may have changed to fit new
        # widths)
        cheights = [
            max(cell.get_height() for cell in (col[iy] for col in self.worktable))
            for iy in range(nrowmax)
        ]

        if self.height:
            # if we are fixing the table height, it means cells must crop text instead of resizing.
            if nrowmax:
                # get minimum possible cell heights for each column
                cheights_min = [
                    max(cell.get_min_height() for cell in (col[iy] for col in self.worktable))
                    for iy in range(nrowmax)
                ]
                chmin = sum(cheights_min)

                # get which cols have separately set heights - these should be locked
                # note that we need to remove cheights_min for each lock to avoid counting
                # it twice (in chmin and in locked_cols)
                    icol: col.options["height"] - cheights_min[icol]
                    if "height" in col.options
                }
                locked_height = sum(locked_cols.values())

                excess = self.height - chmin - locked_height

                if chmin > self.height:
                    # we cannot shrink any more
                        "Cannot shrink table height to %s. Minimum "
                        "size (and/or fixed-height rows) sets minimum at %s."
                        % (self.height, chmin + locked_height)
                    )

                # Add all the excess at the end of the table
                # Note: Older solutions tried to balance individual
                # rows' vsize. This could lead to empty rows that
                # looked like a bug. This solution instead
                # adds empty rows at the end which is less sophisticated
                # but much more visually consistent.
                cheights_min[-1] += excess
                cheights = cheights_min

                # we must tell cells to crop instead of expanding
            options["enforce_size"] = True

        # reformat table (for vertical align)
                try:
                    col.reformat_cell(iy, height=cheights[iy], **options)
                    msg = "ix=%s, iy=%s, height=%s: %s" % (ix, iy, cheights[iy], e.message)
                    raise Exception("Error in vertical align:\n %s" % msg)

        # calculate actual table width/height in characters
        self.cwidth = sum(cwidths)
        self.cheight = sum(cheights)

    def _generate_lines(self):
        """
        Generates lines across all columns
        (each cell may contain multiple lines)
        This will also balance the table.
        """
        self._balance()
        for iy in range(self.nrows):
            cell_row = [col[iy] for col in self.worktable]
            # this produces a list of lists, each of equal length
            cell_data = [cell.get() for cell in cell_row]
            cell_height = min(len(lines) for lines in cell_data)
            for iline in range(cell_height):
                yield ANSIString("").join(_to_ansi(celldata[iline] for celldata in cell_data))

    def add_header(self, *args, **kwargs):
        """
        Add header to table. This is a number of texts to be put at
        the top of the table. They will replace an existing header.

        Args:
            args (str): These strings will be used as the header texts.

            Same keywords as per `EvTable.__init__`. Will be applied
            to the new header's cells.

        """
        self.header = True
        self.add_row(ypos=0, *args, **kwargs)

    def add_column(self, *args, **kwargs):
        """
        Add a column to table. If there are more rows in new column
        than there are rows in the current table, the table will
        expand with empty rows in the other columns. If too few, the
        new column with get new empty rows. All filling rows are added
        to the end.

        Args:
            args (`EvColumn` or multiple strings): Either a single EvColumn instance or
                a number of data string arguments to be used to create a new column.
            header (str, optional): The header text for the column
            xpos (int, optional): Index position in table *before* which
               to input new column. If not given, column will be added to the end
               of the table. Uses Python indexing (so first column is `xpos=0`)

            Other keywords as per `Cell.__init__`.

        """
        # this will replace default options with new ones without changing default
        options = dict(list(self.options.items()) + list(kwargs.items()))

        xpos = kwargs.get("xpos", None)

        if args and isinstance(args[0], EvColumn):
            column = args[0]
            column.reformat(**kwargs)
        else:
            column = EvColumn(*args, **options)
        wtable = self.ncols
        htable = self.nrows

        header = kwargs.get("header", None)
        if header:
            column.add_rows(str(header), ypos=0, **options)
            self.header = True
        elif self.header:
            # we have a header already. Offset
            column.add_rows("", ypos=0, **options)

        # Calculate whether the new column needs to expand to the
        # current table size, or if the table needs to expand to
        # the column size.
        # This needs to happen after the header rows have already been
        # added to the column in order for the size calculations to match.
        excess = len(column) - htable
        if excess > 0:
            # we need to add new rows to table
            for col in self.table:
                empty_rows = ["" for _ in range(excess)]
                col.add_rows(*empty_rows, **options)
            self.nrows += excess
        elif excess < 0:
            # we need to add new rows to new column
            empty_rows = ["" for _ in range(abs(excess))]
            column.add_rows(*empty_rows, **options)

        if xpos is None or xpos > wtable - 1:
            self.table.append(column)
        else:
            # insert column
            xpos = min(wtable - 1, max(0, int(xpos)))
            self.table.insert(xpos, column)
        self.ncols += 1

    def add_row(self, *args, **kwargs):
        """
        Add a row to table (not a header). If there are more cells in
        the given row than there are cells in the current table the
        table will be expanded with empty columns to match. These will
        be added to the end of the table. In the same way, adding a
        line with too few cells will lead to the last ones getting
        padded.

        Args:
            args (str): Any number of string argumnets to use as the
                data in the row (one cell per argument).
            ypos (int, optional): Index position in table before which to
                 input new row. If not given, will be added to the end of the table.
                 Uses Python indexing (so first row is `ypos=0`)

            Other keywords are as per `EvCell.__init__`.

        """
        row = list(args)

        excess = len(row) - wtable

            # we need to add new empty columns to table
            empty_rows = ["" for _ in range(htable)]
            self.table.extend([EvColumn(*empty_rows, **options) for _ in range(excess)])
            # we need to add more cells to row
            row.extend(["" for _ in range(abs(excess))])
        self.ncols = len(self.table)

        if ypos is None or ypos > htable - 1:
            # add new row to the end
            for icol, col in enumerate(self.table):
                col.add_rows(row[icol], **options)
        else:
            # insert row elsewhere
            ypos = min(htable - 1, max(0, int(ypos)))
                col.add_rows(row[icol], ypos=ypos, **options)
        self.nrows += 1

        """
        Force a re-shape of the entire table.

            Table options as per `EvTable.__init__`.

        """
        self.width = kwargs.pop("width", self.width)
        self.height = kwargs.pop("height", self.height)

        hchar = kwargs.pop("header_line_char", self.header_line_char)

        # border settings are also passed on into EvCells (so kwargs.get, not kwargs.pop)
        self.header_line_char = hchar[0] if hchar else self.header_line_char
        self.border_width = kwargs.get("border_width", self.border_width)
        self.corner_char = kwargs.get("corner_char", self.corner_char)
        self.header_line_char = kwargs.get("header_line_char", self.header_line_char)

        self.corner_top_left_char = _to_ansi(kwargs.pop("corner_top_left_char", self.corner_char))
        self.corner_top_right_char = _to_ansi(kwargs.pop("corner_top_right_char", self.corner_char))
            kwargs.pop("corner_bottom_left_char", self.corner_char)
        )
            kwargs.pop("corner_bottom_right_char", self.corner_char)
        )

        self.options.update(kwargs)

    def reformat_column(self, index, **kwargs):
        """
        Sends custom options to a specific column in the table.

        Args:
            index (int): Which column to reformat. The column index is
                given from 0 to Ncolumns-1.

            Column options as per `EvCell.__init__`.

        Raises:
            Exception: if an invalid index is found.

        """
        if index > len(self.table):
            raise Exception("Not a valid column index")
        # we update the columns' options which means eventual width/height
        # will be 'locked in' and withstand auto-balancing width/height from the table later
        self.table[index].options.update(kwargs)
        self.table[index].reformat(**kwargs)

        """
        Return lines of table as a list.

        Returns:
            table_lines (list): The lines of the table, in order.

        """
        return [line for line in self._generate_lines()]

        """print table (this also balances it)"""
        # h = "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
        return str(str(ANSIString("\n").join([line for line in self._generate_lines()])))

"""
Generic function parser for functions embedded in a string, on the form
`$funcname(*args, **kwargs)`, for example:

```
"A string $foo() with $bar(a, b, c, $moo(), d=23) etc."
```

Each arg/kwarg can also be another nested function. These will be executed
inside-out and their return will used as arguments for the enclosing function
(so the same as for regular Python function execution).

This is the base for all forms of embedded func-parsing, like inlinefuncs and
protfuncs. Each function available to use must be registered as a 'safe'
function for the parser to accept it. This is usually done in a module with
regular Python functions on the form:

```python
# in a module whose path is passed to the parser

def _helper(x):
    # use underscore to NOT make the function available as a callable

def funcname(*args, **kwargs):
    # this can be accessed as $funcname(*args, **kwargs)
    # it must always accept *args and **kwargs.
    ...
    return something
```

Usage:

```python

parser = FuncParser("path.to.module_with_callables")
result = parser.parse("String with $funcname() in it")

```

The `FuncParser` also accepts a direct dict mapping of `{'name': callable, ...}`.

---

"""
import dataclasses

from evennia.utils import logger, search
    crop,
    int2str,
    justify,
    pad,
    safe_convert_to_types,
)
from evennia.utils.verb_conjugation.conjugate import verb_actor_stance_components
from evennia.utils.verb_conjugation.pronouns import pronoun_to_viewpoints

# setup

_CLIENT_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
_MAX_NESTING = settings.FUNCPARSER_MAX_NESTING
_START_CHAR = settings.FUNCPARSER_START_CHAR
_ESCAPE_CHAR = settings.FUNCPARSER_ESCAPE_CHAR

@dataclasses.dataclass
class _ParsedFunc:
    """
    Represents a function parsed from the string

    """

    prefix: str = _START_CHAR
    funcname: str = ""
    args: list = dataclasses.field(default_factory=list)
    kwargs: dict = dataclasses.field(default_factory=dict)

    # state storage
    fullstr: str = ""
    infuncstr: str = ""
    rawstr: str = ""
    double_quoted: int = -1
    current_kwarg: str = ""
    open_lparens: int = 0
    open_lsquate: int = 0
    open_lcurly: int = 0
    exec_return = ""

        return self.funcname, self.args, self.kwargs

        return self.prefix + self.rawstr + self.infuncstr

class ParsingError(RuntimeError):
    """
    Failed to parse for some reason.
    """

    pass

class FuncParser:
    """
    Sets up a parser for strings containing `$funcname(*args, **kwargs)`
    substrings.

    """

        self,
        callables,
        start_char=_START_CHAR,
        escape_char=_ESCAPE_CHAR,
        max_nesting=_MAX_NESTING,
        **default_kwargs,
    ):
        """
        Initialize the parser.

        Args:
            callables (str, module, list or dict): Where to find
                'safe' functions to make available in the parser. If a `dict`,
                it should be a direct mapping `{"funcname": callable, ...}`. If
                one or mode modules or module-paths, the module(s) are first checked
                for a dict `FUNCPARSER_CALLABLES = {"funcname", callable, ...}`. If
                no such variable exists, all callables in the module (whose name does
                not start with an underscore) will be made available to the parser.
            start_char (str, optional): A character used to identify the beginning
                of a parseable function. Default is `$`.
            escape_char (str, optional): Prepend characters with this to have
                them not count as a function. Default is the backtick, `\\\\`.
            max_nesting (int, optional): How many levels of nested function calls
                are allowed, to avoid exploitation. Default is 20.
            **default_kwargs: These kwargs will be passed into all callables. These
                kwargs can be overridden both by kwargs passed direcetly to `.parse` *and*
                by kwargs given directly in the string `$funcname` call. They are
                suitable for global defaults that is intended to be changed by the
                user. To guarantee a call always gets a particular kwarg, pass it
                into `.parse` as `**reserved_kwargs` instead.

        """
        if isinstance(callables, dict):
            loaded_callables = {**callables}
        else:
            # load all modules/paths in sequence. Later-added will override
            # earlier same-named callables (allows for overriding evennia defaults)
            loaded_callables = {}
            for module_or_path in make_iter(callables):
                callables_mapping = variable_from_module(
                    module_or_path, variable="FUNCPARSER_CALLABLES"
                )
                if callables_mapping:
                    try:
                        # mapping supplied in variable
                        loaded_callables.update(callables_mapping)
                        raise ParsingError(
                            f"Failure to parse - {module_or_path}.FUNCPARSER_CALLABLES "
                            "(must be a dict {'funcname': callable, ...})"
                        )
                else:
                    # use all top-level variables
                    # (handles both paths and module instances
                    loaded_callables.update(callables_from_module(module_or_path))
        self.validate_callables(loaded_callables)
        self.callables = loaded_callables
        self.escape_char = escape_char
        self.start_char = start_char
        self.default_kwargs = default_kwargs

    def validate_callables(self, callables):
        """
        Validate the loaded callables. Each callable must support at least
        `funcname(*args, **kwargs)`.
        property.

        Args:
            callables (dict): A mapping `{"funcname": callable, ...}` to validate

        Raise:
            AssertionError: If invalid callable was found.

        Notes:
            This is also a good method to override for individual parsers
            needing to run any particular pre-checks.

        """
        for funcname, clble in callables.items():
            try:
                mapping = inspect.getfullargspec(clble)
                logger.log_trace(f"Could not run getfullargspec on {funcname}: {clble}")
            else:
                assert mapping.varargs, f"Parse-func callable '{funcname}' does not support *args."
                assert mapping.varkw, f"Parse-func callable '{funcname}' does not support **kwargs."

    def execute(self, parsedfunc, raise_errors=False, **reserved_kwargs):
        """
        Execute a parsed function

        Args:
            parsedfunc (_ParsedFunc): This dataclass holds the parsed details
                of the function.
            raise_errors (bool, optional): Raise errors. Otherwise return the
                string with the function unparsed.
            **reserved_kwargs: These kwargs are _guaranteed_ to always be passed into
                the callable on every call. It will override any default kwargs
                _and_ also a same-named kwarg given manually in the $funcname
                call. This is often used by Evennia to pass required data into
                the callable, for example the current Session for inlinefuncs.
        Returns:
            any: The result of the execution. If this is a nested function, it
                can be anything, otherwise it will be converted to a string later.
                Always a string on un-raised error (the unparsed function string).

        Raises:
            ParsingError, any: A `ParsingError` if the function could not be
            found, otherwise error from function definition. Only raised if
            `raise_errors` is `True`

        Notes:
            The kwargs passed into the callable will be a mixture of the
            `default_kwargs` passed into `FuncParser.__init__`, kwargs given
            directly in the `$funcdef` string, and the `reserved_kwargs` this
            function gets from `.parse()`. For colliding keys, funcdef-defined
            kwargs will override default kwargs while reserved kwargs will always
            override the other two.

        """
        funcname, args, kwargs = parsedfunc.get()
        func = self.callables.get(funcname)

        if not func:
            if raise_errors:
                available = ", ".join(f"'{key}'" for key in self.callables)
                raise ParsingError(
                    f"Unknown parsed function '{str(parsedfunc)}' (available: {available})"
                )
            return str(parsedfunc)

        # build kwargs in the proper priority order
        kwargs = {
            **self.default_kwargs,
            **kwargs,
            **reserved_kwargs,
            **{"funcparser": self, "raise_errors": raise_errors},
        }

        try:
            ret = func(*args, **kwargs)
            return ret
        except ParsingError:
                raise
                raise

    def parse(
        self,
        string,
        raise_errors=False,
        escape=False,
        strip=False,
        **reserved_kwargs,
    ):
        """
        Use parser to parse a string that may or may not have
        `$funcname(*args, **kwargs)` - style tokens in it. Only the callables
        used to initiate the parser will be eligible for parsing.

        Args:
            raise_errors (bool, optional): By default, a failing parse just
                means not parsing the string but leaving it as-is. If this is
                `True`, errors (like not closing brackets) will lead to an
                ParsingError.
            escape (bool, optional): If set, escape all found functions so they
                are not executed by later parsing.
            strip (bool, optional): If set, strip any inline funcs from string
                as if they were not there.
            return_str (bool, optional): If set (default), always convert the
                parse result to a string, otherwise return the result of the
                latest called inlinefunc (if called separately).
            **reserved_kwargs: If given, these are guaranteed to _always_ pass
                as part of each parsed callable's **kwargs. These  override
                same-named default options given in `__init__` as well as any
                same-named kwarg given in the string function. This is because
                it is often used by Evennia to pass necessary kwargs into each
                callable (like the current Session object for inlinefuncs).

        Returns:
            str or any: The parsed string, or the same string on error (if
                `raise_errors` is `False`). This is always a string

        Raises:
            ParsingError: If a problem is encountered and `raise_errors` is True.

        """
        start_char = self.start_char
        escape_char = self.escape_char

        # replace e.g. $$ with \$ so we only need to handle one escape method
        string = string.replace(start_char + start_char, escape_char + start_char)

        # parsing state
        callstack = []

        double_quoted = -1
        open_lparens = 0  # open (
        open_lsquare = 0  # open [
        open_lcurly = 0  # open {
        escaped = False
        current_kwarg = ""
        exec_return = ""

        curr_func = None
        fullstr = ""  # final string
        infuncstr = ""  # string parts inside the current level of $funcdef (including $)
        literal_infuncstr = False

        for char in string:
            if escaped:
                # always store escaped characters verbatim
                if curr_func:
                    infuncstr += char
                else:
                    fullstr += char
                escaped = False
                continue

            if char == escape_char:
                # don't store the escape-char itself
                escaped = True
                continue

            if char == start_char:
                # start a new function definition (not escaped as $$)

                    # we are starting a nested funcdef
                    if len(callstack) >= _MAX_NESTING - 1:
                        # stack full - ignore this function
                        if raise_errors:
                            raise ParsingError(
                                "Only allows for parsing nesting function defs "
                                f"to a max depth of {_MAX_NESTING}."
                            )
                        infuncstr += char
                        continue
                    else:
                        # store state for the current func and stack it
                        curr_func.current_kwarg = current_kwarg
                        curr_func.infuncstr = infuncstr
                        curr_func.double_quoted = double_quoted
                        curr_func.open_lparens = open_lparens
                        curr_func.open_lsquare = open_lsquare
                        curr_func.open_lcurly = open_lcurly
                        # we must strip the remaining funcstr so it's not counted twice
                        curr_func.rawstr = curr_func.rawstr[: -len(infuncstr)]
                        current_kwarg = ""
                        infuncstr = ""
                        double_quoted = -1
                        open_lparens = 0
                        open_lsquare = 0
                        open_lcurly = 0
                        exec_return = ""
                        literal_infuncstr = False
                        callstack.append(curr_func)

                # start a new func
                curr_func = _ParsedFunc(prefix=char, fullstr=char)
                continue

            if not curr_func:
                # a normal piece of string
                fullstr += char
                # this must always be a string
                return_str = True
                continue

            # in a function def (can be nested)

            curr_func.rawstr += char

            if exec_return != "" and char not in (",=)"):
                # if exec_return is followed by any other character
                # than one demarking an arg,kwarg or function-end
                # it must immediately merge as a string
                infuncstr += str(exec_return)
                exec_return = ""

            if char == '"':  # note that this is the same as '\"'
                # a double quote = flip status
                if double_quoted == 0:
                    infuncstr = infuncstr[1:]
                    double_quoted = -1
                elif double_quoted > 0:
                    prefix = infuncstr[0:double_quoted]
                    infuncstr = prefix + infuncstr[double_quoted + 1 :]
                else:
                    infuncstr = infuncstr.strip()
                    double_quoted = len(infuncstr) - 1
                    literal_infuncstr = True

                continue

            if double_quoted >= 0:
                # inside a string definition - this escapes everything else
                infuncstr += char
                continue

            # special characters detected inside function def
            if char == "(":
                if not curr_func.funcname:
                    # end of a funcdef name
                    curr_func.funcname = infuncstr
                    curr_func.fullstr += infuncstr + char
                    infuncstr = ""
                else:
                    # just a random left-parenthesis
                # track the open left-parenthesis
                open_lparens += 1
                continue

            if char in "[]":
                # a square bracket - start/end of a list?
                open_lsquare += -1 if char == "]" else 1
                continue

            if char in "{}":
                # a curly bracket - start/end of dict/set?
                open_lcurly += -1 if char == "}" else 1
                continue

            if char == "=":
                # beginning of a keyword argument
                if exec_return != "":
                    infuncstr = exec_return
                current_kwarg = infuncstr.strip()
                curr_func.kwargs[current_kwarg] = ""
                curr_func.fullstr += infuncstr + char
                infuncstr = ""
                continue

            if char in (",)"):
                # commas and right-parens may indicate arguments ending

                if open_lparens > 1:
                    # one open left-parens is ok (beginning of arglist), more
                    # indicate we are inside an unclosed, nested (, so
                    # we need to not count this as a new arg or end of funcdef.
                    open_lparens -= 1 if char == ")" else 0
                    continue

                if open_lcurly > 0 or open_lsquare > 0:
                    # also escape inside an open [... or {... structure
                    continue

                    # store the execution return as-received
                    if current_kwarg:
                        curr_func.kwargs[current_kwarg] = exec_return
                    else:
                        curr_func.args.append(exec_return)
                else:
                    if not literal_infuncstr:
                        infuncstr = infuncstr.strip()

                    # store a string instead
                        curr_func.kwargs[current_kwarg] = infuncstr
                    elif literal_infuncstr or infuncstr.strip():
                        # don't store the empty string
                        curr_func.args.append(infuncstr)

                # note that at this point either exec_return or infuncstr will
                # be empty. We need to store the full string so we can print
                # it 'raw' in case this funcdef turns out to e.g. lack an
                # ending paranthesis
                curr_func.fullstr += str(exec_return) + infuncstr + char

                current_kwarg = ""
                literal_infuncstr = False

                if char == ")":
                    # closing the function list - this means we have a
                    # ready function-def to run.
                    open_lparens = 0

                    if strip:
                        # remove function as if it returned empty
                    elif escape:
                        # get function and set it as escaped
                        exec_return = escape_char + curr_func.fullstr
                    else:
                        # execute the function - the result may be a string or
                        # something else
                        exec_return = self.execute(
                            curr_func, raise_errors=raise_errors, **reserved_kwargs
                        )

                    if callstack:
                        # unnest the higher-level funcdef from stack
                        # and continue where we were
                        curr_func = callstack.pop()
                        current_kwarg = curr_func.current_kwarg
                        if curr_func.infuncstr:
                            # if we have an ongoing string, we must merge the
                            # exec into this as a part of that string
                            infuncstr = curr_func.infuncstr + str(exec_return)
                            exec_return = ""
                        curr_func.infuncstr = ""
                        double_quoted = curr_func.double_quoted
                        open_lparens = curr_func.open_lparens
                        open_lsquare = curr_func.open_lsquare
                        open_lcurly = curr_func.open_lcurly
                    else:
                        # back to the top-level string - this means the
                        # exec_return should always be converted to a string.
                        curr_func = None
                        fullstr += str(exec_return)
                        if return_str:
                continue

            infuncstr += char

        if curr_func:
            # if there is a still open funcdef or defs remaining in callstack,
            # these are malformed (no closing bracket) and we should get their
            # strings as-is.
            callstack.append(curr_func)
            for inum, _ in enumerate(range(len(callstack))):
                funcstr = str(callstack.pop())
                if inum == 0 and funcstr.endswith(infuncstr):
                    # avoid double-echo of nested function calls. This should
                    # produce a good result most of the time, but it's not 100%
                    # guaranteed to, since it can ignore genuine duplicates
                    infuncstr = funcstr
                else:
                    infuncstr = funcstr + infuncstr

        if not return_str and exec_return != "":
            # return explicit return
            return exec_return

        # add the last bit to the finished string
        fullstr += infuncstr

        return fullstr

    def parse_to_any(
        self, string, raise_errors=False, escape=False, strip=False, **reserved_kwargs
    ):
        """
        This parses a string and if the string only contains a "$func(...)",
        the return will be the return value of that function, even if it's not
        a string. If mixed in with other strings, the result will still always
        be a string.

        Args:
            raise_errors (bool, optional): If unset, leave a failing (or
                unrecognized) inline function as unparsed in the string. If set,
                raise an ParsingError.

        Returns:
            any: The return from the callable. Or string if the callable is not
                given alone in the string.

        Raises:

        Notes:
            This is a convenience wrapper for `self.parse(..., return_str=False)` which
            accomplishes the same thing.

        Examples:
            ::

                from ast import literal_eval
                from evennia.utils.funcparser import FuncParser

                def ret1(*args, **kwargs):
                    return 1

                parser = FuncParser({"lit": lit})

                assert parser.parse_to_any("$ret1()" == 1
                assert parser.parse_to_any("$ret1() and text" == '1 and text'

        """
        return self.parse(
            string,
            raise_errors=raise_errors,
            escape=escape,
            strip=strip,
            return_str=False,
        )

#
# Default funcparser callables. These are made available from this module's
# FUNCPARSER_CALLABLES.
#

def funcparser_callable_eval(*args, **kwargs):
    """
    Funcparser callable. This will combine safe evaluations to try to parse the
    incoming string into a python object. If it fails, the return will be same
    as the input.

    Args:
        string (str): The string to parse. Only simple literals or operators are allowed.

    Returns:
        any: The string parsed into its Python form, or the same as input.

    Examples:
        - `$py(1) -> 1`
        - `$py([1,2,3,4] -> [1, 2, 3]`
        - `$py(3 + 4) -> 7`

    """
    args, kwargs = safe_convert_to_types(("py", {}), *args, **kwargs)
    return args[0] if args else ""

def funcparser_callable_toint(*args, **kwargs):
    """Usage: $toint(43.0) -> 43"""
    inp = funcparser_callable_eval(*args, **kwargs)
    try:
        return int(inp)
        return inp
        return inp

def funcparser_callable_int2str(*args, **kwargs):
    """
    Usage: $int2str(1) -> 'one' etc, up to 12->twelve.

    Args:
        number (int): The number. If not an int, will be converted.

    Uses the int2str utility function.
    """
        return ""
    try:
        number = int(args[0])
        return args[0]
    return int2str(number)

def funcparser_callable_an(*args, **kwargs):
    """
    Usage: $an(thing) -> a thing

    Adds a/an depending on if the first letter of the given word is a consonant or not.

    """
        return ""
    item = str(args[0])
    if item and item[0] in "aeiouyAEIOUY":
        return f"an {item}"
    return f"a {item}"

def _apply_operation_two_elements(*args, operator="+", **kwargs):
    """
    Helper operating on two arguments

    Args:
        val1 (any): First value to operate on.
        val2 (any): Second value to operate on.

    Return:
        any: The result of val1 + val2. Values must be
            valid simple Python structures possible to add,
            such as numbers, lists etc. The $eval is usually
            better for non-list arithmetic.

    """
    args, kwargs = safe_convert_to_types((("py", "py"), {}), *args, **kwargs)
    if not len(args) > 1:
        return ""
    val1, val2 = args[0], args[1]
    try:
        if operator == "+":
            return val1 + val2
        elif operator == "-":
            return val1 - val2
        elif operator == "*":
            return val1 * val2
        elif operator == "/":
            return val1 / val2
        if kwargs.get("raise_errors"):
            raise
        return ""

def funcparser_callable_add(*args, **kwargs):
    """Usage: `$add(val1, val2) -> val1 + val2`"""
    return _apply_operation_two_elements(*args, operator="+", **kwargs)

def funcparser_callable_sub(*args, **kwargs):
    """Usage: ``$sub(val1, val2) -> val1 - val2`"""
    return _apply_operation_two_elements(*args, operator="-", **kwargs)

def funcparser_callable_mult(*args, **kwargs):
    """Usage: `$mult(val1, val2) -> val1 * val2`"""
    return _apply_operation_two_elements(*args, operator="*", **kwargs)

def funcparser_callable_div(*args, **kwargs):
    """Usage: `$mult(val1, val2) -> val1 / val2`"""
    return _apply_operation_two_elements(*args, operator="/", **kwargs)

def funcparser_callable_round(*args, **kwargs):
    """
    Funcparser callable. Rounds an incoming float to a
    certain number of significant digits.

    Args:
        inp (str or number): If a string, it will attempt
            to be converted to a number first.
        significant (int): The number of significant digits.  Default is None -
            this will turn the result into an int.

    Returns:
        any: The rounded value or inp if inp was not a number.

    Examples:
        - `$round(3.5434343, 3) -> 3.543`
        - `$round($random(), 2)` - rounds random result, e.g `0.22`

    """
        return ""
    args, _ = safe_convert_to_types(((float, int), {}), *args, **kwargs)

    num, *significant = args
    significant = significant[0] if significant else 0
    try:
        return round(num, significant)
            raise
        return ""

def funcparser_callable_random(*args, **kwargs):
    """
    Funcparser callable. Returns a random number between 0 and 1, from 0 to a
    maximum value, or within a given range (inclusive).

    Args:
        minval (str, optional): Minimum value. If not given, assumed 0.
        maxval (str, optional): Maximum value.

    Notes:
        If either of the min/maxvalue has a '.' in it, a floating-point random
        value will be returned. Otherwise it will be an
        integer value in the given range.

    Examples:
        - `$random()` - random value [0 .. 1) (float).
        - `$random(5)` - random value [0..5] (int)
        - `$random(5.0)` - random value [0..5] (float)
        - `$random(5, 10)` - random value [5..10] (int)
        - `$random(5, 10.0)` - random value [5..10] (float)

    """
    args, _ = safe_convert_to_types((("py", "py"), {}), *args, **kwargs)

    nargs = len(args)
    if nargs == 1:
        # only maxval given
        minval, maxval = 0, args[0]
    elif nargs > 1:
        minval, maxval = args[:2]
    else:
        minval, maxval = 0, 1

    try:
        if isinstance(minval, float) or isinstance(maxval, float):
            return minval + ((maxval - minval) * random.random())
        else:
            return random.randint(minval, maxval)
            raise
        return ""

def funcparser_callable_randint(*args, **kwargs):
    """
    Usage: $randint(start, end):

    Legacy alias - always returns integers.

    """
    return int(funcparser_callable_random(*args, **kwargs))

def funcparser_callable_choice(*args, **kwargs):
    """
    FuncParser callable. Picks a random choice from a list.

    Args:
        listing (list): A list of items to randomly choose between.
            This will be converted from a string to a real list.
        *args: If multiple args are given, will pick one randomly from them.

    Returns:
        any: The randomly chosen element.

    Example:
        - `$choice(key, flower, house)`
        - `$choice([1, 2, 3, 4])`

    """
        return ""

        # this needs to be a list/tuple for this to make sense
        args, _ = safe_convert_to_types(("py", {}), args[0], **kwargs)
        args = make_iter(args[0]) if args else None
    else:
        # separate arg per entry
        converters = ["py" for _ in range(nargs)]
        args, _ = safe_convert_to_types((converters, {}), *args, **kwargs)

        return ""
    try:
        return random.choice(args)
            raise
        return ""

def funcparser_callable_pad(*args, **kwargs):
    """
    FuncParser callable. Pads text to given width, optionally with fill-characters

    Args:
        width (int): Width of padding.
        align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'.
        fillchar (str, optional): Character used for padding. Defaults to a space.

    Example:
        - `$pad(text, 12, r, ' ') -> "        text"`
        - `$pad(text, width=12, align=c, fillchar=-) -> "----text----"`

    """
        return ""
    args, kwargs = safe_convert_to_types(
        ((str, int, str, str), {"width": int, "align": str, "fillchar": str}), *args, **kwargs
    )

    text, *rest = args
    nrest = len(rest)
    try:
        width = int(kwargs.get("width", rest[0] if nrest > 0 else _CLIENT_DEFAULT_WIDTH))
    except (TypeError, ValueError):
        width = _CLIENT_DEFAULT_WIDTH

    align = kwargs.get("align", rest[1] if nrest > 1 else "c")
    fillchar = kwargs.get("fillchar", rest[2] if nrest > 2 else " ")
    if align not in ("c", "l", "r"):
        align = "c"
    return pad(str(text), width=width, align=align, fillchar=fillchar)

def funcparser_callable_crop(*args, **kwargs):
    """
    FuncParser callable. Crops ingoing text to given widths.

    Args:
        text (str, optional): Text to crop.
        width (str, optional): Will be converted to an integer. Width of
            crop in characters.
        suffix (str, optional): End string to mark the fact that a part
            of the string was cropped. Defaults to `[...]`.

    Example:
        - `$crop(A long text, 10, [...]) -> "A lon[...]"`
        - `$crop(text, width=11, suffix='[...]) -> "A long[...]"`

    """
        return ""
    try:
    suffix = kwargs.get("suffix", rest[1] if nrest > 1 else "[...]")
    return crop(str(text), width=width, suffix=str(suffix))

def funcparser_callable_space(*args, **kwarg):
    """
    Usage: $space(43)

    Insert a length of space.

    """
        return ""
    try:
        width = int(args[0])
        width = 1
    return " " * width

def funcparser_callable_justify(*args, **kwargs):
    """
    Justify text across a width, default across screen width.

    Args:
        width (int, optional): Defaults to default screen width.
        align (str, optional): One of 'l', 'c', 'r' or 'f' for 'full'.
        indent (int, optional): Intendation of text block, if any.

    Returns:
        str: The justified text.

    Examples:
        - `$just(text, width=40)`
        - `$just(text, align=r, indent=2)`

    """
        return ""
    lrest = len(rest)
    try:
        width = int(kwargs.get("width", rest[0] if lrest > 0 else _CLIENT_DEFAULT_WIDTH))
    align = str(kwargs.get("align", rest[1] if lrest > 1 else "f"))
    try:
        indent = int(kwargs.get("indent", rest[2] if lrest > 2 else 0))
        indent = 0
    return justify(str(text), width=width, align=align, indent=indent)

# legacy for backwards compatibility
def funcparser_callable_left_justify(*args, **kwargs):
    "Usage: $ljust(text)"
    return funcparser_callable_justify(*args, align="l", **kwargs)

def funcparser_callable_right_justify(*args, **kwargs):
    "Usage: $rjust(text)"
    return funcparser_callable_justify(*args, align="r", **kwargs)

def funcparser_callable_center_justify(*args, **kwargs):
    "Usage: $cjust(text)"
    return funcparser_callable_justify(*args, align="c", **kwargs)

def funcparser_callable_clr(*args, **kwargs):
    """
    FuncParser callable. Colorizes nested text.

    Args:
        startclr (str, optional): An ANSI color abbreviation without the
            prefix `|`, such as `r` (red foreground) or `[r` (red background).
        text (str, optional): Text
        endclr (str, optional): The color to use at the end of the string. Defaults
            to `|n` (reset-color).
    Kwargs:
        color (str, optional): If given,

    Example:
        - `$clr(r, text, n) -> "|rtext|n"`
        - `$clr(r, text) -> "|rtext|n`
        - `$clr(text, start=r, end=n) -> "|rtext|n"`

    """
        return ""

    startclr, text, endclr = "", "", ""
        # $clr(pre, text, post))
        startclr, *rest = args
        if rest:
            text, *endclr = rest
            if endclr:
                endclr = endclr[0]
    else:
        # $clr(text, start=pre, end=post)
        text = args[0]
        startclr = kwargs.get("start", "")
        endclr = kwargs.get("end", "")

    startclr = "|" + startclr if startclr else ""
    endclr = "|" + endclr if endclr else ("|n" if startclr else "")
    return f"{startclr}{text}{endclr}"

def funcparser_callable_pluralize(*args, **kwargs):
    """
    FuncParser callable. Handles pluralization of a word.

    Args:
        singular_word (str): The base (singular) word to optionally pluralize
        number (int): The number of elements; if 1 (or 0), use `singular_word` as-is,
            otherwise use plural form.
        plural_word (str, optional): If given, this will be used if `number`
            is greater than one. If not given, we simply add 's' to the end of
            `singular_word`.

    Example:
        - `$pluralize(thing, 2)` -> "things"
        - `$pluralize(goose, 18, geese)` -> "geese"

    """
        return ""
    if nargs > 2:
        singular_word, number, plural_word = args[:3]
        singular_word, number = args[:2]
        plural_word = f"{singular_word}s"
    else:
        singular_word, number = args[0], 1
    return singular_word if abs(int(number)) in (0, 1) else plural_word

def funcparser_callable_search(*args, caller=None, access="control", **kwargs):
    """
    FuncParser callable. Finds an object based on name or #dbref. Note that
    this requries the parser be called with the caller's Session for proper
    security. If called without session, the call is aborted.

    Args:
        query (str): The key or dbref to search for. This can consist of any args used
            for one of the regular search methods. Also kwargs will be passed into
            the search (except the kwargs given below)

        return_list (bool): If set, return a list of objects with
            0, 1 or more matches to `query`. Defaults to False.
        type (str): One of 'obj', 'account', 'script'
        caller (Entity): Supplied to Parser. This is required and will
            be passed into the access check for the entity being searched for.
            The 'control' permission is required.
        access (str): Which locktype access to check. Unset to disable the
            security check.
        **kwargs: Will be passed into the main search.

    Returns:
        any: An entity match or None if no match or a list if `return_list` is set.

    Raise:
        ParsingError: If zero/multimatch and `return_list` is False, or caller was not
            passed into parser.

    Examples:
        - "$search(#233)"
        - "$search(Tom, type=account)"
        - "$search(meadow, return_list=True)"
        - "$search(beach, category=outdoors, type=tag)

    """
    # clean out funcparser-specific kwargs so we can use the kwargs for
    # searching
    search_kwargs = {
        key: value
        for key, value in kwargs.items()
        if key not in ("funcparser", "raise_errors", "type", "return_list")
    }
    return_list = str(kwargs.pop("return_list", "false")).lower() == "true"

        return [] if return_list else None
    if not caller:
        raise ParsingError("$search requires a `caller` passed to the parser.")

    typ = kwargs.get("type", "obj")
    targets = []
    if typ == "obj":
        targets = search.search_object(*args, **search_kwargs)
    elif typ == "account":
        targets = search.search_account(*args, **search_kwargs)
    elif typ == "script":
        targets = search.search_script(*args, **search_kwargs)
    elif typ == "tag":
        targets = search.search_object_by_tag(*args, **search_kwargs)

    if not targets:
            return []
        raise ParsingError(f"$search: Query '{args[0]}' gave no matches.")

    if len(targets) > 1 and not return_list:
        raise ParsingError(
            "$search: Query '{query}' found {num} matches. "
            "Set return_list=True to accept a list".format(query=query, num=len(targets))
        )

    for target in targets:
        if not target.access(caller, access):
            raise ParsingError("$search Cannot add found entity - access failure.")

    return list(targets) if return_list else targets[0]

def funcparser_callable_search_list(*args, caller=None, access="control", **kwargs):
    """
    Usage: $objlist(#123)

    Legacy alias for search with a return_list=True kwarg preset.

    """
    return funcparser_callable_search(
        *args, caller=caller, access=access, return_list=True, **kwargs
    )

def funcparser_callable_you(
    *args, caller=None, receiver=None, mapping=None, capitalize=False, **kwargs
):
    """
    Usage: $you() or $you(key)

    Replaces with you for the caller of the string, with the display_name
    of the caller for others.

        caller (Object): The 'you' in the string. This is used unless another
            you-key is passed to the callable in combination with `mapping`.
        receiver (Object): The recipient of the string.
        mapping (dict, optional): This is a mapping `{key:Object, ...}` and is
            used to find which object `$you(key)` refers to. If not given, the
            `caller` kwarg is used.
        capitalize (bool): Passed by the You helper, to capitalize you.

    Returns:
        str: The parsed string.

    Raises:
        ParsingError: If `caller` and `receiver` were not supplied.

    Notes:
        The kwargs should be passed the to parser directly.

    Examples:
        This can be used by the say or emote hooks to pass actor stance
        strings. This should usually be combined with the $conj() callable.

        - `With a grin, $you() $conj(jump) at $you(tommy).`

        The caller-object will see "With a grin, you jump at Tommy."
        Tommy will see "With a grin, CharName jumps at you."
        Others will see "With a grin, CharName jumps at Tommy."

    """
    if args and mapping:
        # this would mean a $you(key) form
        caller = mapping.get(args[0], None)

    if not (caller and receiver):
        raise ParsingError("No caller or receiver supplied to $you callable.")

    capitalize = bool(capitalize)
    if caller == receiver:
        return "You" if capitalize else "you"
    return (
        caller.get_display_name(looker=receiver)
        if hasattr(caller, "get_display_name")
        else str(caller)
    )

def funcparser_callable_you_capitalize(
    *args, you=None, receiver=None, mapping=None, capitalize=True, **kwargs
):
    """
    Usage: $You() - capitalizes the 'you' output.

    """
    return funcparser_callable_you(
        *args, you=you, receiver=receiver, mapping=mapping, capitalize=capitalize, **kwargs
    )

def funcparser_callable_your(
):
    """
    Usage: $your() or $your(key)

    Replaces with your for the caller of the string, with the display_name +'s

        caller (Object): The 'your' in the string. This is used unless another
            your-key is passed to the callable in combination with `mapping`.

    Returns:

    Raises:

    Notes:

    Examples:
        strings.

        - `$your() pet jumps at $you(tommy).`

        The caller-object will see "Your pet jumps Tommy."
        Tommy will see "CharName's pet jumps at you."
        Others will see "CharName's pet jumps at Tommy."

    """
        # this would mean a $your(key) form

        raise ParsingError("No caller or receiver supplied to $your callable.")

        return "Your" if capitalize else "your"

    name = (
    )

    return name + "'s"

def funcparser_callable_your_capitalize(
):
    """
    Usage: $Your() - capitalizes the 'your' output.

    """
    return funcparser_callable_your(
    )

def funcparser_callable_conjugate(*args, caller=None, receiver=None, **kwargs):
    """
    Usage: $conj(word, [options])

    Conjugate a verb according to if it should be 2nd or third person.

        caller (Object): The object who represents 'you' in the string.

    Returns:

    Raises:
        ParsingError: If `you` and `recipient` were not both supplied.

    Notes:
        Note that the verb will not be capitalized. It also
        assumes that the active party (You) is the one performing the verb.
        This automatic conjugation will fail if the active part is another person
        than 'you'. The caller/receiver must be passed to the parser directly.

    Examples:
        This is often used in combination with the $you/You( callables.

        - `With a grin, $you() $conj(jump)`

        You will see "With a grin, you jump."
        Others will see "With a grin, CharName jumps."

    """
        return ""
        raise ParsingError("No caller/receiver supplied to $conj callable")

    second_person_str, third_person_str = verb_actor_stance_components(args[0])
    return second_person_str if caller == receiver else third_person_str

def funcparser_callable_pronoun(*args, caller=None, receiver=None, capitalize=False, **kwargs):
    """

    Usage: $pron(word, [options])

    Adjust pronouns to the expected form. Pronouns are words you use instead of a
    proper name, such as 'him', 'herself', 'theirs' etc. These look different
    depending on who sees the outgoing string.

    The parser maps between this table ...

    ====================  =======  =======  ==========  ==========  ===========
    1st/2nd person        Subject  Object   Possessive  Possessive  Reflexive
                          Pronoun  Pronoun  Adjective   Pronoun     Pronoun
    1st person               I        me        my        mine       myself
    1st person plural       we       us        our        ours       ourselves
    2nd person              you      you       your       yours      yourself
    2nd person plural       you      you       your       yours      yourselves

    ... and this table (and vice versa).

    3rd person            Subject  Object   Possessive  Possessive  Reflexive
    3rd person male         he       him       his        his        himself
    3rd person female       she      her       her        hers       herself
    3rd person neutral      it       it        its                   itself
    3rd person plural       they    them       their      theirs     themselves

    This system will examine `caller` for either a property or a callable `.gender` to
    get a default gender fallback (if not specified in the call). If a callable,
    `.gender` will be called without arguments and should return a string
    `male`/`female`/`neutral`/`plural` (plural is considered a gender for this purpose).
    If no `gender` property/callable is found, `neutral` is used as a fallback.

    The pronoun-type default (if not specified in call) is `subject pronoun`.

    Args:
        pronoun (str): Input argument to parsed call. This can be any of the pronouns
            in the table above. If given in 1st/second form, they will be mappped to
            3rd-person form for others viewing the message (but will need extra input
            via the `gender`, see below). If given on 3rd person form, this will be
            mapped to 2nd person form for `caller` unless `viewpoint` is specified
            in options.
        options (str, optional): A space- or comma-separated string detailing `pronoun_type`,
            `gender`/`plural` and/or `viewpoint` to help the mapper differentiate between
            non-unique cases (such as if `you` should become `him` or `they`).
            Allowed values are:

            - `subject pronoun`/`subject`/`sp` (I, you, he, they)
            - `object pronoun`/`object/`/`op`  (me, you, him, them)
            - `possessive adjective`/`adjective`/`pa` (my, your, his, their)
            - `possessive pronoun`/`pronoun`/`pp`  (mine, yours, his, theirs)
            - `male`/`m`
            - `female`/`f`
            - `neutral`/`n`
            - `plural`/`p`
            - `1st person`/`1st`/`1`
            - `2nd person`/`2nd`/`2`
            - `3rd person`/`3rd`/`3`

        caller (Object): The object creating the string. If this has a property 'gender',
            it will be checked for a string 'male/female/neutral' to determine
            the 3rd person gender (but if `pronoun_type` contains a gender
            component, that takes precedence). Provided automatically to the
            funcparser.
        receiver (Object): The recipient of the string. This being the same as
            `caller` or not helps determine 2nd vs 3rd-person forms. This is
            provided automatically by the funcparser.
        capitalize (bool): The input retains its capitalization. If this is set the output is
            always capitalized.

    Examples:

        ======================  =============    ===========
        Input                   caller sees      others see
        $pron(I, m)             I                he
        $pron(you,fo)           you              her
        $pron(yourself)         yourself         itself
        $pron(its)              your             its
        $pron(you,op,p)         you              them

    Notes:
        There is no option to specify reflexive pronouns since they are all unique
        and the mapping can always be auto-detected.

    """
        return ""

    pronoun, *options = args
    # options is either multiple args or a space-separated string
    if len(options) == 1:
        options = options[0]

    # default differentiators
    default_pronoun_type = "subject pronoun"
    default_gender = "neutral"
    default_viewpoint = "2nd person"

    if hasattr(caller, "gender"):
        if callable(caller.gender):
            default_gender = caller.gender()
        else:
            default_gender = caller.gender

    if "viewpoint" in kwargs:
        # passed into FuncParser initialization
        default_viewpoint = kwargs["viewpoint"]

    pronoun_1st_or_2nd_person, pronoun_3rd_person = pronoun_to_viewpoints(
        pronoun,
        options,
        pronoun_type=default_pronoun_type,
        gender=default_gender,
        viewpoint=default_viewpoint,
    )

    if capitalize:
        pronoun_1st_or_2nd_person = pronoun_1st_or_2nd_person.capitalize()
        pronoun_3rd_person = pronoun_3rd_person.capitalize()

    return pronoun_1st_or_2nd_person if caller == receiver else pronoun_3rd_person

def funcparser_callable_pronoun_capitalize(
    *args, caller=None, receiver=None, capitalize=True, **kwargs
):
    """
    Usage: $Pron(word, [options]) - always maps to a capitalized word.

    """
    return funcparser_callable_pronoun(
        *args, caller=caller, receiver=receiver, capitalize=capitalize, **kwargs
    )

# these are made available as callables by adding 'evennia.utils.funcparser' as
# a callable-path when initializing the FuncParser.

    # 'standard' callables
    # eval and arithmetic
    "eval": funcparser_callable_eval,
    "add": funcparser_callable_add,
    "sub": funcparser_callable_sub,
    "mult": funcparser_callable_mult,
    "div": funcparser_callable_div,
    "round": funcparser_callable_round,
    "toint": funcparser_callable_toint,
    # randomizers
    "random": funcparser_callable_random,
    "randint": funcparser_callable_randint,
    "choice": funcparser_callable_choice,
    # string manip
    "pad": funcparser_callable_pad,
    "crop": funcparser_callable_crop,
    "just": funcparser_callable_justify,
    "ljust": funcparser_callable_left_justify,
    "rjust": funcparser_callable_right_justify,
    "cjust": funcparser_callable_center_justify,
    "justify": funcparser_callable_justify,  # aliases for backwards compat
    "justify_left": funcparser_callable_left_justify,
    "justify_right": funcparser_callable_right_justify,
    "justify_center": funcparser_callable_center_justify,
    "space": funcparser_callable_space,
    "clr": funcparser_callable_clr,
    "pluralize": funcparser_callable_pluralize,
    "int2str": funcparser_callable_int2str,
    "an": funcparser_callable_an,
}

SEARCHING_CALLABLES = {
    # requires `caller` and optionally `access` to be passed into parser
    "search": funcparser_callable_search,
    "obj": funcparser_callable_search,  # aliases for backwards compat
    "objlist": funcparser_callable_search_list,
    "dbref": funcparser_callable_search,
}

ACTOR_STANCE_CALLABLES = {
    # requires `you`, `receiver` and `mapping` to be passed into parser
    "you": funcparser_callable_you,
    "You": funcparser_callable_you_capitalize,
    "your": funcparser_callable_your,
    "Your": funcparser_callable_your_capitalize,
    "obj": funcparser_callable_you,
    "Obj": funcparser_callable_you_capitalize,
    "conj": funcparser_callable_conjugate,
    "pron": funcparser_callable_pronoun,
    "Pron": funcparser_callable_pronoun_capitalize,
    **FUNCPARSER_CALLABLES,
}

"""
The gametime module handles the global passage of time in the mud.

It also supplies some useful methods to convert between
in-mud time and real-world time as well allows to get the
total runtime of the server and the current uptime.

"""

# Speed-up factor of the in-game time compared
# to real time.

IGNORE_DOWNTIMES = settings.TIME_IGNORE_DOWNTIMES

# Only set if gametime_reset was called at some point.
try:
    GAME_TIME_OFFSET = ServerConfig.objects.conf("gametime_offset", default=0)
except OperationalError:
    # the db is not initialized
    print("Gametime offset could not load - db not set up.")
    GAME_TIME_OFFSET = 0

# Common real-life time measure, in seconds.
# You should not change this.

# these are kept updated by the server maintenance loop
SERVER_START_TIME = 0.0
SERVER_RUNTIME_LAST_UPDATED = 0.0
SERVER_RUNTIME = 0.0

# note that these should not be accessed directly since they may
# need further processing. Access from server_epoch() and game_epoch().
_SERVER_EPOCH = None
_GAME_EPOCH = None

# Helper Script dealing in gametime (created by `schedule` function
# below).

class TimeScript(DefaultScript):

        args = self.db.schedule_args or []
        kwargs = self.db.schedule_kwargs or {}

# Access functions

def runtime():
    """
    Get the total runtime of the server since first start (minus
    downtimes)

    Args:
        format (bool, optional): Format into a time representation.

    Returns:
        time (float or tuple): The runtime or the same time split up
            into time units.

    """
    return SERVER_RUNTIME + time.time() - SERVER_RUNTIME_LAST_UPDATED

def server_epoch():
    """
    Get the server epoch. We may need to calculate this on the fly.

    """
    global _SERVER_EPOCH
    if not _SERVER_EPOCH:
        _SERVER_EPOCH = (
            ServerConfig.objects.conf("server_epoch", default=None) or time.time() - runtime()
        )
    return _SERVER_EPOCH

def uptime():
    """
    Get the current uptime of the server since last reload

    Args:
        format (bool, optional): Format into time representation.

    Returns:
        time (float or tuple): The uptime or the same time split up

    """
    return time.time() - SERVER_START_TIME

def portal_uptime():
    """
    Get the current uptime of the portal.

    Returns:
        time (float): The uptime of the portal.
    """

    return time.time() - evennia.SESSION_HANDLER.portal_start_time

def game_epoch():
    """
    Get the game epoch.

    """
    game_epoch = settings.TIME_GAME_EPOCH
    return game_epoch if game_epoch is not None else server_epoch()

def gametime(absolute=False):
    """
    Get the total gametime of the server since first start (minus downtimes)

    Args:
        absolute (bool, optional): Get the absolute game time, including
            the epoch. This could be converted to an absolute in-game
            date.

    Returns:
        time (float): The gametime as a virtual timestamp.

    Notes:
        If one is using a standard calendar, one could convert the unformatted
        return to a date using Python's standard `datetime` module like this:
        `datetime.datetime.fromtimestamp(gametime(absolute=True))`

    """
    epoch = game_epoch() if absolute else 0
    if IGNORE_DOWNTIMES:
        gtime = epoch + (time.time() - server_epoch()) * TIMEFACTOR
    else:
        gtime = epoch + (runtime() - GAME_TIME_OFFSET) * TIMEFACTOR

def real_seconds_until(sec=None, min=None, hour=None, day=None, month=None, year=None):
    """

    Args:
        sec (int or None): number of absolute seconds.
        min (int or None): number of absolute minutes.
        hour (int or None): number of absolute hours.
        day (int or None): number of absolute days.
        month (int or None): number of absolute months.
        year (int or None): number of absolute years.

    Returns:

    Example:

        If the game time is 5:00, TIME_FACTOR is set to 2 and you ask
        the number of seconds until it's 5:10, then this function should
        return 300 (5 minutes).

    """
    current = datetime.fromtimestamp(gametime(absolute=True))
    s_sec = sec if sec is not None else current.second
    s_min = min if min is not None else current.minute
    s_hour = hour if hour is not None else current.hour
    s_day = day if day is not None else current.day
    s_month = month if month is not None else current.month
    s_year = year if year is not None else current.year
    projected = datetime(s_year, s_month, s_day, s_hour, s_min, s_sec)

        # We increase one unit of time depending on parameters
        if month is not None:
            projected = projected.replace(year=s_year + 1)
        elif day is not None:
            try:
                projected = projected.replace(month=s_month + 1)
                projected = projected.replace(month=1)
        elif hour is not None:
            projected += timedelta(days=1)
        elif min is not None:
            projected += timedelta(seconds=3600)
        else:
            projected += timedelta(seconds=60)

    # Get the number of gametime seconds between these two dates
    seconds = (projected - current).total_seconds()
    return seconds / TIMEFACTOR

def schedule(
    callback,
    repeat=False,
    sec=None,
    min=None,
    hour=None,
    day=None,
    month=None,
    year=None,
    *args,
    **kwargs,
):
    """
    Call a callback at a given in-game time.

    Args:
        callback (function): The callback function that will be called. Note
            that the callback must be a module-level function, since the script will
            be persistent. The callable should be on the form `callable(*args, **kwargs)`
            where args/kwargs are passed into this schedule.
        repeat (bool, optional): Defines if the callback should be called regularly
            at the specified time.
        sec (int or None): Number of absolute game seconds at which to run repeat.
        min (int or None): Number of absolute minutes.
        hour (int or None): Number of absolute hours.
        day (int or None): Number of absolute days.
        month (int or None): Number of absolute months.
        year (int or None): Number of absolute years.
        *args: Passed into the callable. Must be possible to store in Attribute.
        **kwargs: Passed into the callable. Must be possible to store in Attribute.

    Returns:
        Script: The created Script handling the scheduling.

    Examples:
        ::
            schedule(func, min=5, sec=0)  # Will call 5 minutes past the next (in-game) hour.
            schedule(func, hour=2, min=30, sec=0)  # Will call the next (in-game) day at 02:30.

    """
    seconds = real_seconds_until(sec=sec, min=min, hour=hour, day=day, month=month, year=year)
        "evennia.utils.gametime.TimeScript",
        key="TimeScript",
        desc="A gametime-sensitive script",
    )
    script.db.gametime = {
        "sec": sec,
        "min": min,
        "hour": hour,
        "day": day,
        "month": month,
        "year": year,
    }
    script.db.schedule_args = args
    script.db.schedule_kwargs = kwargs

def reset_gametime():
    """
    Resets the game time to make it start from the current time. Note that
    the epoch set by `settings.TIME_GAME_EPOCH` will still apply.

    """
    global GAME_TIME_OFFSET
    GAME_TIME_OFFSET = runtime()
    ServerConfig.objects.conf("gametime_offset", GAME_TIME_OFFSET)

"""
IDmapper extension to the default manager.
"""
from django.db.models.manager import Manager

class SharedMemoryManager(Manager):
    # TODO: improve on this implementation
    # We need a way to handle reverse lookups so that this model can
    # still use the singleton cache, but the active model isn't required
    # to be a SharedMemoryModel.
        """
        Data entity lookup.
        """
        items = list(kwargs)
        inst = None
        if len(items) == 1:
            # CL: support __exact
            key = items[0]
            if key.endswith("__exact"):
                key = key[: -len("__exact")]
            if key in ("pk", self.model._meta.pk.attname):
                try:
                    inst = self.model.get_cached_instance(kwargs[items[0]])
                    # we got the item from cache, but if this is a fk, check it's ours
                    if getattr(inst, str(self.field).split(".")[-1]) != self.instance:
                        inst = None
                    pass
        if inst is None:
            inst = super().get(*args, **kwargs)
        return inst

"""
Django ID mapper

Modified for Evennia by making sure that no model references
leave caching unexpectedly (no use of WeakRefs).

Also adds `cache_size()` for monitoring the size of the cache.
"""

import gc
import os

from django.core.exceptions import FieldError, ObjectDoesNotExist
from django.db.models.base import Model, ModelBase
from django.db.models.signals import post_migrate, post_save, pre_delete
from django.db.transaction import atomic
from django.db.utils import DatabaseError
from twisted.internet.reactor import callFromThread

from evennia.utils.utils import dbref, get_evennia_pids, to_str

from .manager import SharedMemoryManager

AUTO_FLUSH_MIN_INTERVAL = 60.0 * 5  # at least 5 mins between cache flushes

_MONITOR_HANDLER = None

# References to db-updated objects are stored here so the
# main process can be informed to re-cache itself.
PROC_MODIFIED_COUNT = 0
PROC_MODIFIED_OBJS = WeakValueDictionary()

# get info about the current process and thread; determine if our
# current pid is different from the server PID (i.e.  # if we are in a
# subprocess or not)
_SELF_PID = os.getpid()
_SERVER_PID, _PORTAL_PID = get_evennia_pids()
_IS_SUBPROCESS = (_SERVER_PID and _PORTAL_PID) and _SELF_PID not in (_SERVER_PID, _PORTAL_PID)

class SharedMemoryModelBase(ModelBase):
    # CL: upstream had a __new__ method that skipped ModelBase's __new__ if
    # SharedMemoryModelBase was not in the model class's ancestors. It's not
    # clear what was the intended purpose, but skipping ModelBase.__new__
    # broke things; in particular, default manager inheritance.

    def __call__(cls, *args, **kwargs):
        """
        this method will either create an instance (by calling the default implementation)
        or try to retrieve one from the class-wide cache by inferring the pk value from
        `args` and `kwargs`. If instance caching is enabled for this class, the cache is
        populated whenever possible (ie when it is possible to infer the pk value).

        """

        def new_instance():
            return super(SharedMemoryModelBase, cls).__call__(*args, **kwargs)

        instance_key = cls._get_cache_key(args, kwargs)
        # depending on the arguments, we might not be able to infer the PK, so in that case we
        # create a new instance
        if instance_key is None:
            return new_instance()
        cached_instance = cls.get_cached_instance(instance_key)
        if cached_instance is None:
            cached_instance = new_instance()
            cls.cache_instance(cached_instance, new=True)
        return cached_instance

    def _prepare(cls):
        """
        Prepare the cache, making sure that proxies of the same db base
        share the same cache.

        """
        # the dbmodel is either the proxy base or ourselves
        dbmodel = cls._meta.concrete_model if cls._meta.proxy else cls
        cls.__dbclass__ = dbmodel
        if not hasattr(dbmodel, "__instance_cache__"):
            # we store __instance_cache__ only on the dbmodel base
            dbmodel.__instance_cache__ = {}
        super()._prepare()

        """
        Field shortcut creation:

        Takes field names `db_*` and creates property wrappers named
        without the `db_` prefix. So db_key -> key

        This wrapper happens on the class level, so there is no
        overhead when creating objects.  If a class already has a
        wrapper of the given name, the automatic creation is skipped.

        Notes:
            Remember to document this auto-wrapping in the class
            header, this could seem very much like magic to the user
            otherwise.
        """

        attrs["typename"] = cls.__name__
        attrs["_is_deleted"] = False

        # set up the typeclass handling only if a variable _is_typeclass is set on the class
        def create_wrapper(cls, fieldname, wrappername, editable=True, foreignkey=False):
            "Helper method to create property wrappers with unique names (must be in separate call)"

            def _get(cls, fname):
                "Wrapper for getting database field"
                if _GA(cls, "_is_deleted"):
                    raise ObjectDoesNotExist(
                        "Cannot access %s: Hosting object was already deleted." % fname
                    )
                return _GA(cls, fieldname)

            def _get_foreign(cls, fname):
                "Wrapper for returning foreignkey fields"
                    )

            def _set_nonedit(cls, fname, value):
                "Wrapper for blocking editing of field"
                raise FieldError("Field %s cannot be edited." % fname)

            def _set(cls, fname, value):
                "Wrapper for setting database field"
                        "Cannot set %s to %s: Hosting object was already deleted!" % (fname, value)
                    )
                _SA(cls, fname, value)
                # only use explicit update_fields in save if we actually have a
                # primary key assigned already (won't be set when first creating object)
                update_fields = (
                    [fname] if _GA(cls, "_get_pk_val")(_GA(cls, "_meta")) is not None else None
                )
                _GA(cls, "save")(update_fields=update_fields)

            def _set_foreign(cls, fname, value):
                "Setter only used on foreign key relations, allows setting with #dbref"
                    )
                if isinstance(value, (str, int)):
                    value = to_str(value)
                    if value.isdigit() or value.startswith("#"):
                        # we also allow setting using dbrefs, if so we try to load the matching
                        # object. (we assume the object is of the same type as the class holding
                        # the field, if not a custom handler must be used for that field)
                        dbid = dbref(value, reqhash=False)
                        if dbid:
                            model = _GA(cls, "_meta").get_field(fname).model
                            try:
                                value = model._default_manager.get(id=dbid)
                            except ObjectDoesNotExist:
                                # maybe it is just a name that happens to look like a dbid
                                pass
                )

            def _del_nonedit(cls, fname):
                "wrapper for not allowing deletion"

            def _del(cls, fname):
                "Wrapper for clearing database field - sets it to None"
                _SA(cls, fname, None)
                )

            # wrapper factories
            if not editable:

                def fget(cls):
                    return _get(cls, fieldname)

                def fset(cls, val):
                    return _set_nonedit(cls, fieldname, val)

            elif foreignkey:

                    return _get_foreign(cls, fieldname)

                    return _set_foreign(cls, fieldname, val)

            else:

                    return _set(cls, fieldname, val)

            def fdel(cls):
                return _del(cls, fieldname) if editable else _del_nonedit(cls, fieldname)

            # set docstrings for auto-doc
            fget.__doc__ = "A wrapper for getting database field `%s`." % fieldname
            fset.__doc__ = "A wrapper for setting (and saving) database field `%s`." % fieldname
            fdel.__doc__ = "A wrapper for deleting database field `%s`." % fieldname
            # assigning
            attrs[wrappername] = property(fget, fset, fdel)
            # type(cls).__setattr__(cls, wrappername, property(fget, fset, fdel))#, doc))

        # exclude some models that should not auto-create wrapper fields
        if cls.__name__ in ("ServerConfig", "TypeNick"):
            return
        # dynamically create the wrapper properties for all fields not already handled
        # (manytomanyfields are always handlers)
        for fieldname, field in (
            (fname, field)
            for fname, field in list(attrs.items())
            if fname.startswith("db_") and type(field).__name__ != "ManyToManyField"
        ):
            foreignkey = type(field).__name__ == "ForeignKey"
            wrappername = "dbid" if fieldname == "id" else fieldname.replace("db_", "", 1)
            if wrappername not in attrs:
                # makes sure not to overload manually created wrappers on the model
                create_wrapper(
                    cls, fieldname, wrappername, editable=field.editable, foreignkey=foreignkey
                )

        return super().__new__(cls, name, bases, attrs)

class SharedMemoryModel(Model, metaclass=SharedMemoryModelBase):
    """
    Base class for idmapped objects. Inherit from `this`.
    """

    objects = SharedMemoryManager()

    def _get_cache_key(cls, args, kwargs):
        """
        This method is used by the caching subsystem to infer the PK
        value from the constructor arguments.  It is used to decide if
        an instance has to be built or is already in the cache.

        """
        # Quick hack for my composites work for now.
        if hasattr(cls._meta, "pks"):
            pk = cls._meta.pks[0]
        else:
            pk = cls._meta.pk
        # get the index of the pk in the class fields. this should be calculated *once*, but isn't
        # atm
        pk_position = cls._meta.fields.index(pk)
        if len(args) > pk_position:
            # if it's in the args, we can get it easily by index
            result = args[pk_position]
        elif pk.attname in kwargs:
            # retrieve the pk value. Note that we use attname instead of name, to handle the case
            # where the pk is a a ForeignKey.
            result = kwargs[pk.attname]
        elif pk.name != pk.attname and pk.name in kwargs:
            # ok we couldn't find the value, but maybe it's a FK and we can find the corresponding
            # object instead
            result = kwargs[pk.name]

        if result is not None and isinstance(result, Model):
            # if the pk value happens to be a model instance (which can happen wich a FK), we'd
            # rather use its own pk as the key
            result = result._get_pk_val()

    def get_cached_instance(cls, id):
        """
        Method to retrieve a cached instance by pk value. Returns None
        when not found (which will always be the case when caching is
        disabled for this class). Please note that the lookup will be
        done even when instance caching is disabled.

        """
        return cls.__dbclass__.__instance_cache__.get(id)

    def cache_instance(cls, instance, new=False):
        """
        Method to store an instance in the cache.

        Args:
            instance (Class instance): the instance to cache.
            new (bool, optional): this is the first time this instance is
                cached (i.e. this is not an update operation like after a
                db save).

        """
        pk = instance._get_pk_val()
        if pk is not None:
            new = new or pk not in cls.__dbclass__.__instance_cache__
            cls.__dbclass__.__instance_cache__[pk] = instance
            if new:
                try:
                    # trigger the at_init hook only
                    # at first initialization
                    instance.at_init()
                    # The at_init hook is not assigned to all entities
                    pass

    def get_all_cached_instances(cls):
        """
        Return the objects so far cached by idmapper for this class.

        """
        return list(cls.__dbclass__.__instance_cache__.values())

    def _flush_cached_by_key(cls, key, force=True):
        """
        Remove the cached reference.

        """
        try:
            if force or cls.at_idmapper_flush():
                del cls.__dbclass__.__instance_cache__[key]
            else:
                cls._dbclass__.__instance_cache__[key].refresh_from_db()
            # No need to remove if cache doesn't contain it already
            pass

    def flush_cached_instance(cls, instance, force=True):
        """
        Method to flush an instance from the cache. The instance will
        always be flushed from the cache, since this is most likely
        called from delete(), and we want to make sure we don't cache
        dead objects.

        """
        cls._flush_cached_by_key(instance._get_pk_val(), force=force)

    # flush_cached_instance = classmethod(flush_cached_instance)

    def flush_instance_cache(cls, force=False):
        """
        This will clean safe objects from the cache. Use `force`
        keyword to remove all objects, safe or not.

        """
        if force:
            cls.__dbclass__.__instance_cache__ = {}
        else:
            cls.__dbclass__.__instance_cache__ = dict(
                (key, obj)
                for key, obj in cls.__dbclass__.__instance_cache__.items()
                if not obj.at_idmapper_flush()
            )

    # flush_instance_cache = classmethod(flush_instance_cache)

    # per-instance methods

        return super().__eq__(other)

        """

        Returns:
        """

    def flush_from_cache(self, force=False):
        """
        Flush this instance from the instance cache. Use
        `force` to override the result of at_idmapper_flush() for the object.

        """
        pk = self._get_pk_val()
        if pk:
            if force or self.at_idmapper_flush():
                self.__class__.__dbclass__.__instance_cache__.pop(pk, None)

        """
        Delete the object, clearing cache.

        """
        self.flush_from_cache()
        self._is_deleted = True

    def save(self, *args, **kwargs):
        """
        Central database save operation.

        Notes:
            Arguments as per Django documentation.
            Calls `self.at_<fieldname>_postsave(new)`
            (this is a wrapper set by oobhandler:
            self._oob_at_<fieldname>_postsave())

        """
        global _MONITOR_HANDLER
        if not _MONITOR_HANDLER:
            from evennia.scripts.monitorhandler import (
                MONITOR_HANDLER as _MONITOR_HANDLER,
            )

        if _IS_SUBPROCESS:
            # we keep a store of objects modified in subprocesses so
            # we know to update their caches in the central process
            global PROC_MODIFIED_COUNT, PROC_MODIFIED_OBJS
            PROC_MODIFIED_COUNT += 1
            PROC_MODIFIED_OBJS[PROC_MODIFIED_COUNT] = self

        if _IS_MAIN_THREAD:
            # in main thread - normal operation
            try:
                with atomic():
                    super().save(*args, **kwargs)
            except DatabaseError:
                # we handle the 'update_fields did not update any rows' error that
                # may happen due to timing issues with attributes
                ufields_removed = kwargs.pop("update_fields", None)
                if ufields_removed:
                    with atomic():
                        super().save(*args, **kwargs)
                else:
                    raise
        else:
            # in another thread; make sure to save in reactor thread
            def _save_callback(cls, *args, **kwargs):
                super().save(*args, **kwargs)

            callFromThread(_save_callback, self, *args, **kwargs)

            # this can happen if some of the startup methods immediately
            # delete the object (an example are Scripts that start and die immediately)
            return

        # update field-update hooks and eventual OOB watchers
        new = False
        if "update_fields" in kwargs and kwargs["update_fields"]:
            # get field objects from their names
            update_fields = (
                self._meta.get_field(fieldname) for fieldname in kwargs.get("update_fields")
            )
        else:
            # meta.fields are already field objects; get them all
            new = True
            update_fields = self._meta.fields
        for field in update_fields:
            fieldname = field.name
            # trigger eventual monitors
            _MONITOR_HANDLER.at_update(self, fieldname)
            # if a hook is defined it must be named exactly on this form
            hookname = "at_%s_postsave" % fieldname
            if hasattr(self, hookname) and callable(_GA(self, hookname)):
                _GA(self, hookname)(new)

        #            # if a trackerhandler is set on this object, update it with the
        #            # fieldname and the new value
        #            fieldtracker = "_oob_at_%s_postsave" % fieldname
        #            if hasattr(self, fieldtracker):
        #                _GA(self, fieldtracker)(fieldname)
        pass

class WeakSharedMemoryModelBase(SharedMemoryModelBase):
    """
    Uses a WeakValue dictionary for caching instead of a regular one.

    """

        cls.__dbclass__.__instance_cache__ = WeakValueDictionary()

class WeakSharedMemoryModel(SharedMemoryModel, metaclass=WeakSharedMemoryModelBase):
    """
    Uses a WeakValue dictionary for caching instead of a regular one

    """

def flush_cache(**kwargs):
    """
    Flush idmapper cache. When doing so the cache will fire the
    at_idmapper_flush hook to allow the object to optionally handle
    its own flushing.

    Uses a signal so we make sure to catch cascades.

    """

    def class_hierarchy(clslist):
        """Recursively yield a class hierarchy"""
        for cls in clslist:
            subclass_list = cls.__subclasses__()
            if subclass_list:
                for subcls in class_hierarchy(subclass_list):
                    yield subcls
            else:
                yield cls

    for cls in class_hierarchy([SharedMemoryModel]):
        cls.flush_instance_cache()
    # run the python garbage collector
    return gc.collect()

# request_finished.connect(flush_cache)
post_migrate.connect(flush_cache)

def flush_cached_instance(sender, instance, **kwargs):
    """
    Flush the idmapper cache only for a given instance.

    """
    # XXX: Is this the best way to make sure we can flush?
    if not hasattr(instance, "flush_cached_instance"):
        return
    sender.flush_cached_instance(instance, force=True)

pre_delete.connect(flush_cached_instance)

def update_cached_instance(sender, instance, **kwargs):
    """
    Re-cache the given instance in the idmapper cache.

    """
    if not hasattr(instance, "cache_instance"):
        return
    sender.cache_instance(instance)

post_save.connect(update_cached_instance)

LAST_FLUSH = None

def conditional_flush(max_rmem, force=False):
    """
    Flush the cache if the estimated memory usage exceeds `max_rmem`.

    The flusher has a timeout to avoid flushing over and over
    in particular situations (this means that for some setups
    the memory usage will exceed the requirement and a server with
    more memory is probably required for the given game).

    Args:
        max_rmem (int): memory-usage estimation-treshold after which
            cache is flushed.
        force (bool, optional): forces a flush, regardless of timeout.
            Defaults to `False`.

    """
    global LAST_FLUSH

    def mem2cachesize(desired_rmem):
        """
        Estimate the size of the idmapper cache based on the memory
        desired. This is used to optionally cap the cache size.

        desired_rmem - memory in MB (minimum 50MB)

        The formula is empirically estimated from usage tests (Linux)
        and is
            Ncache = RMEM - 35.0 / 0.0157
        where RMEM is given in MB and Ncache is the size of the cache
        for this memory usage. VMEM tends to be about 100MB higher
        than RMEM for large memory usage.
        """
        vmem = max(desired_rmem, 50.0)
        Ncache = int(abs(float(vmem) - 35.0) / 0.0157)
        return Ncache

    if not max_rmem:
        # auto-flush is disabled
        return

    now = time.time()
    if not LAST_FLUSH:
        # server is just starting
        LAST_FLUSH = now
        return

    if ((now - LAST_FLUSH) < AUTO_FLUSH_MIN_INTERVAL) and not force:
        # too soon after last flush.
        logger.log_warn(
            "Warning: Idmapper flush called more than once in %s min interval. Check memory usage."
            % (AUTO_FLUSH_MIN_INTERVAL / 60.0)
        )
        return

    if os.name == "nt":
        # we can't look for mem info in Windows at the moment
        return

    # check actual memory usage
    Ncache_max = mem2cachesize(max_rmem)
    Ncache, _ = cache_size()
    actual_rmem = (
        float(os.popen("ps -p %d -o %s | tail -1" % (os.getpid(), "rss")).read()) / 1000.0
    )  # resident memory

    if Ncache >= Ncache_max and actual_rmem > max_rmem * 0.9:
        # flush cache when number of objects in cache is big enough and our
        # actual memory use is within 10% of our set max
        flush_cache()

def cache_size(mb=True):
    """
    Calculate statistics about the cache.

    Note: we cannot get reliable memory statistics from the cache -
    whereas we could do `getsizof` each object in cache, the result is
    highly imprecise and for a large number of objects the result is
    many times larger than the actual memory usage of the entire server;
    Python is clearly reusing memory behind the scenes that we cannot
    catch in an easy way here.  Ideas are appreciated. /Griatch

    Returns:
      total_num, {objclass:total_num, ...}

    """
    numtotal = [0]  # use mutable to keep reference through recursion
    classdict = {}

    def get_recurse(submodels):
        for submodel in submodels:
            subclasses = submodel.__subclasses__()
            if not subclasses:
                num = len(submodel.get_all_cached_instances())
                numtotal[0] += num
                classdict[submodel.__dbclass__.__name__] = num
            else:
                get_recurse(subclasses)

    get_recurse(SharedMemoryModel.__subclasses__())
    return numtotal[0], classdict

from .models import SharedMemoryModel

class Category(SharedMemoryModel):
    name = models.CharField(max_length=32)

class RegularCategory(models.Model):

class Article(SharedMemoryModel):
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    category2 = models.ForeignKey(RegularCategory, on_delete=models.CASCADE)

class RegularArticle(models.Model):

class SharedMemorysTest(TestCase):
    # TODO: test for cross model relation (singleton to regular)

        n = 0
        category = Category.objects.create(name="Category %d" % (n,))
        regcategory = RegularCategory.objects.create(name="Category %d" % (n,))

        for n in range(0, 10):
            Article.objects.create(
                name="Article %d" % (n,), category=category, category2=regcategory
            )
            RegularArticle.objects.create(
            )

    def testSharedMemoryReferences(self):
        article_list = Article.objects.all().select_related("category")
        last_article = article_list[0]
        for article in article_list[1:]:
            self.assertEqual(article.category is last_article.category, True)
            last_article = article

    def testRegularReferences(self):
        article_list = RegularArticle.objects.all().select_related("category")
            self.assertEqual(article.category2 is last_article.category2, False)

    def testMixedReferences(self):

        # article_list = Article.objects.all().select_related('category')
        # last_article = article_list[0]
        # for article in article_list[1:]:
        #    self.assertEquals(article.category2 is last_article.category2, False)
        #    last_article = article

    def testObjectDeletion(self):
        # This must execute first so its guaranteed to be in memory.
        list(Article.objects.all().select_related("category"))

        article = Article.objects.all()[0:1].get()
        pk = article.pk
        article.delete()
        self.assertEqual(pk not in Article.__instance_cache__, True)

"""
Logging facilities

These are thin wrappers on top of Twisted's logging facilities; logs
are all directed either to stdout (if Evennia is running in
interactive mode) or to $GAME_DIR/server/logs.

The log_file() function uses its own threading system to log to
arbitrary files in $GAME_DIR/server/logs.

Note: All logging functions have two aliases, log_type() and
log_typemsg(). This is for historical, back-compatible reasons.

"""

import os

from twisted import logger as twisted_logger
from twisted.internet.threads import deferToThread
from twisted.python import logfile
from twisted.python import util as twisted_util

log = twisted_logger.Logger()

_LOGDIR = None
_LOG_ROTATE_SIZE = None
_TIMEZONE = None
_CHANNEL_LOG_NUM_TAIL_LINES = None

_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"

def _log(msg, logfunc, prefix="", **kwargs):
    try:
        msg = str(msg)
        msg = str(err)
    if kwargs:
        logfunc(msg, **kwargs)
    else:
        try:
            for line in msg.splitlines():
                logfunc("{line}", prefix=prefix, line=line)
            log.error("Log failure: {err}", err=err)

# log call functions (each has legacy aliases)

def log_info(msg, **kwargs):
    """
    Logs any generic debugging/informative info that should appear in the log.

    Args:
        msg: (string) The message to be logged.
        **kwargs: If given, The `msg` is parsed as a format string with `{..}`
            formatting markers that should match the keywords.

    """
    _log(msg, log.info, **kwargs)

info = log_info
log_infomsg = log_info
log_msg = log_info

def log_warn(msg, **kwargs):
    """
    Logs warnings that aren't critical but should be noted.

    Args:
        msg (str): The message to be logged.

    """
    _log(msg, log.warn, **kwargs)

warn = log_warn
warning = log_warn
log_warnmsg = log_warn

def log_err(msg, **kwargs):
    """
    Logs an error message to the server log.

    Args:

    """
    _log(msg, log.error, **kwargs)

error = log_err
err = log_err
log_errmsg = log_err

def log_trace(msg=None, **kwargs):
    """
    Log a traceback to the log. This should be called from within an
    exception.

    Args:
        msg (str, optional): Adds an extra line with added info
            at the end of the traceback in the log.

    """
    if tracestring:
        _log(tracestring, log.error, prefix="!!", **kwargs)
        if msg:
            _log(msg, log.error, prefix="!!", **kwargs)

log_tracemsg = log_trace
exception = log_trace
critical = log_trace
trace = log_trace

def log_dep(msg, **kwargs):
    """
    Prints a deprecation message.

    Args:
        msg (str): The deprecation message to log.

    """
    _log(msg, log.warn, prefix="DP", **kwargs)

dep = log_dep
deprecated = log_dep
log_depmsg = log_dep

def log_sec(msg, **kwargs):
    """
    Prints a security-related message.

    Args:
        msg (str): The security message to log.

    """
    _log(msg, log.info, prefix="SS", **kwargs)

sec = log_sec
security = log_sec
log_secmsg = log_sec

def log_server(msg, **kwargs):
    """
    This is for the Portal to log captured Server stdout messages (it's
    usually only used during startup, before Server log is open)

    Args:
    """
    _log(msg, log.info, prefix="Server", **kwargs)

class GetLogObserver:
    """
    Sets up how the system logs are formatted.

    """

    component_prefix = ""
    event_levels = {
        twisted_logger.LogLevel.debug: "??",
        twisted_logger.LogLevel.info: "..",
        twisted_logger.LogLevel.warn: "WW",
        twisted_logger.LogLevel.error: "EE",
        twisted_logger.LogLevel.critical: "!!",
    }

    def format_log_event(self, event):
        """
        By assigning log_system here, we skip the spammy display of namespace/level
        in the default log output.

        [component_prefix] [date] [system/lvl] [msg]

        """
        # setting log_system fills the [..] block after the time stamp
        prefix = event.get("prefix", "")
        if prefix:
            event["log_system"] = prefix
        else:
            lvl = event.get("log_level", twisted_logger.LogLevel.info)
            event["log_system"] = self.event_levels.get(lvl, "-")
        event["log_format"] = str(event.get("log_format", ""))
        component_prefix = self.component_prefix or ""
        log_msg = twisted_logger.formatEventAsClassicLogText(
            event, formatTime=lambda e: twisted_logger.formatTime(e, _TIME_FORMAT)
        )
        return f"{component_prefix}{log_msg}"

    def __call__(self, outfile):
        return twisted_logger.FileLogObserver(outfile, self.format_log_event)

# Called by server/portal on startup

class GetPortalLogObserver(GetLogObserver):
    component_prefix = "|Portal| "

class GetServerLogObserver(GetLogObserver):

# logging overrides

def timeformat(when=None):
    """
    This helper function will format the current time in the same
    way as the twisted logger does, including time zone info. Only
    difference from official logger is that we only use two digits
    for the year and don't show timezone for GMT times.

    Args:
        when (int, optional): This is a time in POSIX seconds on the form
            given by time.time(). If not given, this function will
            use the current time.

    Returns:
        timestring (str): A formatted string of the given time.

    """
    when = when if when else time.time()

    # time zone offset: UTC - the actual offset
    tz_offset = datetime.utcfromtimestamp(when) - datetime.fromtimestamp(when)
    tz_offset = tz_offset.days * 86400 + tz_offset.seconds
    # correct given time to utc
    when = datetime.utcfromtimestamp(when - tz_offset)

    if tz_offset == 0:
        tz = ""
    else:
        tz_hour = abs(int(tz_offset // 3600))
        tz_mins = abs(int(tz_offset // 60 % 60))
        tz_sign = "-" if tz_offset >= 0 else "+"
        tz = "%s%02d%s" % (tz_sign, tz_hour, (":%02d" % tz_mins if tz_mins else ""))

    return "%d-%02d-%02d %02d:%02d:%02d%s" % (
        when.year - 2000,
        when.month,
        when.day,
        when.hour,
        when.minute,
        when.second,
        tz,
    )

class WeeklyLogFile(logfile.DailyLogFile):
    """
    Log file that rotates once per week by default. Overrides key methods to change format.

    """

    def __init__(self, name, directory, defaultMode=None, day_rotation=7, max_size=1000000):
        """
        Args:
            name (str): Name of log file.
            directory (str): Directory holding the file.
            defaultMode (str): Permissions used to create file. Defaults to
                current permissions of this file if it exists.
            day_rotation (int): How often to rotate the file.
            max_size (int): Max size of log file before rotation (regardless of
                time). Defaults to 1M.

        """
        self.day_rotation = day_rotation
        self.max_size = max_size
        self.size = 0
        logfile.DailyLogFile.__init__(self, name, directory, defaultMode=defaultMode)

    def _openFile(self):
        logfile.DailyLogFile._openFile(self)
        self.size = self._file.tell()

    def shouldRotate(self):
        """Rotate when the date has changed since last write"""
        # all dates here are tuples (year, month, day)
        now = self.toDate()
        then = self.lastDate
        return (
            now[0] > then[0]
            or now[1] > then[1]
            or now[2] > (then[2] + self.day_rotation)
            or self.size >= self.max_size
        )

    def suffix(self, tupledate):
        """Return the suffix given a (year, month, day) tuple or unixtime.
        Format changed to have 03 for march instead of 3 etc (retaining unix
        file order)

        If we get duplicate suffixes in location (due to hitting size limit),
        we append __1, __2 etc.

        Examples:
            server.log.2020_01_29
            server.log.2020_01_29__1
            server.log.2020_01_29__2

        """
        suffix = ""
        copy_suffix = 0
            try:
                suffix = "_".join(["{:02d}".format(part) for part in tupledate])
                # try taking a float unixtime
                suffix = "_".join(["{:02d}".format(part) for part in self.toDate(tupledate)])

            suffix += f"__{copy_suffix}" if copy_suffix else ""

            if os.path.exists(f"{self.path}.{suffix}"):
                # Append a higher copy_suffix to try to break the tie (starting from 2)
                copy_suffix += 1
            else:
                break
        return suffix

    def rotate(self):
        try:
            super().rotate()
            log_trace(f"Could not rotate the log file {self.name}.")

    def write(self, data):
        """
        Write data to log file

        """
        logfile.BaseLogFile.write(self, data)
        self.lastDate = max(self.lastDate, self.toDate())
        self.size += len(data)

# Arbitrary file logger

class EvenniaLogFile(logfile.LogFile):
    """
    A rotating logfile based off Twisted's LogFile. It overrides
    the LogFile's rotate method in order to append some of the last
    lines of the previous log to the start of the new log, in order
    to preserve a continuous chat history for channel log files.

    """

    # we delay import of settings to keep logger module as free
    # from django as possible.
    global _CHANNEL_LOG_NUM_TAIL_LINES
    if _CHANNEL_LOG_NUM_TAIL_LINES is None:

        _CHANNEL_LOG_NUM_TAIL_LINES = settings.CHANNEL_LOG_NUM_TAIL_LINES
    num_lines_to_append = max(1, _CHANNEL_LOG_NUM_TAIL_LINES)

    def rotate(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 = (
            num_lines_to_append if num_lines_to_append is not None else self.num_lines_to_append
        )
        if not append_tail:
            logfile.LogFile.rotate(self)
            return
        lines = tail_log_file(self.path, 0, self.num_lines_to_append)
        super().rotate()
            self.write(line)

    def seek(self, *args, **kwargs):
        """
        Convenience method for accessing our _file attribute's seek method,
        which is used in tail_log_function.

        Args:
            *args: Same args as file.seek
            **kwargs: Same kwargs as file.seek

        """
        return self._file.seek(*args, **kwargs)

    def readlines(self, *args, **kwargs):
        """
        Convenience method for accessing our _file attribute's readlines method,

        Args:
            *args: same args as file.readlines
            **kwargs: same kwargs as file.readlines

        Returns:
            lines (list): lines from our _file attribute.

        """
        lines = []
        for line in self._file.readlines(*args, **kwargs):
            try:
                lines.append(line.decode("utf-8"))
            except UnicodeDecodeError:
                try:
                    lines.append(str(line))
                    lines.append("")

_LOG_FILE_HANDLES = {}  # holds open log handles
_LOG_FILE_HANDLE_COUNTS = {}
_LOG_FILE_HANDLE_RESET = 500

def _open_log_file(filename):
    """
    Helper to open the log file (always in the log dir) and cache its
    handle.  Will create a new file in the log dir if one didn't
    exist.

    To avoid keeping the filehandle open indefinitely we reset it every
    _LOG_FILE_HANDLE_RESET accesses. This may help resolve issues for very
    long uptimes and heavy log use.

    """
    global _LOG_FILE_HANDLES, _LOG_FILE_HANDLE_COUNTS, _LOGDIR, _LOG_ROTATE_SIZE
    if not _LOGDIR:

        _LOGDIR = settings.LOG_DIR
        _LOG_ROTATE_SIZE = max(1000, settings.CHANNEL_LOG_ROTATE_SIZE)

    filename = os.path.join(_LOGDIR, filename)
    if filename in _LOG_FILE_HANDLES:
        _LOG_FILE_HANDLE_COUNTS[filename] += 1
        if _LOG_FILE_HANDLE_COUNTS[filename] > _LOG_FILE_HANDLE_RESET:
            # close/refresh handle
            _LOG_FILE_HANDLES[filename].close()
            del _LOG_FILE_HANDLES[filename]
        else:
            # return cached handle
            return _LOG_FILE_HANDLES[filename]
    try:
        filehandle = EvenniaLogFile.fromFullPath(filename, rotateLength=_LOG_ROTATE_SIZE)
        # filehandle = open(filename, "a+")  # append mode + reading
        _LOG_FILE_HANDLES[filename] = filehandle
        _LOG_FILE_HANDLE_COUNTS[filename] = 0
        return filehandle
    except IOError:
        log_trace()

def log_file(msg, filename="game.log"):
    """
    Arbitrary file logger using threads.

    Args:
        msg (str): String to append to logfile.
        filename (str, optional): Defaults to 'game.log'. All logs
            will appear in the logs directory and log entries will start
            on new lines following datetime info.

    """

    def callback(filehandle, msg):
        """Writing to file and flushing result"""
        msg = "\n%s [-] %s" % (timeformat(), msg.strip())
        filehandle.write(msg)
        # since we don't close the handle, we need to flush
        # manually or log file won't be written to until the
        # write buffer is full.
        filehandle.flush()

    def errback(failure):
        """Catching errors to normal log"""

    # save to server/logs/ directory
    filehandle = _open_log_file(filename)
    if filehandle:
        deferToThread(callback, filehandle, msg).addErrback(errback)

def log_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

    return os.path.exists(filename)

def rotate_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.

    """
    if log_file_exists(filename):
        file_handle = _open_log_file(filename)
        if file_handle:
            file_handle.rotate(num_lines_to_append=num_lines_to_append)

def delete_log_file(filename):
    """
    Delete a log file

    Args:
       filename(str): The name of the log file, located in settings.LOG_DIR
    """
        global _LOGDIR
        if not _LOGDIR:
            from django.conf import settings

            _LOGDIR = settings.LOG_DIR

        filename = os.path.join(_LOGDIR, filename)
        os.remove(filename)

def tail_log_file(filename, offset, nlines, callback=None):
    """
    Return the tail of the log file.

    Args:
        filename (str): The name of the log file, presumed to be in
            the Evennia log dir.
        offset (int): The line offset *from the end of the file* to start
            reading from. 0 means to start at the latest entry.
        nlines (int): How many lines to return, counting backwards
            from the offset. If file is shorter, will get all lines.
        callback (callable, optional): A function to manage the result of the
            asynchronous file access. This will get a list of lines. If unset,
            the tail will happen synchronously.

    Returns:
        lines (deferred or list): This will be a deferred if `callable` is given,
            otherwise it will be a list with The nline entries from the end of the file, or
            all if the file is shorter than nlines.

    """

    def seek_file(filehandle, offset, nlines, callback):
        """step backwards in chunks and stop only when we have enough lines"""
        while len(lines_found) < (offset + nlines):
            try:
                # file too small for this seek, take what we've got
                break
        # return the right number of lines
        lines_found = lines_found[-nlines - offset : -offset if offset else None]
            callback(lines_found)
        else:
            return lines_found

            return deferToThread(seek_file, filehandle, offset, nlines, callback).addErrback(
                errback
            )
        else:
            return seek_file(filehandle, offset, nlines, callback)
    else:

"""
Settings and configuration for Django.

Read values from the module specified by the DJANGO_SETTINGS_MODULE environment
variable, and then from django.conf.global_settings; see the global_settings.py
for a list of all possible variables.
"""

import os
import warnings
from pathlib import Path

from django.conf import global_settings
from django.utils.deprecation import RemovedInDjango50Warning, RemovedInDjango51Warning
from django.utils.functional import LazyObject, empty

ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE"
DEFAULT_STORAGE_ALIAS = "default"
STATICFILES_STORAGE_ALIAS = "staticfiles"

# RemovedInDjango50Warning
USE_DEPRECATED_PYTZ_DEPRECATED_MSG = (
    "The USE_DEPRECATED_PYTZ setting, and support for pytz timezones is "
    "deprecated in favor of the stdlib zoneinfo module. Please update your "
    "code to use zoneinfo and remove the USE_DEPRECATED_PYTZ setting."
)

USE_L10N_DEPRECATED_MSG = (
    "The USE_L10N setting is deprecated. Starting with Django 5.0, localized "
    "formatting of data will always be enabled. For example Django will "
    "display numbers and dates using the format of the current locale."
)

CSRF_COOKIE_MASKED_DEPRECATED_MSG = (
    "The CSRF_COOKIE_MASKED transitional setting is deprecated. Support for "
    "it will be removed in Django 5.0."
)

DEFAULT_FILE_STORAGE_DEPRECATED_MSG = (
    "The DEFAULT_FILE_STORAGE setting is deprecated. Use STORAGES instead."
)

STATICFILES_STORAGE_DEPRECATED_MSG = (
    "The STATICFILES_STORAGE setting is deprecated. Use STORAGES instead."
)

class SettingsReference(str):
    """
    String subclass which references a current settings value. It's treated as
    the value in memory but serializes to a settings.NAME attribute reference.
    """

    def __new__(self, value, setting_name):
        return str.__new__(self, value)

    def __init__(self, value, setting_name):
        self.setting_name = setting_name

class LazySettings(LazyObject):
    """
    A lazy proxy for either global Django settings or a custom settings object.
    The user can manually configure settings prior to using them. Otherwise,
    Django uses the settings module pointed to by DJANGO_SETTINGS_MODULE.
    """

    def _setup(self, name=None):
        """
        Load the settings module pointed to by the environment variable. This
        is used the first time settings are needed, if the user hasn't
        configured settings manually.
        """
        settings_module = os.environ.get(ENVIRONMENT_VARIABLE)
        if not settings_module:
            desc = ("setting %s" % name) if name else "settings"
            raise ImproperlyConfigured(
                "Requested %s, but settings are not configured. "
                "You must either define the environment variable %s "
                "or call settings.configure() before accessing settings."
                % (desc, ENVIRONMENT_VARIABLE)
            )

        self._wrapped = Settings(settings_module)

        # Hardcode the class name as otherwise it yields 'Settings'.
        if self._wrapped is empty:
            return "<LazySettings [Unevaluated]>"
        return '<LazySettings "%(settings_module)s">' % {
            "settings_module": self._wrapped.SETTINGS_MODULE,
        }

        """Return the value of a setting and cache it in self.__dict__."""
        if (_wrapped := self._wrapped) is empty:
            self._setup(name)
            _wrapped = self._wrapped
        val = getattr(_wrapped, name)

        # Special case some settings which require further modification.
        # This is done here for performance reasons so the modified value is cached.
        if name in {"MEDIA_URL", "STATIC_URL"} and val is not None:
            val = self._add_script_prefix(val)
        elif name == "SECRET_KEY" and not val:
            raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")

        self.__dict__[name] = val
        return val

    def __setattr__(self, name, value):
        """
        Set the value of setting. Clear all cached values if _wrapped changes
        (@override_settings does this) or clear single values when set.
        """
        if name == "_wrapped":
            self.__dict__.clear()
        else:
            self.__dict__.pop(name, None)
        super().__setattr__(name, value)

    def __delattr__(self, name):
        """Delete a setting and clear it from cache if needed."""
        super().__delattr__(name)
        self.__dict__.pop(name, None)

    def configure(self, default_settings=global_settings, **options):
        """
        Called to manually configure the settings. The 'default_settings'
        parameter sets where to retrieve any unspecified values from (its
        argument must support attribute access (__getattr__)).
        """
        if self._wrapped is not empty:
            raise RuntimeError("Settings already configured.")
        holder = UserSettingsHolder(default_settings)
        for name, value in options.items():
            if not name.isupper():
                raise TypeError("Setting %r must be uppercase." % name)
            setattr(holder, name, value)
        self._wrapped = holder

    def _add_script_prefix(value):
        """
        Add SCRIPT_NAME prefix to relative paths.

        Useful when the app is being served at a subpath and manually prefixing
        subpath to STATIC_URL and MEDIA_URL in settings is inconvenient.
        """
        # Don't apply prefix to absolute paths and URLs.
        if value.startswith(("http://", "https://", "/")):
        from django.urls import get_script_prefix

        return "%s%s" % (get_script_prefix(), value)

    @property
    def configured(self):
        """Return True if the settings have already been configured."""
        return self._wrapped is not empty

    def _show_deprecation_warning(self, message, category):
        stack = traceback.extract_stack()
        # Show a warning if the setting is used outside of Django.
        # Stack index: -1 this line, -2 the property, -3 the
        # LazyObject __getattribute__(), -4 the caller.
        filename, _, _, _ = stack[-4]
        if not filename.startswith(os.path.dirname(django.__file__)):
            warnings.warn(message, category, stacklevel=2)

    @property
    def USE_L10N(self):
        self._show_deprecation_warning(
            USE_L10N_DEPRECATED_MSG, RemovedInDjango50Warning
        )
        return self.__getattr__("USE_L10N")

    # RemovedInDjango50Warning.
    @property
    def _USE_L10N_INTERNAL(self):
        # Special hook to avoid checking a traceback in internal use on hot
        # paths.

    # RemovedInDjango51Warning.
    @property
    def DEFAULT_FILE_STORAGE(self):
            DEFAULT_FILE_STORAGE_DEPRECATED_MSG, RemovedInDjango51Warning
        )
        return self.__getattr__("DEFAULT_FILE_STORAGE")

    @property
    def STATICFILES_STORAGE(self):
            STATICFILES_STORAGE_DEPRECATED_MSG, RemovedInDjango51Warning
        )
        return self.__getattr__("STATICFILES_STORAGE")

class Settings:
    def __init__(self, settings_module):
        # update this dict from global settings (but only for ALL_CAPS settings)
        for setting in dir(global_settings):
            if setting.isupper():
                setattr(self, setting, getattr(global_settings, setting))

        # store the settings module in case someone later cares
        self.SETTINGS_MODULE = settings_module

        mod = importlib.import_module(self.SETTINGS_MODULE)

        tuple_settings = (
            "ALLOWED_HOSTS",
            "INSTALLED_APPS",
            "TEMPLATE_DIRS",
            "LOCALE_PATHS",
            "SECRET_KEY_FALLBACKS",
        )
        self._explicit_settings = set()
        for setting in dir(mod):
                setting_value = getattr(mod, setting)

                if setting in tuple_settings and not isinstance(
                    setting_value, (list, tuple)
                ):
                    raise ImproperlyConfigured(
                        "The %s setting must be a list or a tuple." % setting
                    )
                setattr(self, setting, setting_value)
                self._explicit_settings.add(setting)

        if self.USE_TZ is False and not self.is_overridden("USE_TZ"):
            warnings.warn(
                "The default value of USE_TZ will change from False to True "
                "in Django 5.0. Set USE_TZ to False in your project settings "
                "if you want to keep the current default behavior.",
                category=RemovedInDjango50Warning,
            )

        if self.is_overridden("USE_DEPRECATED_PYTZ"):
            warnings.warn(USE_DEPRECATED_PYTZ_DEPRECATED_MSG, RemovedInDjango50Warning)

        if self.is_overridden("CSRF_COOKIE_MASKED"):
            warnings.warn(CSRF_COOKIE_MASKED_DEPRECATED_MSG, RemovedInDjango50Warning)

        if hasattr(time, "tzset") and self.TIME_ZONE:
            # When we can, attempt to validate the timezone. If we can't find
            # this file, no check happens and it's harmless.
            zoneinfo_root = Path("/usr/share/zoneinfo")
            zone_info_file = zoneinfo_root.joinpath(*self.TIME_ZONE.split("/"))
            if zoneinfo_root.exists() and not zone_info_file.exists():
                raise ValueError("Incorrect timezone setting: %s" % self.TIME_ZONE)
            # Move the time zone info into os.environ. See ticket #2315 for why
            # we don't do this unconditionally (breaks Windows).
            os.environ["TZ"] = self.TIME_ZONE
            time.tzset()

        if self.is_overridden("USE_L10N"):
            warnings.warn(USE_L10N_DEPRECATED_MSG, RemovedInDjango50Warning)

        if self.is_overridden("DEFAULT_FILE_STORAGE"):
            if self.is_overridden("STORAGES"):
                raise ImproperlyConfigured(
                    "DEFAULT_FILE_STORAGE/STORAGES are mutually exclusive."
                )
            self.STORAGES = {
                **self.STORAGES,
                DEFAULT_STORAGE_ALIAS: {"BACKEND": self.DEFAULT_FILE_STORAGE},
            }
            warnings.warn(DEFAULT_FILE_STORAGE_DEPRECATED_MSG, RemovedInDjango51Warning)

        if self.is_overridden("STATICFILES_STORAGE"):
                    "STATICFILES_STORAGE/STORAGES are mutually exclusive."
                )
                STATICFILES_STORAGE_ALIAS: {"BACKEND": self.STATICFILES_STORAGE},
            }
            warnings.warn(STATICFILES_STORAGE_DEPRECATED_MSG, RemovedInDjango51Warning)
        # RemovedInDjango51Warning.
        if self.is_overridden("STORAGES"):
            setattr(
                self,
                "DEFAULT_FILE_STORAGE",
                self.STORAGES.get(DEFAULT_STORAGE_ALIAS, {}).get("BACKEND"),
            )
            setattr(
                self,
                "STATICFILES_STORAGE",
                self.STORAGES.get(STATICFILES_STORAGE_ALIAS, {}).get("BACKEND"),
            )

    def is_overridden(self, setting):
        return setting in self._explicit_settings

        return '<%(cls)s "%(settings_module)s">' % {
            "cls": self.__class__.__name__,
            "settings_module": self.SETTINGS_MODULE,
        }

class UserSettingsHolder:
    """Holder for user configured settings."""

    # SETTINGS_MODULE doesn't make much sense in the manually configured
    # (standalone) case.
    SETTINGS_MODULE = None

    def __init__(self, default_settings):
        """
        Requests for configuration variables not in this class are satisfied
        from the module specified in default_settings (if possible).
        """
        self.__dict__["_deleted"] = set()
        self.default_settings = default_settings

        if not name.isupper() or name in self._deleted:
            raise AttributeError
        return getattr(self.default_settings, name)

        self._deleted.discard(name)
        if name == "USE_L10N":
        if name == "CSRF_COOKIE_MASKED":
        if name == "DEFAULT_FILE_STORAGE":
            self.STORAGES[DEFAULT_STORAGE_ALIAS] = {
                "BACKEND": self.DEFAULT_FILE_STORAGE
            }
        if name == "STATICFILES_STORAGE":
            self.STORAGES[STATICFILES_STORAGE_ALIAS] = {
                "BACKEND": self.STATICFILES_STORAGE
            }
        if name == "USE_DEPRECATED_PYTZ":
        if name == "STORAGES":
            if default_file_storage := self.STORAGES.get(DEFAULT_STORAGE_ALIAS):
                super().__setattr__(
                    "DEFAULT_FILE_STORAGE", default_file_storage.get("BACKEND")
                )
            else:
                self.STORAGES.setdefault(
                    DEFAULT_STORAGE_ALIAS,
                    {"BACKEND": "django.core.files.storage.FileSystemStorage"},
                )
            if staticfiles_storage := self.STORAGES.get(STATICFILES_STORAGE_ALIAS):
                    "STATICFILES_STORAGE", staticfiles_storage.get("BACKEND")
                )
            else:
                    STATICFILES_STORAGE_ALIAS,
                    {
                        "BACKEND": (
                            "django.contrib.staticfiles.storage.StaticFilesStorage"
                        ),
                    },
                )

        self._deleted.add(name)
        if hasattr(self, name):
            super().__delattr__(name)

    def __dir__(self):
        return sorted(
            s
            for s in [*self.__dict__, *dir(self.default_settings)]
            if s not in self._deleted
        )

        deleted = setting in self._deleted
        set_locally = setting in self.__dict__
        set_on_default = getattr(
            self.default_settings, "is_overridden", lambda s: False
        )(setting)
        return deleted or set_locally or set_on_default

        return "<%(cls)s>" % {
        }

settings = LazySettings()

from evennia.utils import validatorfuncs
from evennia.utils.validatorfuncs import _TZ_DICT

class BaseOption:
    """
    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
    default value. It saves to the owner's Attributes using its Handler's save
    category.

    Designed to be extremely overloadable as some options can be cantankerous.

        valid: Shortcut to the loaded VALID_HANDLER.
        validator_key (str): The key of the Validator this uses.

    """

        return "<Option {key}: {value}>".format(key=self.key, value=crop(str(self.value), width=10))

    def __init__(self, handler, key, description, default):
        """

        Args:
            handler (OptionHandler): The OptionHandler that 'owns' this Option.
            key (str): The name this will be used for storage in a dictionary.
                Must be unique per OptionHandler.
            description (str): What this Option's text will show in commands and menus.
            default: A default value for this Option.

        """
        self.default_value = default
        self.description = description

        # Value Storage contains None until the Option is loaded.
        self.value_storage = None

        # And it's not loaded until it's called upon to spit out its contents.

    @property
    def changed(self):
        return self.value_storage != self.default_value

    @property
    def default(self):
        return self.default_value

    @property
            self.load()
            return self.value_storage
        else:
            return self.default

        self.set(value)

    def set(self, value, **kwargs):
        """
        Takes user input and stores appropriately. This method allows for
        passing extra instructions into the validator.

        Args:
            value (str): The new value of this Option.
            kwargs (any): Any kwargs will be passed into
                `self.validate(value, **kwargs)` and `self.save(**kwargs)`.

        """
        final_value = self.validate(value, **kwargs)
        self.value_storage = final_value
        self.save(**kwargs)

        """
        Takes the provided save data, validates it, and gets this Option ready to use.

        Returns:
            Boolean: Whether loading was successful.

        """
        loadfunc = self.handler.loadfunc
        load_kwargs = self.handler.load_kwargs

        try:
            self.value_storage = self.deserialize(
                loadfunc(self.key, default=self.default_value, **load_kwargs)
            )

    def save(self, **kwargs):
        """
        Stores the current value using `.handler.save_handler(self.key, value, **kwargs)`
        where `kwargs` are a combination of those passed into this function and
        the ones specified by the `OptionHandler`.

            any (any): Not used by default. These are passed in from self.set
                and allows the option to let the caller customize saving by
                overriding or extend the default save kwargs

        """
        value = self.serialize()
        save_kwargs = {**self.handler.save_kwargs, **kwargs}
        savefunc = self.handler.savefunc
        savefunc(self.key, value=value, **save_kwargs)

    def deserialize(self, save_data):
        """
        Perform sanity-checking on the save data as it is loaded from storage.
        This isn't the same as what validator-functions provide (those work on
        user input). For example, save data might be a timedelta or a list or
        some other object.

        Args:
            save_data: The data to check.

        Returns:
            any (any): Whatever the Option needs to track, like a string or a
                datetime. The display hook is responsible for what is actually
                displayed to user.
        """
        return save_data

    def serialize(self):
        """
        Serializes the save data for Attribute storage.

        Returns:
            any (any): Whatever is best for storage.

        """
        return self.value_storage

    def validate(self, value, **kwargs):
        """
        Validate user input, which is presumed to be a string.

        Args:
            value (str): User input.
            account (AccountDB): The Account that is performing the validation.
                This is necessary because of other settings which may affect the
                check, such as an Account's timezone affecting how their datetime
                entries are processed.
        Returns:
            any (any): The results of the validation.
        Raises:
            ValidationError: If input value failed validation.

        """
        return validatorfuncs.text(value, option_key=self.key, **kwargs)

    def display(self, **kwargs) -> str:
        """
        Renders the Option's value as something pretty to look at.

            any (any): These are options passed by the caller to potentially
                customize display dynamically.

        Returns:
            str: How the stored value should be projected to users (e.g. a raw
                timedelta is pretty ugly).

        """
        return self.value if isinstance(self.value, str) else str(self.value)

# Option classes

class Text(BaseOption):
        got_data = str(save_data)
        if not got_data:
            raise ValueError(f"{self.key} expected Text data, got '{save_data}'")
        return got_data

class Email(BaseOption):
        return validatorfuncs.email(value, option_key=self.key, **kwargs)

            raise ValueError(f"{self.key} expected String data, got '{save_data}'")

class Boolean(BaseOption):
        return validatorfuncs.boolean(value, option_key=self.key, **kwargs)

    def display(self, **kwargs):
        if self.value:
            return "1 - On/True"
        return "0 - Off/False"

        if not isinstance(save_data, bool):
            raise ValueError(f"{self.key} expected Boolean, got '{save_data}'")

class Color(BaseOption):
        return validatorfuncs.color(value, option_key=self.key, **kwargs)

        return f"{self.value} - |{self.value}this|n"

        if not save_data or len(strip_ansi(f"|{save_data}|n")) > 0:
            raise ValueError(f"{self.key} expected Color Code, got '{save_data}'")

class Timezone(BaseOption):
        return validatorfuncs.timezone(value, option_key=self.key, **kwargs)

    @property
        return _TZ_DICT[self.default_value]

        if save_data not in _TZ_DICT:
            raise ValueError(f"{self.key} expected Timezone Data, got '{save_data}'")
        return _TZ_DICT[save_data]

        return str(self.value_storage)

class UnsignedInteger(BaseOption):
    validator_key = "unsigned_integer"

        return validatorfuncs.unsigned_integer(value, option_key=self.key, **kwargs)

        if isinstance(save_data, int) and save_data >= 0:
            return save_data
        raise ValueError(f"{self.key} expected Whole Number 0+, got '{save_data}'")

class SignedInteger(BaseOption):
        return validatorfuncs.signed_integer(value, option_key=self.key, **kwargs)

        if isinstance(save_data, int):
        raise ValueError(f"{self.key} expected Whole Number, got '{save_data}'")

class PositiveInteger(BaseOption):
        return validatorfuncs.positive_integer(value, option_key=self.key, **kwargs)

        if isinstance(save_data, int) and save_data > 0:
        raise ValueError(f"{self.key} expected Whole Number 1+, got '{save_data}'")

class Duration(BaseOption):
        return validatorfuncs.duration(value, option_key=self.key, **kwargs)

            return datetime.timedelta(0, save_data, 0, 0, 0, 0, 0)
        raise ValueError(f"{self.key} expected Timedelta in seconds, got '{save_data}'")

        return self.value_storage.seconds

class Datetime(BaseOption):
        return validatorfuncs.datetime(value, option_key=self.key, **kwargs)

            return datetime.datetime.utcfromtimestamp(save_data)
        raise ValueError(f"{self.key} expected UTC Datetime in EPOCH format, got '{save_data}'")

        return int(self.value_storage.strftime("%s"))

class Future(Datetime):
        return validatorfuncs.future(value, option_key=self.key, **kwargs)

class Lock(Text):
        return validatorfuncs.lock(value, option_key=self.key, **kwargs)

from evennia.utils.containers import OPTION_CLASSES
from evennia.utils.utils import string_partial_matching

class InMemorySaveHandler:
    """
    Fallback SaveHandler, implementing a minimum of the required save mechanism
    and storing data in memory.

    """

        self.storage = {}

    def add(self, key, value=None, **kwargs):
        self.storage[key] = value

    def get(self, key, default=None, **kwargs):
        return self.storage.get(key, default)

class OptionHandler:
    """
    This is a generic Option handler.  Retrieve options either as properties on
    this handler or by using the .get method.

    This is used for Account.options but it could be used by Scripts or Objects
    just as easily. All it needs to be provided is an options_dict.

    """

        self,
        obj,
        options_dict=None,
        save_kwargs=None,
        load_kwargs=None,
    ):
        """
        Initialize an OptionHandler.

        Args:
            obj (object): The object this handler sits on. This is usually a TypedObject.
            options_dict (dict): A dictionary of option keys, where the values
                are options. The format of those tuples is: ('key', "Description to
                show", 'option_type', <default value>)
            savefunc (callable): A callable for all options to call when saving itself.
                It will be called as `savefunc(key, value, **save_kwargs)`. A common one
                to pass would be AttributeHandler.add.
            loadfunc (callable): A callable for all options to call when loading data into
                itself. It will be called as `loadfunc(key, default=default, **load_kwargs)`.
                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.

        """
        self.options_dict = {} if options_dict is None else options_dict

        if not savefunc and loadfunc:
            self._in_memory_handler = InMemorySaveHandler()
            savefunc = InMemorySaveHandler.add
            loadfunc = InMemorySaveHandler.get
        self.savefunc = savefunc
        self.loadfunc = loadfunc
        self.save_kwargs = {} if save_kwargs is None else save_kwargs
        self.load_kwargs = {} if load_kwargs is None else load_kwargs

        # This dictionary stores the in-memory Options objects by their key for
        # quick lookup.

        """
        Allow for obj.options.key

        """

        """
        Allow for obj.options.key = value

        But we must be careful to avoid infinite loops!

        """
        try:
            if key in _GA(self, "options_dict"):
                _GA(self, "set")(key, value)
            pass
        _SA(self, key, value)

    def _load_option(self, key):
        """
        Loads option on-demand if it has not been loaded yet.

        Args:
            key (str): The option being loaded.

        Returns:

        """
        desc, clsname, default_val = self.options_dict[key]
        loaded_option = OPTION_CLASSES.get(clsname)(self, key, desc, default_val)
        # store the value for future easy access
        self.options[key] = loaded_option
        return loaded_option

    def get(self, key, default=None, return_obj=False, raise_error=False):
        """
        Retrieves an Option stored in the handler. Will load it if it doesn't exist.

        Args:
            key (str): The option key to retrieve.
            default (any): What to return if the option is defined.
            return_obj (bool, optional): If True, returns the actual option
                object instead of its value.
            raise_error (bool, optional): Raise Exception if key is not found in options.
        Returns:
            option_value (any or Option): An option value  the Option itself.
        Raises:
            KeyError: If option is not defined.

        """
        if key not in self.options_dict:
            if raise_error:
                raise KeyError(_("Option not found!"))
        # get the options or load/recache it
        op_found = self.options.get(key) or self._load_option(key)
        return op_found if return_obj else op_found.value

    def set(self, key, value, **kwargs) -> "BaseOption":
        """
        Change an individual option.

        Args:
            key (str): The key of an option that can be changed. Allows partial matching.
            value (str): The value that should be checked, coerced, and stored.:
            kwargs (any, optional): These are passed into the Option's validation function,
                save function and display function and allows to customize either.

        Returns:
            BaseOption: The matched object. Its new value can be accessed with
                op.value or op.display().

        """
        if not key:
            raise ValueError(_("Option field blank!"))
        match = string_partial_matching(list(self.options_dict.keys()), key, ret_index=False)
            raise ValueError(_("Option not found!"))
        if len(match) > 1:
                _("Multiple matches:") + f"{', '.join(match)}. " + _("Please be more specific.")
            )
        match = match[0]
        op = self.get(match, return_obj=True)
        op.set(value, **kwargs)
        return op

    def all(self, return_objs=False):
        """
        Get all options defined on this handler.

        Args:
            return_objs (bool, optional): Return the actual Option objects rather
                than their values.
        Returns:
            all_options (dict): All options on this handler, either `{key: value}`
                or `{key: <Option>}` if `return_objs` is `True`.

        """
        return [self.get(key, return_obj=return_objs) for key in self.options_dict]

#
#  Copyright (c) 2009-2010 Gintautas Miliauskas
#
#   Permission is hereby granted, free of charge, to any person
#   obtaining a copy of this software and associated documentation
#   files (the "Software"), to deal in the Software without
#   restriction, including without limitation the rights to use,
#   copy, modify, merge, publish, distribute, sublicense, and/or sell
#   copies of the Software, and to permit persons to whom the
#   Software is furnished to do so, subject to the following
#   conditions:
#
#   The above copyright notice and this permission notice shall be
#   included in all copies or substantial portions of the Software.
#
#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
#   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
#   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
#   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
#   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
#   OTHER DEALINGS IN THE SOFTWARE.

"""
Pickle field implementation for Django.

Modified for Evennia by Griatch and the Evennia community.

"""
from base64 import b64decode, b64encode
from copy import Error as CopyError
from pickle import dumps, loads
from zlib import compress, decompress

# import six # this is actually a pypy component, not in default syslib
from django.forms.fields import CharField
from django.forms.widgets import Textarea
from django.utils.encoding import force_str

from evennia.utils.dbserialize import pack_dbobj

DEFAULT_PROTOCOL = 4

class PickledObject(str):
    """
    A subclass of string so it can be told whether a string is a pickled
    object or not (if the object is an instance of this class then it must
    [well, should] be a pickled one).

    Only really useful for passing pre-encoded values to ``default``
    with ``dbsafe_encode``, not that doing so is necessary. If you
    remove PickledObject and its references, you won't be able to pass
    in pre-encoded values anymore, but you can always just pass in the
    python objects themselves.
    """

class _ObjectWrapper(object):
    """
    A class used to wrap object that have properties that may clash with the
    ORM internals.

    For example, objects with the `prepare_database_save` property such as
    `django.db.Model` subclasses won't work under certain conditions and the
    same apply for trying to retrieve any `callable` object.
    """

    __slots__ = ("_obj",)

        self._obj = obj

def wrap_conflictual_object(obj):
    if hasattr(obj, "prepare_database_save") or callable(obj):
        obj = _ObjectWrapper(obj)
    return obj

def dbsafe_encode(value, compress_object=False, pickle_protocol=DEFAULT_PROTOCOL):
    # We use deepcopy() here to avoid a problem with cPickle, where dumps
    # can generate different character streams for same lookup value if
    # they are referenced differently.
    # The reason this is important is because we do all of our lookups as
    # simple string matches, thus the character streams must be the same
    # for the lookups to work properly. See tests.py for more information.
    try:
        value = deepcopy(value)
    except CopyError:
        # this can happen on a manager query where the search query string is a
        # database model.
        value = pack_dbobj(value)

    value = dumps(value, protocol=pickle_protocol)

    if compress_object:
        value = compress(value)
    value = b64encode(value).decode()  # decode bytes to str
    return PickledObject(value)

def dbsafe_decode(value, compress_object=False):
    value = value.encode()  # encode str to bytes
    value = b64decode(value)
        value = decompress(value)
    return loads(value)

class PickledWidget(Textarea):
    """
    This is responsible for outputting HTML representing a given field.
    """

    def render(self, name, value, attrs=None, renderer=None):
        """Display of the PickledField in django admin"""

        repr_value = repr(value)

        # analyze represented value to see how big the field should be
        if attrs is not None:
            attrs["name"] = name
        else:
            attrs = {"name": name}
        attrs["cols"] = 30
        # adapt number of rows to number of lines in string
        rows = 1
        if isinstance(value, str) and "\n" in repr_value:
            rows = max(1, len(value.split("\n")))
        attrs["rows"] = rows
        attrs = self.build_attrs(attrs)

        try:
            # necessary to convert it back after repr(), otherwise validation errors will mutate it
            value = literal_eval(repr_value)
        except (ValueError, SyntaxError):
            # we could not eval it, just show its prepresentation
            value = repr_value
        return super().render(name, value, attrs=attrs, renderer=renderer)

    def value_from_datadict(self, data, files, name):
        dat = data.get(name)
        # import evennia;evennia.set_trace()
        return dat

class PickledFormField(CharField):
    """
    This represents one input field for the form.

    """

    widget = PickledWidget
    default_error_messages = dict(CharField.default_error_messages)
    default_error_messages["invalid"] = (
        "This is not a Python Literal. You can store things like strings, "
        "integers, or floats, but you must do it by typing them as you would "
        "type them in the Python Interpreter. For instance, strings must be "
        "surrounded by quote marks. We have converted it to a string for your "
        "convenience. If it is acceptable, please hit save again."
    )

        # This needs to fall through to literal_eval.
        kwargs["required"] = False

    def clean(self, value):
        value = super().clean(value)

        # handle empty input
        try:
            if not value.strip():
                # Field was left blank. Make this None.
                value = "None"
            pass

        # parse raw Python
        try:
            return literal_eval(value)
            pass

        # fall through to parsing the repr() of the data
        try:
            value = repr(value)
            raise ValidationError(self.error_messages["invalid"])

class PickledObjectField(models.Field):
    """
    A field that will accept *any* python object and store it in the
    database. PickledObjectField will optionally compress its values if
    declared with the keyword argument ``compress=True``.

    Does not actually encode and compress ``None`` objects (although you
    can still do lookups using None). This way, it is still possible to
    use the ``isnull`` lookup type correctly.
    """

        self.compress = kwargs.pop("compress", False)
        self.protocol = kwargs.pop("protocol", DEFAULT_PROTOCOL)

    def get_default(self):
        """
        Returns the default value for this field.

        The default implementation on models.Field calls force_str
        on the default, which means you can't set arbitrary Python
        objects as the default. To fix this, we just return the value
        without calling force_str on it. Note that if you set a
        callable as a default, the field will still call it. It will
        *not* try to pickle and encode it.

        """
        if self.has_default():
            if callable(self.default):
                return self.default()
        # If the field doesn't have a default, then we punt to models.Field.
        return super().get_default()

    def from_db_value(self, value, *args):
        """
        B64decode and unpickle the object, optionally decompressing it.

        If an error is raised in de-pickling and we're sure the value is
        a definite pickle, the error is allowed to propagate. If we
        aren't sure if the value is a pickle or not, then we catch the
        error and return the original value instead.

        """
        if value is not None:
            try:
                value = dbsafe_decode(value, self.compress)
                # If the value is a definite pickle; and an error is raised in
                # de-pickling it should be allowed to propogate.
                if isinstance(value, PickledObject):
                    raise
            else:
                if isinstance(value, _ObjectWrapper):
                    return value._obj

    def formfield(self, **kwargs):
        return PickledFormField(**kwargs)

    def pre_save(self, model_instance, add):
        value = super().pre_save(model_instance, add)
        return wrap_conflictual_object(value)

    def get_db_prep_value(self, value, connection=None, prepared=False):
        """
        Pickle and b64encode the object, optionally compressing it.

        The pickling protocol is specified explicitly (by default 2),
        rather than as -1 or HIGHEST_PROTOCOL, because we don't want the
        protocol to change over time. If it did, ``exact`` and ``in``
        lookups would likely fail, since pickle would now be generating
        a different string.

        """
        if value is not None and not isinstance(value, PickledObject):
            # We call force_str here explicitly, so that the encoded string
            # isn't rejected by the postgresql backend. Alternatively,
            # we could have just registered PickledObject with the psycopg
            # marshaller (telling it to store it like it would a string), but
            # since both of these methods result in the same value being stored,
            # doing things this way is much easier.
            value = force_str(dbsafe_encode(value, self.compress, self.protocol))

    def value_to_string(self, obj):
        value = self.value_from_object(obj)
        return self.get_db_prep_value(value)

    def get_internal_type(self):
        return "TextField"

    def get_db_prep_lookup(self, lookup_type, value, connection=None, prepared=False):
        if lookup_type not in ["exact", "in", "isnull"]:
            raise TypeError("Lookup type %s is not supported." % lookup_type)
        # The Field model already calls get_db_prep_value before doing the
        # actual lookup, so all we need to do is limit the lookup types.
        return super().get_db_prep_lookup(
            lookup_type, value, connection=connection, prepared=prepared
        )

"""
This is a convenient container gathering all the main
search methods for the various database tables.

It is intended to be used e.g. as

> from evennia.utils import search
> match = search.objects(...)

Note that this is not intended to be a complete listing of all search
methods! You need to refer to the respective manager to get all
possible search methods. To get to the managers from your code, import
the database model and call its 'objects' property.

Also remember that all commands in this file return lists (also if
there is only one match) unless noted otherwise.

Example: To reach the search method 'get_object_with_account'
         in evennia/objects/managers.py:

> from evennia.objects.models import ObjectDB
> match = Object.objects.get_object_with_account(...)

"""

# Import the manager methods to be wrapped

# limit symbol import from API
__all__ = (
    "search_object",
    "search_account",
    "search_script",
    "search_message",
    "search_channel",
    "search_help_entry",
    "search_tag",
    "search_script_tag",
    "search_account_tag",
    "search_channel_tag",
    "search_typeclass",
)

# import objects this way to avoid circular import problems
try:
    ObjectDB = ContentType.objects.get(app_label="objects", model="objectdb").model_class()
    AccountDB = ContentType.objects.get(app_label="accounts", model="accountdb").model_class()
    ScriptDB = ContentType.objects.get(app_label="scripts", model="scriptdb").model_class()
    Msg = ContentType.objects.get(app_label="comms", model="msg").model_class()
    ChannelDB = ContentType.objects.get(app_label="comms", model="channeldb").model_class()
    HelpEntry = ContentType.objects.get(app_label="help", model="helpentry").model_class()
    Tag = ContentType.objects.get(app_label="typeclasses", model="tag").model_class()
except (OperationalError, ProgrammingError):
    # this is a fallback used during tests/doc building
    print("Database not available yet - using temporary fallback for search managers.")
    from evennia.comms.models import ChannelDB, Msg
    from evennia.help.models import HelpEntry
    from evennia.scripts.models import ScriptDB
    from evennia.typeclasses.tags import Tag  # noqa

# Search manager-wrappers

#
# Search objects as a character
#
# NOTE: A more powerful wrapper of this method
#  is reachable from within each command class
#  by using self.caller.search()!
#
#    def object_search(self, ostring=None,
#                      attribute_name=None,
#                      typeclass=None,
#                      candidates=None,
#                      exact=True):
#
#        Search globally or in a list of candidates and return results.
#        The result is always a list of Objects (or the empty list)
#
#        Arguments:
#        ostring: (str) The string to compare names against. By default (if
#                  not attribute_name is set), this will search object.key
#                  and object.aliases in order. Can also be on the form #dbref,
#                  which will, if exact=True be matched against primary key.
#        attribute_name: (str): Use this named ObjectAttribute to match ostring
#                        against, instead of the defaults.
#        typeclass (str or TypeClass): restrict matches to objects having
#                  this typeclass. This will help speed up global searches.
#        candidates (list obj ObjectDBs): If supplied, search will only be
#                  performed among the candidates in this list. A common list
#                  of candidates is the contents of the current location.
#        exact (bool): Match names/aliases exactly or partially. Partial
#                  matching matches the beginning of words in the names/aliases,
#                  using a matching routine to separate multiple matches in
#                  names with multiple components (so "bi sw" will match
#                  "Big sword"). Since this is more expensive than exact
#                  matching, it is recommended to be used together with
#                  the objlist keyword to limit the number of possibilities.
#                  This keyword has no meaning if attribute_name is set.
#
#        Returns:
#        A list of matching objects (or a list with one unique match)
#    def object_search(self, ostring, caller=None,
#                      attribute_name=None):
#
search_object = ObjectDB.objects.search_object
search_objects = search_object
object_search = search_object
objects = search_objects

#
# Search for accounts
#
# account_search(self, ostring)

#     Searches for a particular account by name or
#     database id.
#
#     ostring = a string or database id.
#

search_account = AccountDB.objects.search_account
search_accounts = search_account
account_search = search_account
accounts = search_accounts

#
#   Searching for scripts
#
# script_search(self, ostring, obj=None, only_timed=False)
#
#     Search for a particular script.
#
#     ostring - search criterion - a script ID or key
#     obj - limit search to scripts defined on this object
#     only_timed - limit search only to scripts that run
#                  on a timer.
#

search_script = ScriptDB.objects.search_script
search_scripts = search_script
script_search = search_script
scripts = search_scripts
#
# Searching for communication messages
#
#
# message_search(self, sender=None, receiver=None, channel=None, freetext=None)
#
#     Search the message database for particular messages. At least one
#     of the arguments must be given to do a search.
#
#     sender - get messages sent by a particular account
#     receiver - get messages received by a certain account
#     channel - get messages sent to a particular channel
#     freetext - Search for a text string in a message.
#                NOTE: This can potentially be slow, so make sure to supply
#                one of the other arguments to limit the search.
#

search_message = Msg.objects.search_message
search_messages = search_message
message_search = search_message
messages = search_messages

#
# Search for Communication Channels
#
# channel_search(self, ostring)
#
#     Search the channel database for a particular channel.
#
#     ostring - the key or database id of the channel.
#     exact -  requires an exact ostring match (not case sensitive)
#

search_channel = ChannelDB.objects.search_channel
search_channels = search_channel
channel_search = search_channel
channels = search_channels

#
# Find help entry objects.
#
# search_help(self, ostring, help_category=None)
#
#     Retrieve a search entry object.
#
#     ostring - the help topic to look for
#     category - limit the search to a particular help topic
#

search_help = HelpEntry.objects.search_help
search_help_entry = search_help
search_help_entries = search_help
help_entry_search = search_help
help_entries = search_help

# Locate Attributes

#    search_object_attribute(key, category, value, strvalue) (also search_attribute works)
#    search_account_attribute(key, category, value, strvalue) (also search_attribute works)
#    search_script_attribute(key, category, value, strvalue) (also search_attribute works)
#    search_channel_attribute(key, category, value, strvalue) (also search_attribute works)

# Note that these return the object attached to the Attribute,
# not the attribute object itself (this is usually what you want)

def search_object_attribute(
    key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs
):
    return ObjectDB.objects.get_by_attribute(
        key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs
    )

def search_account_attribute(
):
    return AccountDB.objects.get_by_attribute(
    )

def search_script_attribute(
):
    return ScriptDB.objects.get_by_attribute(
    )

def search_channel_attribute(
):
    return ChannelDB.objects.get_by_attribute(
    )

# search for attribute objects
search_attribute_object = ObjectDB.objects.get_attribute

# Locate Tags

#    search_object_tag(key=None, category=None) (also search_tag works)
#    search_account_tag(key=None, category=None)
#    search_script_tag(key=None, category=None)
#    search_channel_tag(key=None, category=None)

# Note that this returns the object attached to the tag, not the tag
# object itself (this is usually what you want)

def search_object_by_tag(key=None, category=None, tagtype=None, **kwargs):
    """
    Find object based on tag or category.

    Args:
        key (str, optional): The tag key to search for.
        category (str, optional): The category of tag
            to search for. If not set, uncategorized
            tags will be searched.
        tagtype (str, optional): 'type' of Tag, by default
            this is either `None` (a normal Tag), `alias` or
            `permission`. This always apply to all queried tags.
        kwargs (any): Other optional parameter that may be supported
            by the manager method.

    Returns:
        matches (list): List of Objects with tags matching
            the search criteria, or an empty list if no
            matches were found.

    """
    return ObjectDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs)

search_tag = search_object_by_tag  # this is the most common case

def search_account_tag(key=None, category=None, tagtype=None, **kwargs):
    """
    Find account based on tag or category.

    Args:

    Returns:
        matches (list): List of Accounts with tags matching

    """
    return AccountDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs)

def search_script_tag(key=None, category=None, tagtype=None, **kwargs):
    """
    Find script based on tag or category.

    Args:

    Returns:
        matches (list): List of Scripts with tags matching

    """
    return ScriptDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs)

def search_channel_tag(key=None, category=None, tagtype=None, **kwargs):
    """
    Find channel based on tag or category.

    Args:

    Returns:
        matches (list): List of Channels with tags matching

    """
    return ChannelDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs)

# search for tag objects (not the objects they are attached to
search_tag_object = ObjectDB.objects.get_tag

# Locate Objects by Typeclass

# search_objects_by_typeclass(typeclass="", include_children=False, include_parents=False) (also search_typeclass works)
# This returns the objects of the given typeclass

def search_objects_by_typeclass(typeclass, include_children=False, include_parents=False):
    """
    Searches through all objects returning those of a certain typeclass.

    Args:
        typeclass (str or class): A typeclass class or a python path to a typeclass.
        include_children (bool, optional): Return objects with
            given typeclass *and* all children inheriting from this
            typeclass. Mutuall exclusive to `include_parents`.
        include_parents (bool, optional): Return objects with
            given typeclass *and* all parents to this typeclass.
            Mutually exclusive to `include_children`.

    Returns:
        objects (list): The objects found with the given typeclasses.
    """
    return ObjectDB.objects.typeclass_search(
        typeclass=typeclass,
        include_children=include_children,
        include_parents=include_parents,
    )

search_typeclass = search_objects_by_typeclass

"""
Various helper resources for writing unittests.

Classes for testing Evennia core:

- `BaseEvenniaTestCase` - no default objects, only enforced default settings
- `BaseEvenniaTest` - all default objects, enforced default settings
- `BaseEvenniaCommandTest` - for testing Commands, enforced default settings

Classes for testing game folder content:

- `EvenniaTestCase` - no default objects, using gamedir settings (identical to
   standard Python TestCase)
- `EvenniaTest` - all default objects, using gamedir settings
- `EvenniaCommandTest` - for testing game folder commands, using gamedir settings

Other:

- `EvenniaTestMixin` - A class mixin for creating the test environment objects, for
  making custom tests.
- `EvenniaCommandMixin` - A class mixin that adds support for command testing with the .call()
  helper. Used by the command-test classes, but can be used for making a customt test class.

"""
import re
import sys

from evennia import settings_default
from evennia.objects.objects import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom
from evennia.utils import ansi, create
from evennia.utils.idmapper.models import flush_cache
from evennia.utils.utils import all_from_module, to_str
from mock import MagicMock, Mock, patch

_RE_STRIP_EVMENU = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE)

# set up a 'pristine' setting, unaffected by any changes in mygame
DEFAULT_SETTING_RESETS = dict(
    CONNECTION_SCREEN_MODULE="evennia.game_template.server.conf.connection_screens",
    AT_SERVER_STARTSTOP_MODULE="evennia.game_template.server.conf.at_server_startstop",
    AT_SERVICES_PLUGINS_MODULES=["evennia.game_template.server.conf.server_services_plugins"],
    PORTAL_SERVICES_PLUGIN_MODULES=["evennia.game_template.server.conf.portal_services_plugins"],
    MSSP_META_MODULE="evennia.game_template.server.conf.mssp",
    WEB_PLUGINS_MODULE="server.conf.web_plugins",
    LOCK_FUNC_MODULES=("evennia.locks.lockfuncs", "evennia.game_template.server.conf.lockfuncs"),
    INPUT_FUNC_MODULES=[
        "evennia.server.inputfuncs",
        "evennia.game_template.server.conf.inputfuncs",
    ],
    PROTOTYPE_MODULES=["evennia.game_template.world.prototypes"],
    CMDSET_UNLOGGEDIN="evennia.game_template.commands.default_cmdsets.UnloggedinCmdSet",
    CMDSET_SESSION="evennia.game_template.commands.default_cmdsets.SessionCmdSet",
    CMDSET_CHARACTER="evennia.game_template.commands.default_cmdsets.CharacterCmdSet",
    CMDSET_ACCOUNT="evennia.game_template.commands.default_cmdsets.AccountCmdSet",
    CMDSET_PATHS=["evennia.game_template.commands", "evennia", "evennia.contrib"],
    TYPECLASS_PATHS=[
        "evennia",
        "evennia.contrib",
        "evennia.contrib.game_systems",
        "evennia.contrib.base_systems",
        "evennia.contrib.full_systems",
        "evennia.contrib.tutorials",
        "evennia.contrib.utils",
    ],
    BASE_ACCOUNT_TYPECLASS="evennia.accounts.accounts.DefaultAccount",
    BASE_OBJECT_TYPECLASS="evennia.objects.objects.DefaultObject",
    BASE_CHARACTER_TYPECLASS="evennia.objects.objects.DefaultCharacter",
    BASE_ROOM_TYPECLASS="evennia.objects.objects.DefaultRoom",
    BASE_EXIT_TYPECLASS="evennia.objects.objects.DefaultExit",
    BASE_CHANNEL_TYPECLASS="evennia.comms.comms.DefaultChannel",
    BASE_SCRIPT_TYPECLASS="evennia.scripts.scripts.DefaultScript",
    BASE_BATCHPROCESS_PATHS=[
        "evennia.game_template.world",
    ],
    FILE_HELP_ENTRY_MODULES=["evennia.game_template.world.help_entries"],
    FUNCPARSER_OUTGOING_MESSAGES_MODULES=[
        "evennia.utils.funcparser",
        "evennia.game_template.server.conf.inlinefuncs",
    ],
    FUNCPARSER_PROTOTYPE_PARSING_MODULES=[
        "evennia.prototypes.protfuncs",
        "evennia.game_template.server.conf.prototypefuncs",
    ],
    BASE_GUEST_TYPECLASS="evennia.accounts.accounts.DefaultGuest",
    # a special setting boolean TEST_ENVIRONMENT is set by the test runner
    # while the test suite is running.
    TEST_ENVIRONMENT=True,
)

DEFAULT_SETTINGS = {**all_from_module(settings_default), **DEFAULT_SETTING_RESETS}
DEFAULT_SETTINGS.pop("DATABASES")  # we want different dbs tested in CI

# mocking of evennia.utils.utils.delay
def mockdelay(timedelay, callback, *args, **kwargs):
    return Deferred()

# mocking of twisted's deferLater
def mockdeferLater(reactor, timedelay, callback, *args, **kwargs):

def unload_module(module):
    """
    Reset import so one can mock global constants.

    Args:
        module (module, object or str): The module will
            be removed so it will have to be imported again. If given
            an object, the module in which that object sits will be unloaded. A string
            should directly give the module pathname to unload.

    Example:

        ```python
        # (in a test method)
        unload_module(foo)
        with mock.patch("foo.GLOBALTHING", "mockval"):
            import foo
            ... # test code using foo.GLOBALTHING, now set to 'mockval'
        ```

    This allows for mocking constants global to the module, since
    otherwise those would not be mocked (since a module is only
    loaded once).

    """
    if isinstance(module, str):
        modulename = module
    elif hasattr(module, "__module__"):
        modulename = module.__module__
    else:
        modulename = module.__name__

    if modulename in sys.modules:
        del sys.modules[modulename]

def _mock_deferlater(reactor, timedelay, callback, *args, **kwargs):

class EvenniaTestMixin:
    """
    Evennia test environment mixin
    """

    account_typeclass = DefaultAccount
    object_typeclass = DefaultObject
    character_typeclass = DefaultCharacter
    exit_typeclass = DefaultExit
    room_typeclass = DefaultRoom
    script_typeclass = DefaultScript

    def create_accounts(self):
        self.account = create.create_account(
            "TestAccount",
            email="test@test.com",
            password="testpassword",
            typeclass=self.account_typeclass,
        )
        self.account2 = create.create_account(
            "TestAccount2",
        )
        self.account.permissions.add("Developer")

    def teardown_accounts(self):
        if hasattr(self, "account"):
            self.account.delete()
        if hasattr(self, "account2"):
            self.account2.delete()

    # Set up fake prototype module for allowing tests to use named prototypes.
    @override_settings(
        PROTOTYPE_MODULES=["evennia.utils.tests.data.prototypes_example"], DEFAULT_HOME="#1"
    )
    def create_rooms(self):
        self.room1 = create.create_object(self.room_typeclass, key="Room", nohome=True)
        self.room1.db.desc = "room_desc"

        self.room2 = create.create_object(self.room_typeclass, key="Room2")
        self.exit = create.create_object(
            self.exit_typeclass, key="out", location=self.room1, destination=self.room2
        )

    def create_objs(self):
        self.obj1 = create.create_object(
            self.object_typeclass, key="Obj", location=self.room1, home=self.room1
        )
        self.obj2 = create.create_object(
            self.object_typeclass, key="Obj2", location=self.room1, home=self.room1
        )

    def create_chars(self):
            self.character_typeclass, key="Char", location=self.room1, home=self.room1
        )
        self.char1.permissions.add("Developer")
        self.char2 = create.create_object(
            self.character_typeclass, key="Char2", location=self.room1, home=self.room1
        )
        self.char1.account = self.account
        self.account.db._last_puppet = self.char1
        self.char2.account = self.account2
        self.account2.db._last_puppet = self.char2

    def create_script(self):
        self.script = create.create_script(self.script_typeclass, key="Script")

        dummysession = ServerSession()

    def teardown_session(self):
        if hasattr(self, "sessions"):
            del evennia.SESSION_HANDLER[self.session.sessid]

    @patch("evennia.scripts.taskhandler.deferLater", _mock_deferlater)
        """
        Sets up testing environment
        """
        self.backups = (
            evennia.SESSION_HANDLER.data_out,
            evennia.SESSION_HANDLER.disconnect,
            settings.DEFAULT_HOME,
            settings.PROTOTYPE_MODULES,
        )
        evennia.SESSION_HANDLER.data_out = Mock()
        evennia.SESSION_HANDLER.disconnect = Mock()

        self.create_accounts()
        self.create_rooms()
        self.create_objs()
        self.create_chars()
        self.create_script()
        self.setup_session()

        try:
            evennia.SESSION_HANDLER.data_out = self.backups[0]
            evennia.SESSION_HANDLER.disconnect = self.backups[1]
            settings.DEFAULT_HOME = self.backups[2]
            settings.PROTOTYPE_MODULES = self.backups[3]
        except AttributeError as err:
                f"{err}: Teardown error. If you overrode the `setUp()` method "
                "in your test, make sure you also added `super().setUp()`!"
            )

        del evennia.SESSION_HANDLER[self.session.sessid]
        self.teardown_accounts()

@patch("evennia.server.portal.portal.LoopingCall", new=MagicMock())
class EvenniaCommandTestMixin:
    """
    Mixin to add to a test in order to provide the `.call` helper for
    testing the execution and returns of a command.

    Tests a Command by running it and comparing what messages it sends with
    expected values. This tests without actually spinning up the cmdhandler
    for every test, which is more controlled.

    Example:
    ::

        from commands.echo import CmdEcho

        class MyCommandTest(EvenniaTest, CommandTestMixin):

            def test_echo(self):
                '''
                Test that the echo command really returns
                what you pass into it.
                '''
                self.call(MyCommand(), "hello world!",
                          "You hear your echo: 'Hello world!'")

    """

    # formatting for .call's error message
    _ERROR_FORMAT = """
=========================== Wanted message ===================================
{expected_msg}
=========================== Returned message =================================
{returned_msg}
==============================================================================
""".rstrip()

    def call(
        self,
        cmdobj,
        input_args,
        msg=None,
        cmdset=None,
        noansi=True,
        receiver=None,
        cmdstring=None,
        obj=None,
        inputs=None,
        raw_string=None,
    ):
        """
        Test a command by assigning all the needed properties to a cmdobj and
        running the sequence. The resulting `.msg` calls will be mocked and
        the text= calls to them compared to a expected output.

        Args:
            cmdobj (Command): The command object to use.
            input_args (str): This should be the full input the Command should
                see, such as 'look here'. This will become `.args` for the Command
                instance to parse.
            msg (str or dict, optional): This is the expected return value(s)
                returned through `caller.msg(text=...)` calls in the command. If a string, the
                receiver is controlled with the `receiver` kwarg (defaults to `caller`).
                If this is a `dict`, it is a mapping
                `{receiver1: "expected1", receiver2: "expected2",...}` and `receiver` is
                ignored. The message(s) are compared with the actual messages returned
                to the receiver(s) as the Command runs. Each check uses `.startswith`,
                so you can choose to only include the first part of the
                returned message if that's enough to verify a correct result. EvMenu
                decorations (like borders) are stripped and should not be included. This
                should also not include color tags unless `noansi=False`.
                If the command returns texts in multiple separate `.msg`-
                calls to a receiver, separate these with `|` if `noansi=True`
                (default) and `||` if `noansi=False`. If no `msg` is given (`None`),
                then no automatic comparison will be done.
            cmdset (str, optional): If given, make `.cmdset` available on the Command
                instance as it runs. While `.cmdset` is normally available on the
                Command instance by default, this is usually only used by
                commands that explicitly operates/displays cmdsets, like
                `examine`.
            noansi (str, optional): By default the color tags of the `msg` is
                ignored, this makes them significant. If unset, `msg` must contain
                the same color tags as the actual return message.
            caller (Object or Account, optional): By default `self.char1` is used as the
                command-caller (the `.caller` property on the Command). This allows to
                execute with another caller, most commonly an Account.
            receiver (Object or Account, optional): This is the object to receive the
                return messages we want to test. By default this is the same as `caller`
                (which in turn defaults to is `self.char1`). Note that if `msg` is
                a `dict`, this is ignored since the receiver is already specified there.
            cmdstring (str, optional): Normally this is the Command's `key`.
                This allows for tweaking the `.cmdname` property of the
                Command`.  This isb used for commands with multiple aliases,
                where the command explicitly checs which alias was used to
                determine its functionality.
            obj (str, optional): This sets the `.obj` property of the Command - the
                object on which the Command 'sits'. By default this is the same as `caller`.
                This can be used for testing on-object Command interactions.
            inputs (list, optional): A list of strings to pass to functions that pause to
                take input from the user (normally using `@interactive` and
                `ret = yield(question)` or `evmenu.get_input`). Each  element of the
                list will be passed into the command as if the user answered each prompt
                in that order.
            raw_string (str, optional): Normally the `.raw_string` property  is set as
                a combination of your `key/cmdname` and `input_args`. This allows
                direct control of what this is, for example for testing edge cases
                or malformed inputs.

        Returns:
            str or dict: The message sent to `receiver`, or a dict of
                `{receiver: "msg", ...}` if multiple are given. This is usually
                only used with `msg=None` to do the validation externally.

        Raises:
            AssertionError: If the returns of `.msg` calls (tested with `.startswith`) does not
                match `expected_input`.

        Notes:
            As part of the tests, all methods of the Command will be called in
            the proper order:

            - cmdobj.at_pre_cmd()
            - cmdobj.parse()
            - cmdobj.func()
            - cmdobj.at_post_cmd()

        """
        # The `self.char1` is created in the `EvenniaTest` base along with
        # other helper objects like self.room and self.obj
        caller = caller if caller else self.char1
        cmdobj.caller = caller
        cmdobj.cmdname = cmdstring if cmdstring else cmdobj.key
        cmdobj.raw_cmdname = cmdobj.cmdname
        cmdobj.cmdstring = cmdobj.cmdname  # deprecated
        cmdobj.args = input_args
        cmdobj.cmdset = cmdset
        cmdobj.session = evennia.SESSION_HANDLER.session_from_sessid(1)
        cmdobj.account = self.account
        cmdobj.raw_string = raw_string if raw_string is not None else cmdobj.key + " " + input_args
        cmdobj.obj = obj or (caller if caller else self.char1)
        inputs = inputs or []

        # set up receivers
        receiver_mapping = {}
        if isinstance(msg, dict):
            # a mapping {receiver: msg, ...}
                recv: str(msg).strip() if msg else None for recv, msg in msg.items()
            }
        else:
            # a single expected string and thus a single receiver (defaults to caller)
            receiver = receiver if receiver else caller
            receiver_mapping[receiver] = str(msg).strip() if msg is not None else None

        unmocked_msg_methods = {}
        for receiver in receiver_mapping:
            # save the old .msg method so we can get it back
            # cleanly  after the test
            unmocked_msg_methods[receiver] = receiver.msg
            # replace normal `.msg` with a mock
            receiver.msg = Mock()

        # Run the methods of the Command. This mimics what happens in the
        # cmdhandler. This will have the mocked .msg be called as part of the
        # execution. Mocks remembers what was sent to them so we will be able
        # to retrieve what was sent later.
        try:
            if cmdobj.at_pre_cmd():
                return
            cmdobj.parse()
            ret = cmdobj.func()

            # handle func's with yield in them (making them generators)
                    try:
                        inp = inputs.pop() if inputs else None
                        if inp:
                            try:
                                # this mimics a user's reply to a prompt
                                ret.send(inp)
                            except TypeError:
                                next(ret)
                                ret = ret.send(inp)
                        else:
                            # non-input yield, like yield(10). We don't pause
                            # but fire it immediately.
                            next(ret)
                    except StopIteration:
                        break

            cmdobj.at_post_cmd()
            pass
            pass

        for inp in inputs:
            # 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
        # go to each receiver.

        returned_msgs = {}
        for receiver, expected_msg in receiver_mapping.items():
            # get the stored messages from the Mock with Mock.mock_calls.
                args[0] if args and args[0] else kwargs.get("text", to_str(kwargs))
                for name, args, kwargs in receiver.msg.mock_calls
            ]
            # we can return this now, we are done using the mock
            receiver.msg = unmocked_msg_methods[receiver]

                str(smsg[0]) if isinstance(smsg, tuple) else str(smsg) for smsg in stored_msg
            ]
            if expected_msg is None:
                # no expected_msg; just build the returned_msgs dict

                returned_msg = "\n".join(str(msg) for msg in stored_msg)
                returned_msgs[receiver] = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
            else:
                # compare messages to expected

                # set our separator for returned messages based on parsing ansi or not
                msg_sep = "|" if noansi else "||"

                # We remove Evmenu decorations since that just makes it harder
                # to write the comparison string. We also strip ansi before this
                # comparison since otherwise it would mess with the regex.
                returned_msg = msg_sep.join(
                    _RE_STRIP_EVMENU.sub("", ansi.parse_ansi(mess, strip_ansi=noansi))
                    for mess in stored_msg
                ).strip()

                # this is the actual test
                if expected_msg == "" and returned_msg or not returned_msg.startswith(expected_msg):
                    # failed the test
                    raise AssertionError(
                        self._ERROR_FORMAT.format(
                            expected_msg=expected_msg, returned_msg=returned_msg
                        )
                    )
                # passed!
                returned_msgs[receiver] = returned_msg

        if len(returned_msgs) == 1:
            return list(returned_msgs.values())[0]
        return returned_msgs

# Base testing classes

@override_settings(**DEFAULT_SETTINGS)
class BaseEvenniaTestCase(TestCase):
    """
    Base test (with no default objects) but with enforced default settings.

    """

    def tearDown(self) -> None:

class EvenniaTestCase(TestCase):
    """
    For use with gamedir settings; Just like the normal test case, only for naming consistency.

    Notes:

    -   Inheriting from this class will bypass EvenniaTestMixin, and therefore
        not setup some default objects. This can result in faster tests.

    -   If you do inherit from this class for your unit tests, and have
        overridden the tearDown() method, please also call flush_cache(). Not
        doing so will result in flakey and order-dependent tests due to the
        Django ID cache not being flushed.
    """

class BaseEvenniaTest(EvenniaTestMixin, TestCase):
    """
    This class parent has all default objects and uses only default settings.

    """

class EvenniaTest(EvenniaTestMixin, TestCase):
    """
    This test class is intended for inheriting in mygame tests.
    It helps ensure your tests are run with your own objects
    and settings from your game folder.

    """

    object_typeclass = settings.BASE_OBJECT_TYPECLASS
    character_typeclass = settings.BASE_CHARACTER_TYPECLASS
    exit_typeclass = settings.BASE_EXIT_TYPECLASS
    script_typeclass = settings.BASE_SCRIPT_TYPECLASS

@patch("evennia.commands.account.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.admin.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.batchprocess.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.building.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.comms.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.general.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.help.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.syscommands.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.system.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.unloggedin.COMMAND_DEFAULT_CLASS", MuxCommand)
class BaseEvenniaCommandTest(BaseEvenniaTest, EvenniaCommandTestMixin):
    """
    Commands only using the default settings.

    """

class EvenniaCommandTest(EvenniaTest, EvenniaCommandTestMixin):
    """
    Parent class to inherit from - makes tests use your own
    classes and settings in mygame.

    """

"""
ANSI -> html converter

Credit for original idea and implementation
goes to Muhammad Alkarouri and his
snippet #577349 on http://code.activestate.com.

(extensively modified by Griatch 2010)
"""

import re
from html import escape as html_escape

from .ansi import *

class TextToHTMLparser(object):
    """
    This class describes a parser for converting from ANSI to html.
    """

    tabstop = 4

    style_codes = [
        # non-color style markers
        ANSI_NORMAL,
        ANSI_UNDERLINE,
        ANSI_HILITE,
        ANSI_UNHILITE,
        ANSI_INVERSE,
        ANSI_BLINK,
        ANSI_INV_HILITE,
        ANSI_BLINK_HILITE,
        ANSI_INV_BLINK,
        ANSI_INV_BLINK_HILITE,
    ]

    ansi_color_codes = [
        # Foreground colors
        ANSI_BLACK,
        ANSI_RED,
        ANSI_GREEN,
        ANSI_YELLOW,
        ANSI_BLUE,
        ANSI_MAGENTA,
        ANSI_CYAN,
        ANSI_WHITE,
    ]

    xterm_fg_codes = [XTERM256_FG.format(i + 16) for i in range(240)]

    ansi_bg_codes = [
        # Background colors
        ANSI_BACK_BLACK,
        ANSI_BACK_RED,
        ANSI_BACK_GREEN,
        ANSI_BACK_YELLOW,
        ANSI_BACK_BLUE,
        ANSI_BACK_MAGENTA,
        ANSI_BACK_CYAN,
        ANSI_BACK_WHITE,
    ]

    xterm_bg_codes = [XTERM256_BG.format(i + 16) for i in range(240)]

    re_style = re.compile(
        r"({})".format(
            "|".join(
                style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes
            ).replace("[", r"\[")
        )
    )

    colorlist = (
        [ANSI_UNHILITE + code for code in ansi_color_codes]
        + [ANSI_HILITE + code for code in ansi_color_codes]
        + xterm_fg_codes
    )

    bglist = ansi_bg_codes + [ANSI_HILITE + code for code in ansi_bg_codes] + xterm_bg_codes

    re_string = re.compile(
        r"(?P<htmlchars>[<&>])|(?P<tab>[\t]+)|(?P<lineend>\r\n|\r|\n)",
        re.S | re.M | re.I,
    )
    re_url = re.compile(
        r'(?<!=")(\b(?:ftp|www|https?)\W+(?:(?!\.(?:\s|$)|&\w+;)[^"\',;$*^\\(){}<>\[\]\s])+)(\.(?:\s|$)|&\w+;|)'
    )
    re_protocol = re.compile(r"^(?:ftp|https?)://")
    re_valid_no_protocol = re.compile(
        r"^(?:www|ftp)\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b[-a-zA-Z0-9@:%_\+.~#?&//=]*"
    )
    re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
    re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)

    def remove_bells(self, text):
        """
        Remove ansi specials

        Args:

        Returns:

        """
        return text.replace(ANSI_BEEP, "")

    def remove_backspaces(self, text):
        """
        Removes special escape sequences

        Args:

        Returns:

        """
        backspace_or_eol = r"(.\010)|(\033\[K)"
        n = 1
        while n > 0:
            text, n = re.subn(backspace_or_eol, "", text, 1)

    def convert_linebreaks(self, text):
        """
        Extra method for cleaning linebreaks

        Args:

        Returns:

        """
        return text.replace("\n", r"<br>")

        """
        Replace urls (http://...) by valid HTML.

        Args:

        Returns:

        """
        m = self.re_url.search(text)
        if m:
            href = m.group(1)
            label = href
            # if there is no protocol (i.e. starts with www or ftp)
            # prefix with http:// so the link isn't treated as relative
            if not self.re_protocol.match(href):
                if not self.re_valid_no_protocol.match(href):
                    return text
                href = "http://" + href
            rest = m.group(2)
            # -> added target to output prevent the web browser from attempting to
            # change pages (and losing our webclient session).
            return (
                text[: m.start()]
                + f'<a href="{href}" target="_blank">{label}</a>{rest}'
                + text[m.end() :]
            )
        else:

        """
        replaces MXP links with HTML code.

        Args:

        Returns:

        """
        val = (
            r"""<a id="mxplink" href="#" """
            """onclick="Evennia.msg(&quot;text&quot;,[&quot;{cmd}&quot;],{{}});"""
            """return false;">{text}</a>""".format(cmd=cmd, text=text)
        )
        return val

        """
        Args:
        Returns:
        """
        val = r"""<a id="mxplink" href="{url}" target="_blank">{text}</a>""".format(
            url=url, text=text
        )
        return val

        """

        Args:

        Returns:

        """
        if cdict["htmlchars"]:
            return html_escape(cdict["htmlchars"])
        elif cdict["lineend"]:
            return "<br>"
        elif cdict["tab"]:
            text = cdict["tab"].replace("\t", " " * (self.tabstop))

        """
        Takes a string with parsed ANSI codes and replaces them with
        HTML spans and CSS classes.

        Args:

        Returns:
        """

        # initialize all the flags and classes
        classes = []
        clean = True

                str_list[i] = "</span>" if not clean else ""
                classes = []
                clean = True

                # erase ANSI code from output
                str_list[i] = ""
                fg = substr

                # set new bg
                bg = substr

                if (
                    substr in (ANSI_BLINK, ANSI_BLINK_HILITE, ANSI_INV_BLINK_HILITE)
                    and "blink" not in classes
                ):
                    classes.append("blink")

                if substr == ANSI_UNDERLINE and "underline" not in classes:
                    classes.append("underline")

            else:
                if not str_list[i - 1]:
                    try:

                    else:

                        classes.append(bg_class)
                        classes.append(color_class)
                    # define the new style span
                    prefix = '<span class="{}">'.format(" ".join(classes))
                    # close any prior span
                    if not clean:
                        prefix = "</span>" + prefix
                    # add span to output
                    str_list[i - 1] = prefix

                    # clean out color classes to easily update next time
                    classes = [cls for cls in classes if "color" not in cls]
                    # flag as currently being styled
                    clean = False

        # close span if necessary
        if not clean:
            str_list.append("</span>")
        return "".join(str_list)

        """

        Args:

        Returns:

        """
        result = self.convert_linebreaks(result)
        # clean out eventual ansi that was missed
        ## result = parse_ansi(result, strip_ansi=True)

HTML_PARSER = TextToHTMLparser()

#
#

def parse_html(string, strip_ansi=False, parser=HTML_PARSER):
    """
    Parses a string, replace ANSI markup with html
    """

"""
Contains all the validation functions.

All validation functions must have a checker (probably a session) and entry arg.

They can employ more paramters at your leisure.

"""

import datetime as _dt
import re as _re

import pytz as _pytz

from evennia.utils.utils import string_partial_matching as _partial
from evennia.utils.utils import validate_email_address

_TZ_DICT = {str(tz): _pytz.timezone(tz) for tz in _pytz.common_timezones}

def text(entry, option_key="Text", **kwargs):
    try:
        return str(entry)
        raise ValueError(_("Input could not be converted to text ({err})").format(err=err))

def color(entry, option_key="Color", **kwargs):
    """
    The color should be just a color character, so 'r' if red color is desired.

    """
    if not entry:
        raise ValueError(_("Nothing entered for a {option_key}!").format(option_key=option_key))
    test_str = strip_ansi(f"|{entry}|n")
    if test_str:
            _("'{entry}' is not a valid {option_key}.").format(entry=entry, option_key=option_key)
        )
    return entry

def datetime(entry, option_key="Datetime", account=None, from_tz=None, **kwargs):
    """
    Process a datetime string in standard forms while accounting for the
    inputer's timezone. Always returns a result in UTC.

    Args:
        entry (str): A date string from a user.
        option_key (str): Name to display this datetime as.
        account (AccountDB): The Account performing this lookup. Unless `from_tz` is provided,
            the account's timezone option will be used.
        from_tz (pytz.timezone): An instance of a pytz timezone object from the
            user. If not provided, tries to use the timezone option of `account`.
            If neither one is provided, defaults to UTC.
    Returns:
        datetime in UTC.
    Raises:
        ValueError: If encountering a malformed timezone, date string or other format error.

    """
        raise ValueError(_("No {option_key} entered!").format(option_key=option_key))
    if not from_tz:
        from_tz = _pytz.UTC
            acct_tz = account.options.get("timezone", "UTC")
            try:
                from_tz = _pytz.timezone(acct_tz)
                    _("Timezone string '{acct_tz}' is not a valid timezone ({err})").format(
                        acct_tz=acct_tz, err=err
                    )
                )
        else:
            from_tz = _pytz.UTC

    utc = _pytz.UTC
    now = _dt.datetime.utcnow().replace(tzinfo=utc)
    cur_year = now.strftime("%Y")
    split_time = entry.split(" ")
    if len(split_time) == 3:
        entry = f"{split_time[0]} {split_time[1]} {split_time[2]} {cur_year}"
    elif len(split_time) == 4:
        entry = f"{split_time[0]} {split_time[1]} {split_time[2]} {split_time[3]}"
    else:
            _("{option_key} must be entered in a 24-hour format such as: {timeformat}").format(
                option_key=option_key, timeformat=now.strftime("%b %d %H:%M")
            )
        )
    try:
        local = _dt.datetime.strptime(entry, "%b %d %H:%M %Y")
            )
        )
    local_tz = from_tz.localize(local)
    return local_tz.astimezone(utc)

def duration(entry, option_key="Duration", **kwargs):
    """
    Take a string and derive a datetime timedelta from it.

    Args:
        entry (string): This is a string from user-input. The intended format is, for example:
            "5d 2w 90s" for 'five days, two weeks, and ninety seconds.' Invalid sections are
            ignored.
        option_key (str): Name to display this query as.

    Returns:
        timedelta

    """
    time_string = entry.lower().split(" ")
    seconds = 0
    minutes = 0
    hours = 0
    days = 0
    weeks = 0

    for interval in time_string:
        if _re.match(r"^[\d]+s$", interval):
            seconds += int(interval.rstrip("s"))
        elif _re.match(r"^[\d]+m$", interval):
            minutes += int(interval.rstrip("m"))
        elif _re.match(r"^[\d]+h$", interval):
            hours += int(interval.rstrip("h"))
        elif _re.match(r"^[\d]+d$", interval):
            days += int(interval.rstrip("d"))
        elif _re.match(r"^[\d]+w$", interval):
            weeks += int(interval.rstrip("w"))
        elif _re.match(r"^[\d]+y$", interval):
            days += int(interval.rstrip("y")) * 365
        else:
                _("Could not convert section '{interval}' to a {option_key}.").format(
                    interval=interval, option_key=option_key
                )
            )

    return _dt.timedelta(days, seconds, 0, 0, minutes, hours, weeks)

def future(entry, option_key="Future Datetime", from_tz=None, **kwargs):
    time = datetime(entry, option_key, from_tz=from_tz)
    if time < _dt.datetime.utcnow().replace(tzinfo=_dt.timezone.utc):
            _("That {option_key} is in the past! Must give a Future datetime!").format(
                option_key=option_key
            )
        )
    return time

def signed_integer(entry, option_key="Signed Integer", **kwargs):
            _("Must enter a whole number for {option_key}!").format(option_key=option_key)
        )
    try:
        num = int(entry)
            _("Could not convert '{entry}' to a whole " "number for {option_key}!").format(
                entry=entry, option_key=option_key
            )
        )
    return num

def positive_integer(entry, option_key="Positive Integer", **kwargs):
    num = signed_integer(entry, option_key)
    if not num >= 1:
            _("Must enter a whole number greater than 0 for {option_key}!").format(
            )
        )
    return num

def unsigned_integer(entry, option_key="Unsigned Integer", **kwargs):
    if not num >= 0:
            _("{option_key} must be a whole number greater than " "or equal to 0!").format(
            )
        )
    return num

def boolean(entry, option_key="True/False", **kwargs):
    """
    Simplest check in computer logic, right? This will take user input to flick the switch on or off

    Args:
        entry (str): A value such as True, On, Enabled, Disabled, False, 0, or 1.
        option_key (str): What kind of Boolean we are setting. What Option is this for?

    Returns:
        Boolean

    """
    error = _("Must enter a true/false input for {option_key}. Accepts {alternatives}.").format(
        option_key=option_key, alternatives="0/1, True/False, On/Off, Yes/No, Enabled/Disabled"
    )
    if not isinstance(entry, str):
        raise ValueError(error)
    entry = entry.upper()
    if entry in ("1", "TRUE", "ON", "ENABLED", "ENABLE", "YES"):
    if entry in ("0", "FALSE", "OFF", "DISABLED", "DISABLE", "NO"):
    raise ValueError(error)

def timezone(entry, option_key="Timezone", **kwargs):
    """
    Takes user input as string, and partial matches a Timezone.

    Args:
        entry (str): The name of the Timezone.
        option_key (str): What this Timezone is used for.

    Returns:
        A PYTZ timezone.

    """
    found = _partial(list(_TZ_DICT.keys()), entry, ret_index=False)
    if len(found) > 1:
            _("That matched: {matches}. Please be more specific!").format(
                matches=", ".join(str(t) for t in found)
            )
        )
    if found:
        return _TZ_DICT[found[0]]
    raise ValueError(
        _("Could not find timezone '{entry}' for {option_key}!").format(
            entry=entry, option_key=option_key
        )
    )

def email(entry, option_key="Email Address", **kwargs):
        raise ValueError(_("Email address field empty!"))
    valid = validate_email_address(entry)
    if not valid:
        raise ValueError(_("That isn't a valid {option_key}!").format(option_key=option_key))

def lock(entry, option_key="locks", access_options=None, **kwargs):
    entry = entry.strip()
        raise ValueError(_("No {option_key} entered to set!").format(option_key=option_key))
    for locksetting in entry.split(";"):
        access_type, lockfunc = locksetting.split(":", 1)
        if not access_type:
            raise ValueError(_("Must enter an access type!"))
        if access_options:
            if access_type not in access_options:
                    _("Access type must be one of: {alternatives}").format(
                        alternatives=", ".join(access_options)
                    )
                )
        if not lockfunc:
            raise ValueError(_("Lock func not entered."))

"""
English verb conjugation

Original Author: Tom De Smedt <tomdesmedt@organisms.be> of Nodebox
Refactored by Griatch 2021, for Evennia.

This is distributed under the GPL2 license. See ./LICENSE.txt for details.

The verb.txt morphology was adopted from the XTAG morph_englis.flat:
http://www.cis.upenn.edu/~xtag/

"""

import os

_VERBS_FILE = "verbs.txt"

# Each verb and its tenses is a list in verbs.txt,
# indexed according to the following keys:
# the negated forms (for supported verbs) are ind+11.

verb_tenses_keys = {
    "infinitive": 0,
    "1st singular present": 1,
    "2nd singular present": 2,
    "3rd singular present": 3,
    "present plural": 4,
    "present participle": 5,
    "1st singular past": 6,
    "2nd singular past": 7,
    "3rd singular past": 8,
    "past plural": 9,
    "past": 10,
    "past participle": 11,
}

# allow to specify tenses with a shorter notation
verb_tenses_aliases = {
    "inf": "infinitive",
    "1sgpres": "1st singular present",
    "2sgpres": "2nd singular present",
    "3sgpres": "3rd singular present",
    "pl": "present plural",
    "prog": "present participle",
    "1sgpast": "1st singular past",
    "2sgpast": "2nd singular past",
    "3sgpast": "3rd singular past",
    "pastpl": "past plural",
    "ppart": "past participle",
}

# Each verb has morphs for infinitve,
# 3rd singular present, present participle,
# past and past participle.
# Verbs like "be" have other morphs as well
# (i.e. I am, you are, she is, they aren't)
# Additionally, the following verbs can be negated:
# be, can, do, will, must, have, may, need, dare, ought.

# load the conjugation forms from ./verbs.txt
verb_tenses = {}

path = os.path.join(os.path.dirname(__file__), _VERBS_FILE)
with open(path) as fil:
        wordlist = [part.strip() for part in line.split(",")]
        verb_tenses[wordlist[0]] = wordlist

# Each verb can be lemmatised:
# inflected morphs of the verb point
# to its infinitive in this dictionary.
verb_lemmas = {}
for infinitive in verb_tenses:
    for tense in verb_tenses[infinitive]:
        if tense:
            verb_lemmas[tense] = infinitive

def verb_infinitive(verb):
    """
    Returns the uninflected form of the verb, like 'are' -> 'be'

    Args:
        verb (str): The verb to get the uninflected form of.

    Returns:
        str: The uninflected verb form of `verb`.

    """

    return verb_lemmas.get(verb, "")

def verb_conjugate(verb, tense="infinitive", negate=False):
    """
    Inflects the verb to the given tense.

    Args:
        verb (str): The single verb to conjugate.
        tense (str): The tense to convert to. This can be given either as a long or short form
            - "infinitive" ("inf") - be
            - "1st/2nd/3rd singular present" ("1/2/3sgpres") - am/are/is
            - "present plural" ("pl") - are
            - "present participle" ("prog") - being
            - "1st/2nd/3rd singular past" ("1/2/3sgpast") - was/were/was
            - "past plural" ("pastpl") - were
            - "past" - were
            - "past participle" ("ppart") - been
        negate (bool): Negates the verb. This only supported
            for a limited number of verbs: be, can, do, will, must, have, may,
            need, dare, ought.

    Returns:
        str: The conjugated verb. If conjugation fails, the original verb is returned.

    Examples:
        The verb 'be':
        - present: I am, you are, she is,
        - present participle: being,
        - past: I was, you were, he was,
        - past participle: been,
        - negated present: I am not, you aren't, it isn't.

    """
    tense = verb_tenses_aliases.get(tense, tense)
    verb = verb_infinitive(verb)
    ind = verb_tenses_keys[tense]
    if negate:
        ind += len(verb_tenses_keys)
    try:
        return verb_tenses[verb][ind]
    except (IndexError, KeyError):
        # TODO implement simple algorithm here with +s for certain tenses?
        return verb

def verb_present(verb, person="", negate=False):
    """
    Inflects the verb in the present tense.

    Args:
        person (str or int): This can be 1, 2, 3, "1st", "2nd", "3rd", "plural" or "*".
        negate (bool):  Some verbs like be, have, must, can be negated.

    Returns:
        str: The present tense verb.

    Example:
        had -> have

    """

    person = str(person).replace("pl", "*").strip("stndrgural")
    mapping = {
        "1": "1st singular present",
        "2": "2nd singular present",
        "3": "3rd singular present",
        "*": "present plural",
    }
    if person in mapping and verb_conjugate(verb, mapping[person], negate) != "":
        return verb_conjugate(verb, mapping[person], negate)

    return verb_conjugate(verb, "infinitive", negate)

def verb_present_participle(verb):
    """
    Inflects the verb in the present participle.

    Args:
        verb (str): The verb to inflect.

    Returns:
        str: The inflected verb.

    Examples:
        give -> giving, be -> being, swim -> swimming

    """
    return verb_conjugate(verb, "present participle")

def verb_past(verb, person="", negate=False):
    """

    Inflects the verb in the past tense.

    Args:
        person (str, optional): The person can be specified with 1, 2, 3,
            "1st", "2nd", "3rd", "plural", "*".
        negate (bool, optional):  Some verbs like be, have, must, can be negated.

    Returns:

    Examples:
        give -> gave, be -> was, swim -> swam

    """

    mapping = {
        "1": "1st singular past",
        "2": "2nd singular past",
        "3": "3rd singular past",
        "*": "past plural",
    }

    return verb_conjugate(verb, "past", negate=negate)

def verb_past_participle(verb):
    """

    Args:

    Returns:

    Examples:
        give -> given, be -> been, swim -> swum

    """
    return verb_conjugate(verb, "past participle")

def verb_all_tenses():
    """
    Get all all possible verb tenses.

    Returns:
        list: A list if string names.

    """

    return list(verb_tenses_keys.keys())

def verb_tense(verb):
    """
    Returns a string from verb_tenses_keys representing the verb's tense.

    Args:
        verb (str): The verb to check the tense of.

    Returns:
        str: The tense.

    Example:
        given -> "past participle"

    """
    infinitive = verb_infinitive(verb)
    data = verb_tenses.get(infinitive)
    if not data:
        return infinitive
    for tense in verb_tenses_keys:
        if data[verb_tenses_keys[tense]] == verb:
            return tense
        if data[verb_tenses_keys[tense] + len(verb_tenses_keys)] == verb:

def verb_is_tense(verb, tense):
    """
    Checks whether the verb is in the given tense.

    Args:
        verb (str): The verb to check.
        tense (str): The tense to check.

    Return:
        bool: If verb matches given tense.

    """
    return verb_tense(verb) == tense

def verb_is_present(verb, person="", negated=False):
    """
    Checks whether the verb is in the present tense.

    Args:
        person (str): Check which person.
        negated (bool): Check if verb was negated.

    Returns:
        bool: If verb was in present tense.

    """

    person = str(person).replace("*", "plural")
    tense = verb_tense(verb)
    if tense is not None:
        if "present" in tense and person in tense:
            if not negated:
            elif "n't" in verb or " not" in verb:

def verb_is_present_participle(verb):
    """
    Checks whether the verb is in present participle.

    Args:

    Returns:
        bool: Result of check.

    """

    return tense == "present participle"

def verb_is_past(verb, person="", negated=False):
    """
    Checks whether the verb is in the past tense.

    Args:
        person (str): The person to check.
        negated (bool): Check if verb is negated.

    Returns:

    """

        if "past" in tense and person in tense:

def verb_is_past_participle(verb):
    """
    Checks whether the verb is in past participle.

    Args:

    Returns:
        bool: The result of the check.

    """
    return tense == "past participle"

def verb_actor_stance_components(verb):
    """
    Figure out actor stance components of a verb.

    Args:
        verb (str): The verb to analyze

    Returns:
        tuple: The 2nd person (you) and 3rd person forms of the verb,
            in the same tense as the ingoing verb.

    """
    if "participle" in tense or "plural" in tense:
        return (verb, verb)
    if tense == "infinitive" or "present" in tense:
        you_str = verb_present(verb, person="2") or verb
        them_str = verb_present(verb, person="3") or verb + "s"
    else:
        you_str = verb_past(verb, person="2") or verb
        them_str = verb_past(verb, person="3") or verb + "s"
    return (you_str, them_str)

"""
English pronoun mapping between 1st/2nd person and 3rd person perspective (and vice-versa).

This file is released under the Evennia regular BSD License.
(Griatch 2021) - revised by InspectorCaracal 2022

Pronouns are words you use instead of a proper name, such as 'him', 'herself', 'theirs' etc. These
look different depending on who sees the outgoing string. This mapping maps between 1st/2nd case and
the 3rd person case and back. In some cases, the mapping is not unique; it is assumed the system can
differentiate between the options in some other way.

====================  =======  ========  ==========  ==========  ===========
viewpoint/pronouns    Subject  Object    Possessive  Possessive  Reflexive
                      Pronoun  Pronoun   Adjective   Pronoun     Pronoun
1st person              I        me        my         mine       myself
1st person plural       we       us        our        ours       ourselves
2nd person              you      you       your       yours      yourself
2nd person plural       you      you       your       yours      yourselves

3rd person male         he       him       his        his        himself
3rd person female       she      her       her        hers       herself
3rd person neutral      it       it        its        its        itself
3rd person plural       they     them      their      theirs     themselves
"""
from evennia.utils.utils import copy_word_case, is_iter

DEFAULT_PRONOUN_TYPE = "subject pronoun"
DEFAULT_VIEWPOINT = "2nd person"
DEFAULT_GENDER = "neutral"

PRONOUN_TYPES = [
    "subject pronoun",
    "object pronoun",
    "possessive adjective",
    "possessive pronoun",
    "reflexive pronoun",
]
VIEWPOINTS = ["1st person", "2nd person", "3rd person"]
GENDERS = ["male", "female", "neutral", "plural"]

PRONOUN_MAPPING = {
    "1st person": {
        "subject pronoun": {
            "neutral": "I",
            "plural": "we",
        },
        "object pronoun": {
            "neutral": "me",
            "plural": "us",
        },
        "possessive adjective": {
            "neutral": "my",
            "plural": "our",
        },
        "possessive pronoun": {
            "neutral": "mine",
            "plural": "ours",
        },
        "reflexive pronoun": {"neutral": "myself", "plural": "ourselves"},
    },
    "2nd person": {
            "neutral": "you",
        },
        },
            "neutral": "your",
        },
            "neutral": "yours",
        },
        "reflexive pronoun": {
            "neutral": "yourself",
            "plural": "yourselves",
        },
    },
    "3rd person": {
        "subject pronoun": {"male": "he", "female": "she", "neutral": "it", "plural": "they"},
        "object pronoun": {"male": "him", "female": "her", "neutral": "it", "plural": "them"},
            "male": "his",
            "female": "her",
            "neutral": "its",
            "plural": "their",
        },
            "female": "hers",
            "plural": "theirs",
        },
            "male": "himself",
            "female": "herself",
            "neutral": "itself",
            "plural": "themselves",
        },
    },
}

PRONOUN_TABLE = {
    "I": ("1st person", ("neutral", "male", "female", "plural"), "subject pronoun"),
    "me": ("1st person", ("neutral", "male", "female", "plural"), "object pronoun"),
    "my": ("1st person", ("neutral", "male", "female", "plural"), "possessive adjective"),
    "mine": ("1st person", ("neutral", "male", "female", "plural"), "possessive pronoun"),
    "myself": ("1st person", ("neutral", "male", "female", "plural"), "reflexive pronoun"),
    "we": ("1st person", "plural", "subject pronoun"),
    "us": ("1st person", "plural", "object pronoun"),
    "our": ("1st person", "plural", "possessive adjective"),
    "ours": ("1st person", "plural", "possessive pronoun"),
    "ourselves": ("1st person", "plural", "reflexive pronoun"),
    "you": (
        "2nd person",
        ("neutral", "male", "female", "plural"),
        ("subject pronoun", "object pronoun"),
    ),
    "your": ("2nd person", ("neutral", "male", "female", "plural"), "possessive adjective"),
    "yours": ("2nd person", ("neutral", "male", "female", "plural"), "possessive pronoun"),
    "yourself": ("2nd person", ("neutral", "male", "female"), "reflexive pronoun"),
    "yourselves": ("2nd person", "plural", "reflexive pronoun"),
    "he": ("3rd person", "male", "subject pronoun"),
    "him": ("3rd person", "male", "object pronoun"),
    "his": (
        "3rd person",
        "male",
        ("possessive pronoun", "possessive adjective"),
    ),
    "himself": ("3rd person", "male", "reflexive pronoun"),
    "she": ("3rd person", "female", "subject pronoun"),
    "her": (
        "female",
        ("object pronoun", "possessive adjective"),
    ),
    "hers": ("3rd person", "female", "possessive pronoun"),
    "herself": ("3rd person", "female", "reflexive pronoun"),
    "it": (
        "neutral",
    ),
    "its": (
    ),
    "itself": ("3rd person", "neutral", "reflexive pronoun"),
    "they": ("3rd person", "plural", "subject pronoun"),
    "them": ("3rd person", "plural", "object pronoun"),
    "their": ("3rd person", "plural", "possessive adjective"),
    "theirs": ("3rd person", "plural", "possessive pronoun"),
    "themselves": ("3rd person", "plural", "reflexive pronoun"),
}

# define the default viewpoint conversions
VIEWPOINT_CONVERSION = {
    "1st person": "3rd person",
    "2nd person": "3rd person",
    "3rd person": ("2nd person", "1st person"),
}

ALIASES = {
    "m": "male",
    "f": "female",
    "n": "neutral",
    "p": "plural",
    "1st": "1st person",
    "2nd": "2nd person",
    "3rd": "3rd person",
    "1": "1st person",
    "2": "2nd person",
    "3": "3rd person",
    "s": "subject pronoun",
    "sp": "subject pronoun",
    "subject": "subject pronoun",
    "op": "object pronoun",
    "object": "object pronoun",
    "pa": "possessive adjective",
    "pp": "possessive pronoun",
}

def pronoun_to_viewpoints(pronoun, options=None, pronoun_type=None, gender=None, viewpoint=None):
    """
    Access function for determining the forms of a pronoun from different viewpoints.

    Args:
        pronoun (str): A valid English pronoun, such as 'you', 'his', 'themselves' etc.
        options (str or list, optional): A list or space-separated string of options to help
            the engine when there is no unique mapping to use. This could for example
            be "2nd female" (alias 'f') or "possessive adjective" (alias 'pa' or 'a').
        pronoun_type (str, optional): An explicit object pronoun to separate cases where
            there is no unique mapping. Pronoun types defined in `options` take precedence.
            Values are

            - `possessive pronoun`/`pronoun`/`pp` (mine, yours, his, theirs)

        gender (str, optional): Specific gender to use (plural counts a gender for this purpose).
            A gender specified in `options` takes precedence. Values and aliases are:

        viewpoint (str, optional): A specified viewpoint of the one talking, to use
            when there is no unique mapping. A viewpoint given in `options` take
            precedence. Values and aliases are:

    Returns:
        tuple: A tuple `(1st/2nd_person_pronoun, 3rd_person_pronoun)` to show to the one sending the
        string and others respectively. If pronoun is invalid, the word is returned verbatim.

    Note:
        The capitalization of the original word will be retained.

    """
    if not pronoun:
        return pronoun

    pronoun_lower = "I" if pronoun == "I" else pronoun.lower()

    if pronoun_lower not in PRONOUN_TABLE:

    # get the default data for the input pronoun
    source_viewpoint, source_gender, source_type = PRONOUN_TABLE[pronoun_lower]

    # use the source pronoun's attributes as defaults
    if pronoun_type not in PRONOUN_TYPES:
        pronoun_type = source_type[0] if is_iter(source_type) else source_type
    if viewpoint not in VIEWPOINTS:
        viewpoint = source_viewpoint
    if gender not in GENDERS:
        gender = source_gender[0] if is_iter(source_gender) else source_gender

        # option string/list will override the kwargs differentiators given
        if isinstance(options, str):
            options = options.split()
        options = [str(part).strip().lower() for part in options]
        options = [ALIASES.get(opt, opt) for opt in options]

        for opt in options:
            if opt in PRONOUN_TYPES:
                pronoun_type = opt
            elif opt in VIEWPOINTS:
                viewpoint = opt
            elif opt in GENDERS:
                gender = opt

    # check if pronoun maps to multiple options and differentiate
    # but don't allow invalid differentiators
    if is_iter(source_type):
        pronoun_type = pronoun_type if pronoun_type in source_type else source_type[0]
    else:
        pronoun_type = source_type
    target_viewpoint = VIEWPOINT_CONVERSION[source_viewpoint]
    if is_iter(target_viewpoint):
        viewpoint = viewpoint if viewpoint in target_viewpoint else target_viewpoint[0]
    else:
        viewpoint = target_viewpoint

    # by this point, gender will be a valid option from GENDERS and type/viewpoint will be validated
    # step down into the mapping to get the converted pronoun
    viewpoint_map = PRONOUN_MAPPING[viewpoint]
    pronouns = viewpoint_map.get(pronoun_type, viewpoint_map[DEFAULT_PRONOUN_TYPE])
    mapped_pronoun = pronouns.get(gender, pronouns[DEFAULT_GENDER])

    # keep the same capitalization as the original
    if pronoun != "I":
        # don't remap I, since this is always capitalized.
        mapped_pronoun = copy_word_case(pronoun, mapped_pronoun)
    if mapped_pronoun == "i":
        mapped_pronoun = mapped_pronoun.upper()

    if viewpoint == "3rd person":
        # the desired viewpoint is 3rd person, meaning the incoming viewpoint
        # must have been 1st or 2nd person.
        return pronoun, mapped_pronoun
    else:
        # the desired viewpoint is 1st or 2nd person, so incoming must have been
        # in 3rd person form.
        return mapped_pronoun, pronoun

"""
Unit tests for verb conjugation.

"""

from . import conjugate, pronouns

class TestVerbConjugate(TestCase):
    """
    Test the conjugation.

    """

        [
            ("have", "have"),
            ("swim", "swim"),
            ("give", "give"),
            ("given", "give"),
            ("am", "be"),
            ("doing", "do"),
            ("are", "be"),
        ]
    )
    def test_verb_infinitive(self, verb, expected):
        """
        Test the infinite-getter.
        """
        self.assertEqual(expected, conjugate.verb_infinitive(verb))

        [
            ("inf", "have", "have"),
            ("inf", "swim", "swim"),
            ("inf", "give", "give"),
            ("inf", "given", "give"),
            ("inf", "am", "be"),
            ("inf", "doing", "do"),
            ("inf", "are", "be"),
            ("2sgpres", "am", "are"),
            ("3sgpres", "am", "is"),
        ]
    )
    def test_verb_conjugate(self, tense, verb, expected):
        """
        Test conjugation for different tenses.

        """
        self.assertEqual(expected, conjugate.verb_conjugate(verb, tense=tense))

        [
            ("1st", "have", "have"),
            ("1st", "swim", "swim"),
            ("1st", "give", "give"),
            ("1st", "given", "give"),
            ("1st", "am", "am"),
            ("1st", "doing", "do"),
            ("1st", "are", "am"),
            ("2nd", "were", "are"),
            ("3rd", "am", "is"),
        ]
    )
    def test_verb_present(self, person, verb, expected):
        """
        Test the present.

        """
        self.assertEqual(expected, conjugate.verb_present(verb, person=person))

        [
            ("have", "having"),
            ("swim", "swimming"),
            ("give", "giving"),
            ("given", "giving"),
            ("am", "being"),
            ("doing", "doing"),
            ("are", "being"),
        ]
    )
    def test_verb_present_participle(self, verb, expected):
        """
        Test the present_participle

        """
        self.assertEqual(expected, conjugate.verb_present_participle(verb))

        [
            ("1st", "have", "had"),
            ("1st", "swim", "swam"),
            ("1st", "give", "gave"),
            ("1st", "given", "gave"),
            ("1st", "am", "was"),
            ("1st", "doing", "did"),
            ("1st", "are", "was"),
            ("2nd", "were", "were"),
        ]
    )
    def test_verb_past(self, person, verb, expected):
        """
        Test the past getter.

        """
        self.assertEqual(expected, conjugate.verb_past(verb, person=person))

        [
            ("have", "had"),
            ("swim", "swum"),
            ("give", "given"),
            ("given", "given"),
            ("am", "been"),
            ("doing", "done"),
            ("are", "been"),
        ]
    )
    def test_verb_past_participle(self, verb, expected):
        """
        Test the past participle.

        """
        self.assertEqual(expected, conjugate.verb_past_participle(verb))

    def test_verb_get_all_tenses(self):
        """
        Test getting all tenses.

        """
        self.assertEqual(list(conjugate.verb_tenses_keys.keys()), conjugate.verb_all_tenses())

        [
            ("have", "infinitive"),
            ("swim", "infinitive"),
            ("give", "infinitive"),
            ("given", "past participle"),
            ("am", "1st singular present"),
            ("doing", "present participle"),
            ("are", "2nd singular present"),
        ]
    )
    def test_verb_tense(self, verb, expected):
        """
        Test the tense retriever.

        """
        self.assertEqual(expected, conjugate.verb_tense(verb))

        [
            ("inf", "have", True),
            ("inf", "swim", True),
            ("inf", "give", True),
            ("inf", "given", False),
            ("inf", "am", False),
            ("inf", "doing", False),
            ("inf", "are", False),
        ]
    )
    def test_verb_is_tense(self, tense, verb, expected):
        """
        Test the tense-checker

        """
        self.assertEqual(expected, conjugate.verb_is_tense(verb, tense))

        [
            ("1st", "have", False),
            ("1st", "swim", False),
            ("1st", "give", False),
            ("1st", "given", False),
            ("1st", "am", True),
            ("1st", "doing", False),
            ("1st", "are", False),
            ("1st", "had", False),
        ]
    )
    def test_verb_is_present(self, person, verb, expected):
        """

        """
        self.assertEqual(expected, conjugate.verb_is_present(verb, person=person))

        [
            ("have", False),
            ("swim", False),
            ("give", False),
            ("given", False),
            ("am", False),
            ("doing", True),
            ("are", False),
        ]
    )
    def test_verb_is_present_participle(self, verb, expected):
        """

        """
        self.assertEqual(expected, conjugate.verb_is_present_participle(verb))

        [
            ("1st", "am", False),
            ("2nd", "were", True),
        ]
    )
    def test_verb_is_past(self, person, verb, expected):
        """

        """
        self.assertEqual(expected, conjugate.verb_is_past(verb, person=person))

        [
            ("swimming", False),
            ("given", True),
            ("doing", False),
            ("had", False),
        ]
    )
    def test_verb_is_past_participle(self, verb, expected):
        """

        """
        self.assertEqual(expected, conjugate.verb_is_past_participle(verb))

        [
            ("have", ("have", "has")),
            ("swimming", ("swimming", "swimming")),
            ("give", ("give", "gives")),
            ("given", ("given", "given")),
            ("am", ("are", "is")),
            ("doing", ("doing", "doing")),
            ("are", ("are", "is")),
            ("had", ("had", "had")),
            ("grin", ("grin", "grins")),
            ("smile", ("smile", "smiles")),
            ("vex", ("vex", "vexes")),
            ("thrust", ("thrust", "thrusts")),
        ]
    )
    def test_verb_actor_stance_components(self, verb, expected):
        """

        """
        self.assertEqual(expected, conjugate.verb_actor_stance_components(verb))

class TestPronounMapping(TestCase):
    """
    Test pronoun viewpoint mapping
    """

        [
            ("you", "you", "it"),  # default 3rd is "neutral"
            ("I", "I", "it"),
            ("Me", "Me", "It"),
            ("ours", "ours", "theirs"),
            ("yourself", "yourself", "itself"),
            ("yourselves", "yourselves", "themselves"),
            ("he", "you", "he"),  # assume 2nd person
            ("her", "you", "her"),
            ("their", "your", "their"),
            ("itself", "yourself", "itself"),
            ("herself", "yourself", "herself"),
            ("themselves", "yourselves", "themselves"),
        ]
    )
    def test_default_mapping(self, pronoun, expected_1st_or_2nd_person, expected_3rd_person):
        """
        Test the pronoun mapper.

        """
        received_1st_or_2nd_person, received_3rd_person = pronouns.pronoun_to_viewpoints(pronoun)

        self.assertEqual(expected_1st_or_2nd_person, received_1st_or_2nd_person)
        self.assertEqual(expected_3rd_person, received_3rd_person)

        [
            ("you", "m", "you", "he"),
            ("you", "f op", "you", "her"),
            ("you", "p op", "you", "them"),
            ("I", "m", "I", "he"),
            ("Me", "n", "Me", "It"),
            ("your", "p", "your", "their"),
            ("yourself", "m", "yourself", "himself"),
            ("yourself", "f", "yourself", "herself"),
            ("yourselves", "", "yourselves", "themselves"),
            ("he", "1", "I", "he"),
            ("he", "1 p", "we", "he"),  # royal we
            ("we", "m", "we", "he"),  # royal we, other way
            ("her", "p", "you", "her"),
            ("her", "pa", "your", "her"),
            ("their", "ma", "your", "their"),
        ]
    )
    def test_mapping_with_options(
        self, pronoun, options, expected_1st_or_2nd_person, expected_3rd_person
    ):
        """

        """
        received_1st_or_2nd_person, received_3rd_person = pronouns.pronoun_to_viewpoints(
            pronoun, options
        )

        [
            ("you", "p", "you", "they"),
            ("I", "p", "I", "they"),
            ("Me", "p", "Me", "Them"),
            ("they", "1 p", "we", "they"),
            ("they", "", "you", "they"),
            ("yourself", "p", "yourself", "themselves"),
            ("myself", "p", "myself", "themselves"),
        ]
    )
    def test_colloquial_plurals(
    ):
        """
        The use of this module by the funcparser expects a default person-pronoun
        of the neutral "they", which is categorized here by the plural.

        """
        )

#
# This sets up how models are displayed
# in the web admin interface.
#
from django import forms
from django.contrib import admin, messages
from django.contrib.admin.options import IS_POPUP_VAR
from django.contrib.admin.utils import unquote
from django.contrib.admin.widgets import FilteredSelectMultiple, ForeignKeyRawIdWidget
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import path, reverse
from django.utils.decorators import method_decorator
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.views.decorators.debug import sensitive_post_parameters

from . import utils as adminutils
from .attributes import AttributeInline
from .tags import TagInline

sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())

# handle the custom User editor
class AccountChangeForm(UserChangeForm):
    """
    Modify the accountdb class.

    """

        model = AccountDB
        fields = "__all__"

    username = forms.RegexField(
        label="Username",
        max_length=30,
        regex=r"^[\w. @+-]+$",
        widget=forms.TextInput(attrs={"size": "30"}),
        error_messages={
            "invalid": "This value may contain only letters, spaces, numbers "
            "and @/./+/-/_ characters."
        },
        help_text="30 characters or fewer. Letters, spaces, digits and " "@/./+/-/_ only.",
    )

    db_typeclass_path = forms.ChoiceField(
        label="Typeclass",
        help_text="This is the Python-path to the class implementing the actual account functionality. "
        "You usually don't need to change this from the default.<BR>"
        "If your custom class is not found here, it may not be imported into Evennia yet.",
        choices=lambda: adminutils.get_and_load_typeclasses(parent=AccountDB),
    )

    db_lock_storage = forms.CharField(
        label="Locks",
        required=False,
        widget=forms.Textarea(attrs={"cols": "100", "rows": "2"}),
        help_text="Locks limit access to the entity. Written on form `type:lockdef;type:lockdef..."
        "<BR>(Permissions (used with the perm() lockfunc) are Tags with the 'permission' type)",
    )

    db_cmdset_storage = forms.CharField(
        label="CommandSet",
        initial=settings.CMDSET_ACCOUNT,
        widget=forms.TextInput(attrs={"size": "78"}),
    )

    is_superuser = forms.BooleanField(
        label="Superuser status",
        help_text="Superusers bypass all in-game locks and has all "
        "permissions without explicitly assigning them. Usually "
        "only one superuser (user #1) is needed and only a superuser "
        "can create another superuser.<BR>"
        "Only Superusers can change the user/group permissions below.",
    )

    def clean_username(self):
        """
        Clean the username and check its existence.

        """
        username = self.cleaned_data["username"]
        if username.upper() == self.instance.username.upper():
            return username
        elif AccountDB.objects.filter(username__iexact=username):
            raise forms.ValidationError("An account with that name " "already exists.")
        return self.cleaned_data["username"]

        """
        Tweak some fields dynamically.

        """

        # better help text for cmdset_storage
        account_cmdset = settings.CMDSET_ACCOUNT
        self.fields["db_cmdset_storage"].help_text = (
            "Path to Command-set path. Most non-character objects don't need a cmdset"
            " and can leave this field blank. Default cmdset-path<BR> for Accounts "
            f"is <strong>{account_cmdset}</strong> ."
        )

class AccountCreationForm(UserCreationForm):
    """
    Create a new AccountDB instance.
    """

        },
    )

        """
        Cleanup username.
        """
        if AccountDB.objects.filter(username__iexact=username):
            raise forms.ValidationError("An account with that name already " "exists.")

class AccountTagInline(TagInline):
    """
    Inline Account Tags.

    """

    model = AccountDB.db_tags.through
    related_field = "accountdb"

class AccountAttributeInline(AttributeInline):
    """
    Inline Account Attributes.

    """

    model = AccountDB.db_attributes.through

class ObjectPuppetInline(admin.StackedInline):
    """
    Inline creation of puppet-Object in Account.

    """

    from .objects import ObjectCreateForm

    verbose_name = "Puppeted Object"
    model = ObjectDB
    view_on_site = False
    show_change_link = True
    # template = "admin/accounts/stacked.html"
    form = ObjectCreateForm
    fieldsets = (
        (
            None,
            {
                "fields": (
                    ("db_key", "db_typeclass_path"),
                    ("db_location", "db_home", "db_destination"),
                    "db_cmdset_storage",
                    "db_lock_storage",
                ),
                "description": "Object currently puppeted by the account (note that this "
                "will go away if account logs out or unpuppets)",
            },
        ),
    )

    extra = 0
    readonly_fields = (
        "db_key",
        "db_typeclass_path",
        "db_destination",
        "db_location",
        "db_home",
        "db_account",
        "db_cmdset_storage",
        "db_lock_storage",
    )

    # disable adding/deleting this inline - read-only!
    def has_add_permission(self, request, obj=None):

    def has_delete_permission(self, request, obj=None):

@admin.register(AccountDB)
class AccountAdmin(BaseUserAdmin):
    """
    This is the main creation screen for Users/accounts

    """

    list_display = (
        "id",
        "username",
        "is_staff",
        "is_superuser",
        "db_date_created",
    )
    list_display_links = ("id", "username")
    form = AccountChangeForm
    add_form = AccountCreationForm
    search_fields = ["=id", "^username", "db_typeclass_path"]
    ordering = ["-db_date_created", "id"]
    list_filter = ["is_superuser", "is_staff", "db_typeclass_path"]
    inlines = [AccountTagInline, AccountAttributeInline]
    readonly_fields = ["db_date_created", "serialized_string", "puppeted_objects"]
        (
            None,
            {
                    ("username", "db_typeclass_path"),
                    "password",
                    "email",
                    "db_date_created",
                    "puppeted_objects",
                    "serialized_string",
                )
            },
        ),
        (
            "Admin/Website properties",
            {
                    ("first_name", "last_name"),
                    "last_login",
                    "date_joined",
                    "is_active",
                    "is_staff",
                    "is_superuser",
                    "user_permissions",
                    "groups",
                ),
                "description": "<i>Used by the website/Django admin. "
                "Except for `superuser status`, the permissions are not used in-game.</i>",
            },
        ),
    )

    add_fieldsets = (
        (
            None,
            {
                "fields": ("username", "password1", "password2", "email"),
                "description": "<i>These account details are shared by the admin "
                "system and the game.</i>",
            },
        ),
    )

    def serialized_string(self, obj):
        """
        Get the serialized version of the object.

        """
        from evennia.utils import dbserialize

        return str(dbserialize.pack_dbobj(obj))

    serialized_string.help_text = (
        "Copy & paste this string into an Attribute's `value` field to store this account there."
    )

    def puppeted_objects(self, obj):
        """
        Get any currently puppeted objects (read only list)

        """
        return mark_safe(
            ", ".join(
                '<a href="{url}">{name}</a>'.format(
                    url=reverse("admin:objects_objectdb_change", args=[obj.id]), name=obj.db_key
                )
                for obj in ObjectDB.objects.filter(db_account=obj)
            )
        )

    puppeted_objects.help_text = (
        "Objects currently puppeted by this Account. "
        "Link new ones from the `Objects` admin page.<BR>"
        "Note that these will disappear when a user unpuppets or goes offline - "
        "this is normal."
    )

    def get_form(self, request, obj=None, **kwargs):
        """
        Overrides help texts.

        """
        help_texts = kwargs.get("help_texts", {})
        help_texts["serialized_string"] = self.serialized_string.help_text
        help_texts["puppeted_objects"] = self.puppeted_objects.help_text
        kwargs["help_texts"] = help_texts

        # security disabling for non-superusers
        form = super().get_form(request, obj, **kwargs)
        disabled_fields = set()
        if not request.user.is_superuser:
            disabled_fields |= {"is_superuser", "user_permissions", "user_groups"}
        for field_name in disabled_fields:
            if field_name in form.base_fields:
                form.base_fields[field_name].disabled = True

    @sensitive_post_parameters_m
    def user_change_password(self, request, id, form_url=""):
        user = self.get_object(request, unquote(id))
        if not self.has_change_permission(request, user):
            raise PermissionDenied
        if user is None:
            raise Http404("%(name)s object with primary key %(key)r does not exist.") % {
                "name": self.model._meta.verbose_name,
                "key": escape(id),
            }
        if request.method == "POST":
            form = self.change_password_form(user, request.POST)
            if form.is_valid():
                form.save()
                change_message = self.construct_change_message(request, form, None)
                self.log_change(request, user, change_message)
                msg = "Password changed successfully."
                messages.success(request, msg)
                update_session_auth_hash(request, form.user)
                return HttpResponseRedirect(
                    reverse(
                        "%s:%s_%s_change"
                        % (
                            self.admin_site.name,
                            user._meta.app_label,
                            # the model_name is something we need to hardcode
                            # since our accountdb is a proxy:
                            "accountdb",
                        ),
                        args=(user.pk,),
                    )
                )
        else:
            form = self.change_password_form(user)

        fieldsets = [(None, {"fields": list(form.base_fields)})]
        adminForm = admin.helpers.AdminForm(form, fieldsets, {})

        context = {
            "title": "Change password: %s" % escape(user.get_username()),
            "adminForm": adminForm,
            "form_url": form_url,
            "form": form,
            "is_popup": (IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET),
            "add": True,
            "change": False,
            "has_delete_permission": False,
            "has_change_permission": True,
            "has_absolute_url": False,
            "opts": self.model._meta,
            "original": user,
            "save_as": False,
            "show_save": True,
            **self.admin_site.each_context(request),
        }

        request.current_app = self.admin_site.name

        return TemplateResponse(
            request,
            self.change_user_password_template or "admin/auth/user/change_password.html",
            context,
        )

    def save_model(self, request, obj, form, change):
        """
        Custom save actions.

        Args:
            obj (Object): Object to save.
            form (Form): Related form instance.
            change (bool): False if this is a new save and not an update.

        """
        if not change:
            # calling hooks for new account
            obj.set_class_from_typeclass(typeclass_path=settings.BASE_ACCOUNT_TYPECLASS)
            obj.basetype_setup()
            obj.at_account_creation()

    def response_add(self, request, obj, post_url_continue=None):
        from django.http import HttpResponseRedirect
        from django.urls import reverse

        return HttpResponseRedirect(reverse("admin:accounts_accountdb_change", args=[obj.id]))

#
#
from django.contrib.admin.utils import flatten_fieldsets
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
from django.http import HttpResponseRedirect
from django.utils.html import format_html

class ObjectAttributeInline(AttributeInline):
    """
    Defines inline descriptions of Attributes (experimental)

    """

    model = ObjectDB.db_attributes.through
    related_field = "objectdb"

class ObjectTagInline(TagInline):
    """
    Defines inline descriptions of Tags (experimental)

    """

    model = ObjectDB.db_tags.through

class ObjectCreateForm(forms.ModelForm):
    """
    This form details the look of the fields.

    """

        model = ObjectDB

    db_key = forms.CharField(
        label="Name/Key",
        help_text="Main identifier, like 'apple', 'strong guy', 'Elizabeth' etc. "
        "If creating a Character, check so the name is unique among characters!",
    )
        initial={settings.BASE_OBJECT_TYPECLASS: settings.BASE_OBJECT_TYPECLASS},
        help_text="This is the Python-path to the class implementing the actual functionality. "
        f"<BR>If you are creating a Character you usually need <B>{settings.BASE_CHARACTER_TYPECLASS}</B> "
        "or a subclass of that. <BR>If your custom class is not found in the list, it may not be imported "
        "into Evennia yet.",
        choices=lambda: adminutils.get_and_load_typeclasses(parent=ObjectDB),
    )

        help_text="In-game lock definition string. If not given, defaults will be used. "
        "This string should be on the form "
        "<i>type:lockfunction(args);type2:lockfunction2(args);...",
    )
        label="CmdSet",
        initial="",
    )

    # This is not working well because it will not properly allow an empty choice, and will
    # also not work well for comma-separated storage without more work. Notably, it's also
    # a bit hard to visualize.
    # db_cmdset_storage = forms.MultipleChoiceField(
    #     label="CmdSet",
    #     required=False,
    #     choices=adminutils.get_and_load_typeclasses(parent=ObjectDB))

    db_location = forms.ModelChoiceField(
        ObjectDB.objects.all(),
        label="Location",
        widget=ForeignKeyRawIdWidget(
            ObjectDB._meta.get_field("db_location").remote_field, admin.site
        ),
        help_text="The (current) in-game location.<BR>"
        "Usually a Room but can be<BR>"
        "empty for un-puppeted Characters.",
    )
    db_home = forms.ModelChoiceField(
        label="Home",
        ),
        help_text="Fallback in-game location.<BR>"
        "All objects should usually have<BR>"
        "a home location.",
    )
    db_destination = forms.ModelChoiceField(
        label="Destination",
            ObjectDB._meta.get_field("db_destination").remote_field, admin.site
        ),
        help_text="Only used by Exits.",
    )

        """

        """

        # set default home
        home_id = str(settings.DEFAULT_HOME)
        home_id = home_id[1:] if home_id.startswith("#") else home_id
        default_home = ObjectDB.objects.filter(id=home_id)
        if default_home:
            default_home = default_home[0]
        self.fields["db_home"].initial = default_home
        self.fields["db_location"].initial = default_home

        char_cmdset = settings.CMDSET_CHARACTER
            " and can leave this field blank. Default cmdset-path<BR> for Characters "
            f"is <strong>{char_cmdset}</strong> ."
        )

class ObjectEditForm(ObjectCreateForm):
    """
    Form used for editing. Extends the create one with more fields

    """

    db_account = forms.ModelChoiceField(
        AccountDB.objects.all(),
        label="Puppeting Account",
            ObjectDB._meta.get_field("db_account").remote_field, admin.site
        ),
        help_text="An Account puppeting this Object (if any).<BR>Note that when a user logs "
        "off/unpuppets, this<BR>field will be empty again. This is normal.",
    )

@admin.register(ObjectDB)
class ObjectAdmin(admin.ModelAdmin):
    """
    Describes the admin page for Objects.

    """

    inlines = [ObjectTagInline, ObjectAttributeInline]
        "id",
        "db_key",
    )
    list_display_links = ("id", "db_key")
    ordering = ["-db_date_created", "-id"]
    search_fields = [
        "=id",
        "^db_key",
        "^db_account__db_key",
        "^db_location__db_key",
    ]
    raw_id_fields = ("db_destination", "db_location", "db_home", "db_account")
    readonly_fields = ("serialized_string", "link_button")

    save_as = True
    save_on_top = True
    list_select_related = True
    list_filter = ("db_typeclass_path",)

    # editing fields setup

    form = ObjectEditForm
        (
            None,
            {
                    ("db_account", "link_button"),
                )
            },
        ),
    )

    add_form = ObjectCreateForm
        (
            None,
            {
                )
            },
        ),
    )

        """

        """

        "Copy & paste this string into an Attribute's `value` field to store this object there."
    )

    def get_fieldsets(self, request, obj=None):
        """
        Return fieldsets.

        Args:
            obj (Object, optional): Database object.
        """
        if not obj:
            return self.add_fieldsets
        return super().get_fieldsets(request, obj)

        """
        Use special form during creation.

        Args:

        """

        defaults = {}
            defaults.update(
                {"form": self.add_form, "fields": flatten_fieldsets(self.add_fieldsets)}
            )
        defaults.update(kwargs)
        return super().get_form(request, obj, **defaults)

    def get_urls(self):
        urls = super().get_urls()
        custom_urls = [
            path(
                "account-object-link/<int:pk>",
                self.admin_site.admin_view(self.link_object_to_account),
                name="object-account-link",
            )
        ]
        return custom_urls + urls

    def link_button(self, obj):
        return format_html(
            '<a class="button" href="{}">Link to Account</a>&nbsp;',
            reverse("admin:object-account-link", args=[obj.pk]),
        )

    link_button.short_description = "Create attrs/locks for puppeting"
    link_button.allow_tags = True

    def link_object_to_account(self, request, object_id):
        """
        Link object and account when pressing the button.

        This will:

        - Set account.db._last_puppet to this object
        - Add object to account.characters
        - Change object locks to allow puppeting by account

        """
        obj = self.get_object(request, object_id)
        account = obj.db_account

            account.db._last_puppet = obj
            account.characters.add(obj)
            if not obj.access(account, "puppet"):
                lock = obj.locks.get("puppet")
                lock += f" or pid({account.id})"
                obj.locks.add(lock)
            self.message_user(
                request,
                "Did the following (where possible): "
                f"Set Account.db._last_puppet = {obj}, "
                f"Added {obj} to Account.characters list, "
                f"Added 'puppet:pid({account.id})' lock to {obj}.",
            )
        else:
                request,
                "Account must be connected for this action "
                "(set Puppeting Account and save this page first).",
                level=messages.ERROR,
            )

        # stay on the same page
        return HttpResponseRedirect(reverse("admin:objects_objectdb_change", args=[obj.pk]))

        """
        Model-save hook.

        Args:
            obj (Object): Database object.
            form (Form): Form instance.
            change (bool): If this is a change or a new object.

        """
            # adding a new object
            # have to call init with typeclass passed to it
            obj.set_class_from_typeclass(typeclass_path=obj.db_typeclass_path)
            obj.basetype_posthook_setup()
            obj.at_object_creation()
        else:
            obj.at_init()

        return HttpResponseRedirect(reverse("admin:objects_objectdb_change", args=[obj.id]))

"""
Attribute admin.

Note that we don't present a separate admin for these, since they are only
relevant together with a specific object.

"""

from django.contrib import admin

from evennia.utils.dbserialize import _SaverSet, from_pickle
from evennia.utils.picklefield import PickledFormField

class AttributeForm(forms.ModelForm):
    """
    This form overrides the base behavior of the ModelForm that would be used for a Attribute-through-model.
    Since the through-models only have access to the foreignkeys of the Attribute and the Object that they're
    attached to, we need to spoof the behavior of it being a form that would correspond to its Attribute,
    or the creation of an Attribute. Instead of being saved, we'll call to the Object's handler, which will handle
    the creation, change, or deletion of an Attribute for us, as well as updating the handler's cache so that all
    changes are instantly updated in-game.
    """

    attr_key = forms.CharField(
        label="Attribute Name",
        help_text="The main identifier of the Attribute. For Nicks, this is the pattern-matching string.",
    )
    attr_category = forms.CharField(
        label="Category",
        help_text="Categorization. Unset (default) gives a category of `None`, which is "
        "is what is searched with e.g. `obj.db.attrname`. For 'nick'-type attributes, this is usually "
        "'inputline' or 'channel'.",
    )
    attr_value = PickledFormField(
        label="Value",
        help_text="Value to pickle/save. Db-objects are serialized as a list "
        "containing `__packed_dbobj__` (they can't easily be added from here). Nicks "
        "store their pattern-replacement here.",
    )
    attr_type = forms.ChoiceField(
        label="Type",
        choices=[(None, "-"), ("nick", "nick")],
        help_text="Unset for regular Attributes, 'nick' for Nick-replacement usage.",
    )
    attr_lockstring = forms.CharField(
        help_text="Lock string on the form locktype:lockdef;lockfunc:lockdef;...",
        widget=forms.Textarea(attrs={"rows": 1, "cols": 8}),
    )

        fields = ("attr_key", "attr_value", "attr_category", "attr_lockstring", "attr_type")

        """
        If we have an Attribute, then we'll prepopulate our instance with the fields we'd expect it
        to have based on the Attribute. attr_key, attr_category, attr_value, attr_type,
        and attr_lockstring all refer to the corresponding Attribute fields. The initial data of the form fields will
        similarly be populated.

        """
        attr_key = None
        attr_category = None
        attr_value = None
        attr_type = None
        attr_lockstring = None
        if hasattr(self.instance, "attribute"):
            attr_key = self.instance.attribute.db_key
            attr_category = self.instance.attribute.db_category
            attr_value = self.instance.attribute.db_value
            attr_type = self.instance.attribute.db_attrtype
            attr_lockstring = self.instance.attribute.db_lock_storage
            self.fields["attr_key"].initial = attr_key
            self.fields["attr_category"].initial = attr_category
            self.fields["attr_type"].initial = attr_type
            self.fields["attr_value"].initial = attr_value
            self.fields["attr_lockstring"].initial = attr_lockstring
        self.instance.attr_key = attr_key
        self.instance.attr_category = attr_category
        self.instance.attr_value = attr_value

        # prevent from being transformed to str
        if isinstance(attr_value, (set, _SaverSet)):
            self.fields["attr_value"].disabled = True

        self.instance.deserialized_value = from_pickle(attr_value)
        self.instance.attr_type = attr_type
        self.instance.attr_lockstring = attr_lockstring

    def save(self, commit=True):
        """
        One thing we want to do here is the or None checks, because forms are saved with an empty
        string rather than null from forms, usually, and the Handlers may handle empty strings
        differently than None objects. So for consistency with how things are handled in game,
        we'll try to make sure that empty form fields will be None, rather than ''.
        """
        # we are spoofing an Attribute for the Handler that will be called
        instance = self.instance
        instance.attr_key = self.cleaned_data["attr_key"] or "no_name_entered_for_attribute"
        instance.attr_category = self.cleaned_data["attr_category"] or None
        instance.attr_value = self.cleaned_data["attr_value"]
        # convert the serialized string value into an object, if necessary, for AttributeHandler
        instance.attr_value = from_pickle(instance.attr_value)
        instance.attr_type = self.cleaned_data["attr_type"] or None
        instance.attr_lockstring = self.cleaned_data["attr_lockstring"]
        return instance

    def clean_attr_value(self):
        """
        Prevent certain data-types from being cleaned due to literal_eval
        failing on them. Otherwise they will be turned into str.

        """
        data = self.cleaned_data["attr_value"]
        initial = self.instance.attr_value
        if isinstance(initial, (set, _SaverSet, datetime)):
            return initial

class AttributeFormSet(forms.BaseInlineFormSet):
    """
    Attribute version of TagFormSet, as above.
    """

        def get_handler(finished_object):
            related = getattr(finished_object, self.related_field)
            try:
                attrtype = finished_object.attr_type
                attrtype = finished_object.attribute.db_attrtype
            if attrtype == "nick":
                handler_name = "nicks"
            else:
                handler_name = "attributes"
            return getattr(related, handler_name)

        instances = super().save(commit=False)
        for obj in self.deleted_objects:
            # self.deleted_objects is a list created when super of save is called, we'll remove those
            handler = get_handler(obj)
            handler.remove(obj.attr_key, category=obj.attr_category)

        for instance in instances:
            handler = get_handler(instance)

            value = instance.attr_value

            try:
                handler.add(
                    instance.attr_key,
                    value,
                    category=instance.attr_category,
                    strattr=False,
                    lockstring=instance.attr_lockstring,
                )
            except (TypeError, ValueError):
                # catch errors in nick templates and continue
                traceback.print_exc()
                continue

class AttributeInline(admin.TabularInline):
    """
    A handler for inline Attributes. This class should be subclassed in the admin of your models,
    and the 'model' and 'related_field' class attributes must be set. model should be the
    through model (ObjectDB_db_tag', for example), while related field should be the name
    of the field on that through model which points to the model being used: 'objectdb',
    'msg', 'accountdb', etc.
    """

    # Set this to the through model of your desired M2M when subclassing.
    model = None
    verbose_name = "Attribute"
    verbose_name_plural = "Attributes"
    form = AttributeForm
    formset = AttributeFormSet
    related_field = None  # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing
    # raw_id_fields = ('attribute',)
    # readonly_fields = ('attribute',)
    extra = 0

    def get_formset(self, request, obj=None, **kwargs):
        """
        get_formset has to return a class, but we need to make the class that we return
        know about the related_field that we'll use. Returning the class itself rather than
        a proxy isn't threadsafe, since it'd be the base class and would change if multiple
        people used the admin at the same time
        """
        formset = super().get_formset(request, obj, **kwargs)

        class ProxyFormset(formset):
            pass

        ProxyFormset.related_field = self.related_field
        return ProxyFormset

"""
This defines how Comm models are displayed in the web admin interface.

"""

from evennia.comms.models import ChannelDB, Msg

class MsgTagInline(TagInline):
    """
    Inline display for Msg-tags.

    """

    model = Msg.db_tags.through
    related_field = "msg"

class MsgForm(forms.ModelForm):
    """
    Custom Msg form.

    """

        models = Msg

    db_header = forms.CharField(
        label="Header",
        help_text="Optional header for the message; it could be a title or "
        "metadata depending on msg-use.",
    )

    )

@admin.register(Msg)
class MsgAdmin(admin.ModelAdmin):
    """
    Defines display for Msg objects

    """

    inlines = [MsgTagInline]
    form = MsgForm
        "id",
        "sender",
        "receiver",
        "start_of_message",
    )
    list_display_links = ("id", "db_date_created", "start_of_message")
        "=id",
        "^db_date_created",
        "^db_message",
        "^db_sender_accounts__db_key",
        "^db_sender_objects__db_key",
        "^db_sender_scripts__db_key",
        "^db_sender_external",
        "^db_receivers_accounts__db_key",
        "^db_receivers_objects__db_key",
        "^db_receivers_scripts__db_key",
        "^db_receiver_external",
    ]
    readonly_fields = ["db_date_created", "serialized_string"]

    raw_id_fields = (
        "db_sender_accounts",
        "db_sender_objects",
        "db_sender_scripts",
        "db_receivers_accounts",
        "db_receivers_objects",
        "db_receivers_scripts",
        "db_hide_from_accounts",
        "db_hide_from_objects",
    )

        (
            None,
            {
                    (
                        "db_sender_accounts",
                        "db_sender_objects",
                        "db_sender_scripts",
                        "db_sender_external",
                    ),
                    (
                        "db_receivers_accounts",
                        "db_receivers_objects",
                        "db_receivers_scripts",
                        "db_receiver_external",
                    ),
                    ("db_hide_from_accounts", "db_hide_from_objects"),
                    "db_header",
                    "db_message",
                )
            },
        ),
    )

    def sender(self, obj):
        senders = [o for o in obj.senders if o]
            return senders[0]

    sender.help_text = "If multiple, only the first is shown."

    def receiver(self, obj):
        receivers = [o for o in obj.receivers if o]
        if receivers:
            return receivers[0]

    receiver.help_text = "If multiple, only the first is shown."

    def start_of_message(self, obj):
        crop_length = 50
        if obj.db_message:
            msg = obj.db_message
            if len(msg) > (crop_length - 5):
                msg = msg[:50] + "[...]"
            return msg

        """

        """

        "Copy & paste this string into an Attribute's `value` field to store "
        "this message-object there."
    )

        """

        """
        return super().get_form(request, obj, **kwargs)

class ChannelAttributeInline(AttributeInline):
    """
    Inline display of Channel Attribute - experimental

    """

    model = ChannelDB.db_attributes.through
    related_field = "channeldb"

class ChannelTagInline(TagInline):
    """
    Inline display of Channel Tags - experimental

    """

    model = ChannelDB.db_tags.through

class ChannelForm(forms.ModelForm):
    """
    Form for accessing channels.

    """

        model = ChannelDB

    )

@admin.register(ChannelDB)
class ChannelAdmin(admin.ModelAdmin):
    """
    Defines display for Channel objects

    """

    inlines = [ChannelTagInline, ChannelAttributeInline]
    form = ChannelForm
        "id",
        "db_key",
        "no_of_subscribers",
    )
    ordering = ["-db_date_created", "-id", "-db_key"]
    search_fields = ["id", "db_key", "db_tags__db_key"]
    readonly_fields = ["serialized_string"]
    raw_id_fields = ("db_object_subscriptions", "db_account_subscriptions")
        (
            None,
            {
                    ("db_key",),
                    "db_account_subscriptions",
                    "db_object_subscriptions",
                )
            },
        ),
    )

    def subscriptions(self, obj):
        """
        Helper method to get subs from a channel.

        Args:
            obj (Channel): The channel to get subs from.

        """
        return ", ".join([str(sub) for sub in obj.subscriptions.all()])

    def no_of_subscribers(self, obj):
        """
        Get number of subs for a a channel .

        Args:

        """
        return sum(1 for sub in obj.subscriptions.all())

        """

        """

        "Copy & paste this string into an Attribute's `value` field to store this channel there."
    )

        """

        """

        """

        Args:

        """
            obj.set_class_from_typeclass(typeclass_path=settings.BASE_CHANNEL_TYPECLASS)
        obj.at_init()

        return HttpResponseRedirect(reverse("admin:comms_channeldb_change", args=[obj.id]))

"""
Admin views.

"""

from django.contrib.admin.sites import site
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import render

@staff_member_required
def evennia_admin(request):
    """
    Helpful Evennia-specific admin page.

    """
    return render(request, "admin/frontpage.html", {"accountdb": AccountDB})

def admin_wrapper(request):
    """
    Wrapper that allows us to properly use the base Django admin site, if needed.

    """
    return staff_member_required(site.index)(request)

"""
This defines how to edit help entries in Admin.
"""

class HelpTagInline(TagInline):
    model = HelpEntry.db_tags.through
    related_field = "helpentry"

class HelpEntryForm(forms.ModelForm):
    "Defines how to display the help entry"

        model = HelpEntry

    db_help_category = forms.CharField(
        label="Help category", initial="General", help_text="organizes help entries in lists"
    )
        initial="view:all()",
        help_text="Set lock to view:all() unless you want it to only show to certain users."
        "<BR>Use the `edit:` limit if wanting to limit who can edit from in-game. By default it's "
        "only limited to who can use the `sethelp` command (Builders).",
    )

@admin.register(HelpEntry)
class HelpEntryAdmin(admin.ModelAdmin):
    "Sets up the admin manaager for help entries"
    inlines = [HelpTagInline]
    list_display = ("id", "db_key", "db_help_category", "db_lock_storage", "db_date_created")
    search_fields = ["^db_key", "db_entrytext"]
    ordering = ["db_help_category", "db_key"]
    list_filter = ["db_help_category"]

    form = HelpEntryForm
        (
            None,
            {
                    ("db_key", "db_help_category"),
                    "db_entrytext",
                    # "db_date_created",
                ),
            },
        ),
    )

#
#

class ScriptForm(forms.ModelForm):
        label="Name/Key", help_text="Script identifier, shown in listings etc."
    )

        help_text="This is the Python-path to the class implementing the actual script functionality. "
        "<BR>If your custom class is not found here, it may not be imported into Evennia yet.",
        choices=lambda: adminutils.get_and_load_typeclasses(
            parent=ScriptDB, excluded_parents=["evennia.prototypes.prototypes.DbPrototype"]
        ),
    )

    )

    db_interval = forms.IntegerField(
        label="Repeat Interval",
        help_text="Optional timer component.<BR>How often to call the Script's<BR>`at_repeat` hook, in seconds."
        "<BR>Set to 0 to disable.",
    )
    db_repeats = forms.IntegerField(
        help_text="Only repeat this many times." "<BR>Set to 0 to run indefinitely."
    )
    db_start_delay = forms.BooleanField(help_text="Wait <B>Interval</B> seconds before first call.")
    db_persistent = forms.BooleanField(
        label="Survives reboot", help_text="If unset, a server reboot will remove the timer."
    )

class ScriptTagInline(TagInline):
    """
    Inline script tags.

    """

    model = ScriptDB.db_tags.through
    related_field = "scriptdb"

class ScriptAttributeInline(AttributeInline):
    """
    Inline attribute tags.

    """

    model = ScriptDB.db_attributes.through

@admin.register(ScriptDB)
class ScriptAdmin(admin.ModelAdmin):
    """
    Displaying the main Script page.

    """

        "id",
        "db_key",
        "db_obj",
        "db_interval",
        "db_repeats",
        "db_persistent",
    )
    search_fields = ["=id", "^db_key", "db_typeclass_path"]
    form = ScriptForm
    raw_id_fields = ("db_obj",)

        (
            None,
            {
                    ("db_interval", "db_repeats", "db_start_delay", "db_persistent"),
                    "db_obj",
                )
            },
        ),
    )
    inlines = [ScriptTagInline, ScriptAttributeInline]

        """

        """

        "Copy & paste this string into an Attribute's `value` field to store this script there."
    )

        """

        """

        """

        Args:

        """

"""

This sets up how models are displayed
in the web admin interface.

"""

@admin.register(ServerConfig)
class ServerConfigAdmin(admin.ModelAdmin):
    """
    Custom admin for server configs

    """

    list_display = ("db_key", "db_value")
    list_display_links = ("db_key",)
    ordering = ["db_key", "db_value"]
    search_fields = ["db_key"]

"""
Tag admin

"""

class TagForm(forms.ModelForm):
    """
    Form to display fields in the stand-alone Tag display.

    """

    db_key = forms.CharField(label="Key/Name", required=True, help_text="The main key identifier")
    db_category = forms.CharField(
        help_text="Used for grouping tags. Unset (default) gives a category of None",
    )
    db_tagtype = forms.ChoiceField(
        choices=[(None, "-"), ("alias", "alias"), ("permission", "permission")],
        help_text="Tags are used for different things. Unset for regular tags.",
    )
    db_model = forms.ChoiceField(
        label="Model",
        help_text="Each Tag can only 'attach' to one type of entity.",
        choices=(
            [
                ("objectdb", "objectdb"),
                ("accountdb", "accountdb"),
                ("scriptdb", "scriptdb"),
                ("channeldb", "channeldb"),
                ("helpentry", "helpentry"),
                ("msg", "msg"),
            ]
        ),
    )
    db_data = forms.CharField(
        label="Data",
        help_text="Usually unused. Intended for info about the tag itself",
    )

        fields = ("tag_key", "tag_category", "tag_data", "tag_type")

class InlineTagForm(forms.ModelForm):
    """
    Form for displaying tags inline together with other entities.

    This form overrides the base behavior of the ModelForm that would be used for a
    Tag-through-model.  Since the through-models only have access to the foreignkeys of the Tag and
    the Object that they're attached to, we need to spoof the behavior of it being a form that would
    correspond to its tag, or the creation of a tag. Instead of being saved, we'll call to the
    Object's handler, which will handle the creation, change, or deletion of a tag for us, as well
    as updating the handler's cache so that all changes are instantly updated in-game.
    """

    tag_key = forms.CharField(
        label="Tag Name", required=True, help_text="This is the main key identifier"
    )
    tag_category = forms.CharField(
    )
    tag_type = forms.ChoiceField(
    )
    tag_data = forms.CharField(
        help_text="Usually unused. Intended for eventual info about the tag itself",
    )

        """
        If we have a tag, then we'll prepopulate our instance with the fields we'd expect it
        to have based on the tag. tag_key, tag_category, tag_type, and tag_data all refer to
        the corresponding tag fields. The initial data of the form fields will similarly be
        populated.
        """
        tagkey = None
        tagcategory = None
        tagtype = None
        tagdata = None
        if hasattr(self.instance, "tag"):
            tagkey = self.instance.tag.db_key
            tagcategory = self.instance.tag.db_category
            tagtype = self.instance.tag.db_tagtype
            tagdata = self.instance.tag.db_data
            self.fields["tag_key"].initial = tagkey
            self.fields["tag_category"].initial = tagcategory
            self.fields["tag_type"].initial = tagtype
            self.fields["tag_data"].initial = tagdata
        self.instance.tag_key = tagkey
        self.instance.tag_category = tagcategory
        self.instance.tag_type = tagtype
        self.instance.tag_data = tagdata

        """
        """
        # we are spoofing a tag for the Handler that will be called
        # instance = super().save(commit=False)
        instance.tag_key = self.cleaned_data["tag_key"]
        instance.tag_category = self.cleaned_data["tag_category"] or None
        instance.tag_type = self.cleaned_data["tag_type"] or None
        instance.tag_data = self.cleaned_data["tag_data"] or None

class TagFormSet(forms.BaseInlineFormSet):
    """
    The Formset handles all the inline forms that are grouped together on the change page of the
    corresponding object. All the tags will appear here, and we'll save them by overriding the
    formset's save method. The forms will similarly spoof their save methods to return an instance
    which hasn't been saved to the database, but have the relevant fields filled out based on the
    contents of the cleaned form. We'll then use that to call to the handler of the corresponding
    Object, where the handler is an AliasHandler, PermissionsHandler, or TagHandler, based on the
    type of tag.
    """

    verbose_name = "Tag"
    verbose_name_plural = "Tags"

            try:
                tagtype = finished_object.tag_type
                tagtype = finished_object.tag.db_tagtype
            if tagtype == "alias":
                handler_name = "aliases"
            elif tagtype == "permission":
                handler_name = "permissions"
            else:
                handler_name = "tags"

        # self.deleted_objects is a list created when super of save is called, we'll remove those
            handler.remove(obj.tag_key, category=obj.tag_category)
            handler.add(instance.tag_key, category=instance.tag_category, data=instance.tag_data)

class TagInline(admin.TabularInline):
    """
    A handler for inline Tags. This class should be subclassed in the admin of your models,
    """

    form = InlineTagForm
    formset = TagFormSet
    # raw_id_fields = ('tag',)
    # readonly_fields = ('tag',)
    extra = 0

        """
        """

            pass

@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    """
    A django Admin wrapper for Tags.

    """

    search_fields = ("db_key", "db_category", "db_tagtype")
    list_display = ("db_key", "db_category", "db_tagtype", "db_model", "db_data")
    list_filter = ("db_tagtype", "db_category", "db_model")
    form = TagForm

        (
            None,
            {"fields": (("db_key", "db_category"), ("db_tagtype", "db_model"), "db_data")},
        ),
    )

"""
Helper utils for admin views.

"""

from evennia.utils.utils import get_all_cmdsets, get_all_typeclasses, inherits_from

def get_and_load_typeclasses(parent=None, excluded_parents=None):
    """
    Get all typeclasses. We we need to initialize things here
    for them to be actually available in the admin process.
    This is intended to be used with forms.ChoiceField.

    Args:
        parent (str or class, optional): Limit selection to this class and its children
            (at any distance).
        exclude (list): Class-parents to exclude from the resulting list. All
            children of these paretns will be skipped.

    Returns:
        list: A list of (str, str), the way ChoiceField wants it.

    """
    # this is necessary in order to have typeclasses imported and accessible
    # in the inheritance tree.

    evennia._init()

    # this return a dict (path: class}
    tmap = get_all_typeclasses(parent=parent)

    # filter out any excludes
    excluded_parents = excluded_parents or []
    tpaths = [
        path
        for path, tclass in tmap.items()
        if not any(inherits_from(tclass, excl) for excl in excluded_parents)
    ]

    # sort so we get custom paths (not in evennia repo) first
    tpaths = sorted(tpaths, key=lambda k: (1 if k.startswith("evennia.") else 0, k))

    # the base models are not typeclasses so we filter them out
    tpaths = [
        path
        for path in tpaths
        if path
        not in (
            "evennia.objects.models.ObjectDB",
            "evennia.accounts.models.AccountDB",
            "evennia.scripts.models.ScriptDB",
            "evennia.comms.models.ChannelDB",
        )
    ]

    # return on form excepted by ChoiceField
    return [(path, path) for path in tpaths if path]

def get_and_load_cmdsets(parent=None, excluded_parents=None):
    """
    Get all cmdsets available or as children based on a parent cmdset.  We need
    to initialize things here to make sure as much as possible is loaded in the
    admin process. This is intended to be used with forms.ChoiceField.

    Args:
        parent (str, optional): Python-path to the parent cmdset, if any.
        excluded_parents (list): A list of cmset-paths to exclude from the result.

    Returns:

    """
    # we must do this to have cmdsets imported and accessible in the inheritance tree.

    cmap = get_all_cmdsets(parent)

    cpaths = [path for path in cmap if not any(path == excluded for excluded in excluded_parents)]

    cpaths = sorted(cpaths, key=lambda k: (1 if k.startswith("evennia.") else 0, k))

    # return on form expected by ChoiceField
    return [("", "-")] + [(path, path) for path in cpaths if path]

"""
FilterSets allow clients to specify querystrings that will determine the data
that is retrieved in GET requests. By default, Django Rest Framework uses the
'django-filter' package as its backend. Django-filter also has a section in its
documentation specifically regarding DRF integration.

https://django-filter.readthedocs.io/en/latest/guide/rest_framework.html

"""
from typing import Union

from django_filters.filters import EMPTY_VALUES, CharFilter
from django_filters.rest_framework.filterset import FilterSet

def get_tag_query(tag_type: Union[str, None], key: str) -> Q:
    """
    Returns a Q object for searching by tag names for typeclasses
    Args:
        tag_type(str or None): The type of tag (None, 'alias', etc)
        key (str): The name of the tag

    Returns:
        A Q object that for searching by this tag type and name

    """
    return Q(db_tags__db_tagtype=tag_type) & Q(db_tags__db_key__iexact=key)

class TagTypeFilter(CharFilter):
    """
    This class lets you create different filters for tags of a specified db_tagtype.

    """

    tag_type = None

    def filter(self, qs, value):
        # if no value is specified, we don't use the filter
        if value in EMPTY_VALUES:
            return qs
        # if they enter a value, we filter objects by having a tag of this type with the given name
        return qs.filter(get_tag_query(self.tag_type, value)).distinct()

class AliasFilter(TagTypeFilter):
    """A filter for objects by their aliases (tags with a tagtype of 'alias'"""

    tag_type = "alias"

class PermissionFilter(TagTypeFilter):
    """A filter for objects by their permissions (tags with a tagtype of 'permission'"""

    tag_type = "permission"

SHARED_FIELDS = ["db_key", "db_typeclass_path", "db_tags__db_key", "db_tags__db_category"]

class BaseTypeclassFilterSet(FilterSet):
    """
    A parent class with filters for aliases and permissions

    """

    name = CharFilter(lookup_expr="iexact", method="filter_name", field_name="db_key")
    alias = AliasFilter(lookup_expr="iexact")
    permission = PermissionFilter(lookup_expr="iexact")

    def filter_name(queryset, name, value):
        """
        Filters a queryset by aliases or the key of the typeclass
        Args:
            queryset: The queryset being filtered
            name: The name of the field
            value: The value passed in from GET params

        Returns:
            The filtered queryset
        """
        query = Q(**{f"{name}__iexact": value})
        query |= get_tag_query("alias", value)
        return queryset.filter(query).distinct()

class ObjectDBFilterSet(BaseTypeclassFilterSet):
    """
    This adds filters for ObjectDB instances - characters, rooms, exits, etc

    """

        fields = SHARED_FIELDS + [
            "db_location__db_key",
            "db_home__db_key",
            "db_location__id",
            "db_home__id",
        ]

class AccountDBFilterSet(BaseTypeclassFilterSet):
    """This adds filters for Account objects"""

    name = CharFilter(lookup_expr="iexact", method="filter_name", field_name="username")

        fields = [
            fi
            for fi in (SHARED_FIELDS + ["username", "db_is_connected", "db_is_bot"])
            if fi != "db_key"
        ]

class ScriptDBFilterSet(BaseTypeclassFilterSet):
    """This adds filters for Script objects"""

        model = ScriptDB
            "db_desc",
            "db_obj__db_key",
            "db_obj__id",
            "db_account__id",
            "db_account__username",
            "db_is_active",
            "db_persistent",
            "db_interval",
        ]

class HelpFilterSet(FilterSet):

    """
    Filter for help entries

    """

    category = CharFilter(lookup_expr="iexact", method="filter_name", field_name="db_category")

"""
Sets up an api-access permission check using the in-game permission hierarchy.

"""

from rest_framework import permissions

from evennia.locks.lockhandler import check_perm

class EvenniaPermission(permissions.BasePermission):
    """
    A Django Rest Framework permission class that allows us to use Evennia's
    permission structure. Based on the action in a given view, we'll check a
    corresponding Evennia access/lock check.

    """

    # subclass this to change these permissions
    MINIMUM_LIST_PERMISSION = settings.REST_FRAMEWORK.get("DEFAULT_LIST_PERMISSION", "builder")
    MINIMUM_CREATE_PERMISSION = settings.REST_FRAMEWORK.get("DEFAULT_CREATE_PERMISSION", "builder")
    view_locks = settings.REST_FRAMEWORK.get("DEFAULT_VIEW_LOCKS", ["examine"])
    destroy_locks = settings.REST_FRAMEWORK.get("DEFAULT_DESTROY_LOCKS", ["delete"])
    update_locks = settings.REST_FRAMEWORK.get("DEFAULT_UPDATE_LOCKS", ["control", "edit"])

    def has_permission(self, request, view):
        """Checks for permissions

        Args:
            request (Request): The incoming request object.
            view (View): The django view we are checking permission for.

        Returns:
            bool: If permission is granted or not. If we return False here, a PermissionDenied
            error will be raised from the view.

        Notes:
            This method is a check that always happens first. If there's an object involved,
            such as with retrieve, update, or delete, then the has_object_permission method
            is called after this, assuming this returns `True`.
        """
        # Only allow authenticated users to call the API
        if not request.user.is_authenticated:
        if request.user.is_superuser:
        # these actions don't support object-level permissions, so use the above definitions
        if view.action == "list":
            return check_perm(request.user, self.MINIMUM_LIST_PERMISSION)
        if view.action == "create":
            return check_perm(request.user, self.MINIMUM_CREATE_PERMISSION)
        return True  # this means we'll check object-level permissions

    def check_locks(obj, user, locks):
        """Checks access for user for object with given locks
        Args:
            obj: Object instance we're checking
            user (Account): User who we're checking permissions
            locks (list): list of lockstrings

        Returns:
            bool: True if they have access, False if they don't
        """
        return any([obj.access(user, lock) for lock in locks])

    def has_object_permission(self, request, view, obj):
        """Checks object-level permissions after has_permission

        Args:
            obj: Object we're checking object-level permissions for

        Returns:

        Notes:
            This method assumes that has_permission has already returned True. We check
            equivalent Evennia permissions in the request.user to determine if they can
            complete the action.
        """
        if view.action in ("list", "retrieve"):
            # access_type is based on the examine command
            return self.check_locks(obj, request.user, self.view_locks)
        if view.action == "destroy":
            # access type based on the destroy command
            return self.check_locks(obj, request.user, self.destroy_locks)
        if view.action in ("update", "partial_update", "set_attribute"):
            # access type based on set command
            return self.check_locks(obj, request.user, self.update_locks)

"""
Set a more useful description on the Api root.

"""

from rest_framework import routers

class EvenniaAPIRoot(routers.APIRootView):
    """
    Root of the Evennia API tree.

    """

    pass

class APIRootRouter(routers.DefaultRouter):
    APIRootView = EvenniaAPIRoot

"""
Serializers in the Django Rest Framework are similar to Forms in normal django.
They're used for transmitting and validating data, both going to clients and
coming to the server. However, where forms often contained presentation logic,
such as specifying widgets to use for selection, serializers typically leave
those decisions in the hands of clients, and are more focused on converting
data from the server to JSON (serialization) for a response, and validating
and converting JSON data sent from clients to our enpoints into python objects,
often django model instances, that we can use (deserialization).

"""

from rest_framework import serializers

class AttributeSerializer(serializers.ModelSerializer):
    """
    Serialize Attribute views.

    """

    value_display = serializers.SerializerMethodField(source="value")
    db_value = serializers.CharField(write_only=True, required=False)

        model = Attribute
        fields = ["db_key", "db_category", "db_attrtype", "value_display", "db_value"]

    def get_value_display(obj: Attribute) -> str:
        """
        Gets the string display of an Attribute's value for serialization
        Args:
            obj: Attribute being serialized

        Returns:
            The Attribute's value in string format

        """
        if obj.db_strvalue:
            return obj.db_strvalue
        return str(obj.value)

class TagSerializer(serializers.ModelSerializer):
        model = Tag
        fields = ["db_key", "db_category", "db_data", "db_tagtype"]

class SimpleObjectDBSerializer(serializers.ModelSerializer):
        model = DefaultObject
        fields = ["id", "db_key"]

class TypeclassSerializerMixin:
    """
    Mixin that contains types shared by typeclasses. A note about tags,
    aliases, and permissions. You might note that the methods and fields are
    defined here, but they're included explicitly in each child class. What
    gives? It's a DRF error: serializer method fields which are inherited do
    not resolve correctly in child classes, and as of this current version
    (3.11) you must have them in the child classes explicitly to avoid field
    errors. Similarly, the child classes must contain the attribute serializer
    explicitly to not have them render PK-related fields.

    """

    shared_fields = [
        "id",
        "db_key",
        "attributes",
        "aliases",
        "tags",
        "permissions",
    ]

    def get_tags(obj):
        """
        Serializes tags from the object's Tagshandler
        Args:
            obj: Typeclassed object being serialized

        Returns:
            List of TagSerializer data
        """
        return TagSerializer(obj.tags.get(return_tagobj=True, return_list=True), many=True).data

    def get_aliases(obj):
        """
        Serializes tags from the object's Aliashandler
        Args:

        Returns:
        """
        return TagSerializer(obj.aliases.get(return_tagobj=True, return_list=True), many=True).data

    def get_permissions(obj):
        """
        Serializes tags from the object's Permissionshandler
        Args:

        Returns:
        """
        return TagSerializer(
            obj.permissions.get(return_tagobj=True, return_list=True), many=True
        ).data

    def get_attributes(obj):
        """
        Serializes attributes from the object's AttributeHandler
        Args:

        Returns:
            List of AttributeSerializer data
        """
        return AttributeSerializer(obj.attributes.all(), many=True).data

    def get_nicks(obj):
        """
        Serializes attributes from the object's NicksHandler
        Args:

        Returns:
        """
        return AttributeSerializer(obj.nicks.all(), many=True).data

class TypeclassListSerializerMixin:
    """
    Shortened serializer for list views.

    """

        "id",
        "db_key",
    ]

class ObjectDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
    """
    Serializing Objects.

    """

    attributes = serializers.SerializerMethodField()
    nicks = serializers.SerializerMethodField()
    contents = serializers.SerializerMethodField()
    exits = serializers.SerializerMethodField()
    tags = serializers.SerializerMethodField()
    aliases = serializers.SerializerMethodField()
    permissions = serializers.SerializerMethodField()

        fields = [
            "db_location",
            "db_home",
            "contents",
            "exits",
            "nicks",
        ] + TypeclassSerializerMixin.shared_fields
        read_only_fields = ["id"]

    def get_exits(obj):
        """
        Gets exits for the object
        Args:
            obj: Object being serialized

        Returns:
            List of data from SimpleObjectDBSerializer
        """
        exits = [ob for ob in obj.contents if ob.destination]
        return SimpleObjectDBSerializer(exits, many=True).data

    def get_contents(obj):
        """
        Gets non-exits for the object
        Args:

        Returns:
        """
        non_exits = [ob for ob in obj.contents if not ob.destination]
        return SimpleObjectDBSerializer(non_exits, many=True).data

class ObjectListSerializer(TypeclassListSerializerMixin, serializers.ModelSerializer):
    """
    Shortened representation for listings.]

    """

        fields = [
        ] + TypeclassListSerializerMixin.shared_fields

class AccountSerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
    """
    This uses the DefaultAccount object to have access to the sessions property

    """

    db_key = serializers.CharField(required=False)
    session_ids = serializers.SerializerMethodField()

    def get_session_ids(obj):
        """
        Gets a list of session IDs connected to this Account
        Args:
            obj (DefaultAccount): Account we're grabbing sessions from

        Returns:
            List of session IDs
        """
        return [sess.sessid for sess in obj.sessions.all() if hasattr(sess, "sessid")]

        model = DefaultAccount
        fields = ["username", "session_ids", "nicks"] + TypeclassSerializerMixin.shared_fields

class AccountListSerializer(TypeclassListSerializerMixin, serializers.ModelSerializer):
    """
    A shortened form for listing.

    """

        fields = ["username"] + [
            fi for fi in TypeclassListSerializerMixin.shared_fields if fi != "db_key"
        ]

class ScriptDBSerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
    """
    Serializing Account.

    """

        fields = [
            "db_start_delay",
            "db_repeats",

class ScriptListSerializer(TypeclassListSerializerMixin, serializers.ModelSerializer):
    """
    Shortened form for listing.

    """

        fields = [

class HelpSerializer(TypeclassSerializerMixin, serializers.ModelSerializer):
    """
    Serializers Help entries (not a typeclass).

    """

        fields = [
            "id",
            "db_key",
            "db_help_category",
            "db_entrytext",
            "db_date_created",
            "tags",
        ]

class HelpListSerializer(TypeclassListSerializerMixin, serializers.ModelSerializer):
    """
    Shortened form for listings.

    """

        fields = [
            "id",
            "db_key",
        ]

"""
Tests for the REST API.

"""

from django.urls import include, path, reverse
from rest_framework.test import APIClient

from evennia.web.api import serializers

urlpatterns = [
    path(r"^", include("evennia.web.website.urls")),
    path(r"^api/", include("evennia.web.api.urls", namespace="api")),
]

@override_settings(REST_API_ENABLED=True, ROOT_URLCONF=__name__, AUTH_USERNAME_VALIDATORS=[])
class TestEvenniaRESTApi(BaseEvenniaTest):
    client_class = APIClient

        self.account.is_superuser = True
        self.client.force_login(self.account)
        # scripts do not have default locks. Without them, even superuser access check fails
        self.script.locks.add("edit: perm(Admin); examine: perm(Admin); delete: perm(Admin)")

        try:
            super().tearDown()
            pass

    def get_view_details(self, action):
        """Helper function for generating list of named tuples"""
        View = namedtuple(
            "View",
            [
                "view_name",
                "obj",
                "list",
                "serializer",
                "list_serializer",
                "create_data",
                "retrieve_data",
            ],
        )
        views = [
            View(
                "object-%s" % action,
                self.obj1,
                [self.obj1, self.char1, self.exit, self.room1, self.room2, self.obj2, self.char2],
                serializers.ObjectDBSerializer,
                serializers.ObjectListSerializer,
                {"db_key": "object-create-test-name"},
                serializers.ObjectDBSerializer(self.obj1).data,
            ),
            View(
                "character-%s" % action,
                [self.char1, self.char2],
                {"db_key": "character-create-test-name"},
                serializers.ObjectDBSerializer(self.char1).data,
            ),
            View(
                "exit-%s" % action,
                self.exit,
                [self.exit],
                {"db_key": "exit-create-test-name"},
                serializers.ObjectDBSerializer(self.exit).data,
            ),
            View(
                "room-%s" % action,
                self.room1,
                [self.room1, self.room2],
                {"db_key": "room-create-test-name"},
                serializers.ObjectDBSerializer(self.room1).data,
            ),
            View(
                "script-%s" % action,
                self.script,
                [self.script],
                serializers.ScriptDBSerializer,
                serializers.ScriptListSerializer,
                {"db_key": "script-create-test-name"},
                serializers.ScriptDBSerializer(self.script).data,
            ),
            View(
                "account-%s" % action,
                self.account2,
                [self.account, self.account2],
                serializers.AccountSerializer,
                serializers.AccountListSerializer,
                {"username": "account-create-test-name"},
                serializers.AccountSerializer(self.account2).data,
            ),
        ]
        return views

    def test_retrieve(self):
        views = self.get_view_details("detail")
        for view in views:
            with self.subTest(msg="Testing {} retrieve".format(view.view_name)):
                view_url = reverse("api:{}".format(view.view_name), kwargs={"pk": view.obj.pk})
                response = self.client.get(view_url)
                self.assertEqual(response.status_code, 200)
                self.assertDictEqual(response.data, view.retrieve_data)

    def test_update(self):
            with self.subTest(msg="Testing {} update".format(view.view_name)):
                # test both PUT (update) and PATCH (partial update) here
                for new_key, method in (("foobar", "put"), ("fizzbuzz", "patch")):
                    field = "username" if "account" in view.view_name else "db_key"
                    data = {field: new_key}
                    response = getattr(self.client, method)(view_url, data=data)
                    self.assertEqual(response.status_code, 200)
                    view.obj.refresh_from_db()
                    self.assertEqual(getattr(view.obj, field), new_key)
                    self.assertEqual(response.data[field], new_key)

            with self.subTest(msg="Testing {} delete".format(view.view_name)):
                response = self.client.delete(view_url)
                self.assertEqual(response.status_code, 204)
                with self.assertRaises(ObjectDoesNotExist):

        views = self.get_view_details("list")
            with self.subTest(msg=f"Testing {view.view_name} "):
                view_url = reverse(f"api:{view.view_name}")
                self.assertCountEqual(
                    response.data["results"], [view.list_serializer(obj).data for obj in view.list]
                )

            with self.subTest(msg=f"Testing {view.view_name} create"):
                # create is a POST request off of <type>-list
                # check failures from not sending required fields
                response = self.client.post(view_url)
                self.assertEqual(response.status_code, 400)
                # check success when sending the required data
                response = self.client.post(view_url, data=view.create_data)
                self.assertEqual(response.status_code, 201, f"Response was {response.data}")

    def test_set_attribute(self):
        views = self.get_view_details("set-attribute")
            with self.subTest(msg=f"Testing {view.view_name}"):
                view_url = reverse(f"api:{view.view_name}", kwargs={"pk": view.obj.pk})
                self.assertEqual(response.status_code, 400, f"Response was: {response.data}")
                # test adding an attribute
                self.assertEqual(view.obj.db.some_test_attr, None)
                attr_name = "some_test_attr"
                attr_data = {"db_key": attr_name, "db_value": "test_value"}
                response = self.client.post(view_url, data=attr_data)
                self.assertEqual(response.status_code, 200, f"Response was: {response.data}")
                self.assertEquals(view.obj.attributes.get(attr_name), "test_value")
                # now test removing it
                attr_data = {"db_key": attr_name}
                self.assertEquals(view.obj.attributes.get(attr_name), None)

# Note that we import as `DjangoRequestFactory` and `DjangoClient` in order
# to make it harder for the user to import the wrong thing without realizing.
import io

from django.core.handlers.wsgi import WSGIHandler
from django.test import override_settings, testcases
from django.test.client import Client as DjangoClient
from django.test.client import ClientHandler
from django.test.client import RequestFactory as DjangoRequestFactory
from django.utils.encoding import force_bytes
from django.utils.http import urlencode

from rest_framework.compat import coreapi, requests
from rest_framework.settings import api_settings

def force_authenticate(request, user=None, token=None):
    request._force_auth_user = user
    request._force_auth_token = token

if requests is not None:
    class HeaderDict(requests.packages.urllib3._collections.HTTPHeaderDict):
        def get_all(self, key, default):
            return self.getheaders(key)

    class MockOriginalResponse:
        def __init__(self, headers):
            self.msg = HeaderDict(headers)
            self.closed = False

        def isclosed(self):
            return self.closed

        def close(self):
            self.closed = True

    class DjangoTestAdapter(requests.adapters.HTTPAdapter):
        """
        A transport adapter for `requests`, that makes requests via the
        Django WSGI app, rather than making actual HTTP requests over the network.
        """
            self.app = WSGIHandler()
            self.factory = DjangoRequestFactory()

        def get_environ(self, request):
            """
            Given a `requests.PreparedRequest` instance, return a WSGI environ dict.
            """
            method = request.method
            url = request.url
            kwargs = {}

            # Set request content, if any exists.
            if request.body is not None:
                if hasattr(request.body, 'read'):
                    kwargs['data'] = request.body.read()
                else:
                    kwargs['data'] = request.body
            if 'content-type' in request.headers:
                kwargs['content_type'] = request.headers['content-type']

            # Set request headers.
            for key, value in request.headers.items():
                key = key.upper()
                if key in ('CONNECTION', 'CONTENT-LENGTH', 'CONTENT-TYPE'):
                    continue
                kwargs['HTTP_%s' % key.replace('-', '_')] = value

            return self.factory.generic(method, url, **kwargs).environ

        def send(self, request, *args, **kwargs):
            """
            Make an outgoing request to the Django WSGI application.
            """
            raw_kwargs = {}

            def start_response(wsgi_status, wsgi_headers, exc_info=None):
                status, _, reason = wsgi_status.partition(' ')
                raw_kwargs['status'] = int(status)
                raw_kwargs['reason'] = reason
                raw_kwargs['headers'] = wsgi_headers
                raw_kwargs['version'] = 11
                raw_kwargs['preload_content'] = False
                raw_kwargs['original_response'] = MockOriginalResponse(wsgi_headers)

            # Make the outgoing request via WSGI.
            environ = self.get_environ(request)
            wsgi_response = self.app(environ, start_response)

            # Build the underlying urllib3.HTTPResponse
            raw_kwargs['body'] = io.BytesIO(b''.join(wsgi_response))
            raw = requests.packages.urllib3.HTTPResponse(**raw_kwargs)

            # Build the requests.Response
            return self.build_response(request, raw)

            pass

    class RequestsClient(requests.Session):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            adapter = DjangoTestAdapter()
            self.mount('http://', adapter)
            self.mount('https://', adapter)

        def request(self, method, url, *args, **kwargs):
            if not url.startswith('http'):
                raise ValueError('Missing "http:" or "https:". Use a fully qualified URL, eg "http://testserver%s"' % url)
            return super().request(method, url, *args, **kwargs)

else:
    def RequestsClient(*args, **kwargs):
        raise ImproperlyConfigured('requests must be installed in order to use RequestsClient.')

if coreapi is not None:
    class CoreAPIClient(coreapi.Client):
            self._session = RequestsClient()
            kwargs['transports'] = [coreapi.transports.HTTPTransport(session=self.session)]

        @property
        def session(self):
            return self._session

else:
    def CoreAPIClient(*args, **kwargs):
        raise ImproperlyConfigured('coreapi must be installed in order to use CoreAPIClient.')

class APIRequestFactory(DjangoRequestFactory):
    renderer_classes_list = api_settings.TEST_REQUEST_RENDERER_CLASSES
    default_format = api_settings.TEST_REQUEST_DEFAULT_FORMAT

    def __init__(self, enforce_csrf_checks=False, **defaults):
        self.enforce_csrf_checks = enforce_csrf_checks
        self.renderer_classes = {}
        for cls in self.renderer_classes_list:
            self.renderer_classes[cls.format] = cls
        super().__init__(**defaults)

    def _encode_data(self, data, format=None, content_type=None):
        """
        Encode the data returning a two tuple of (bytes, content_type)
        """

        if data is None:
            return ('', content_type)

        assert format is None or content_type is None, (
            'You may not set both `format` and `content_type`.'
        )

        if content_type:
            # Content type specified explicitly, treat data as a raw bytestring
            ret = force_bytes(data, settings.DEFAULT_CHARSET)

        else:
            format = format or self.default_format

            assert format in self.renderer_classes, (
                "Invalid format '{}'. Available formats are {}. "
                "Set TEST_REQUEST_RENDERER_CLASSES to enable "
                "extra request formats.".format(
                    format,
                    ', '.join(["'" + fmt + "'" for fmt in self.renderer_classes])
                )
            )

            # Use format and render the data into a bytestring
            renderer = self.renderer_classes[format]()
            ret = renderer.render(data)

            # Determine the content-type header from the renderer
            content_type = renderer.media_type
            if renderer.charset:
                content_type = "{}; charset={}".format(
                    content_type, renderer.charset
                )

            # Coerce text to bytes if required.
            if isinstance(ret, str):
                ret = ret.encode(renderer.charset)

        return ret, content_type

    def get(self, path, data=None, **extra):
        r = {
            'QUERY_STRING': urlencode(data or {}, doseq=True),
        }
        if not data and '?' in path:
            # Fix to support old behavior where you have the arguments in the
            # url. See #1461.
            query_string = force_bytes(path.split('?')[1])
            query_string = query_string.decode('iso-8859-1')
            r['QUERY_STRING'] = query_string
        r.update(extra)
        return self.generic('GET', path, **r)

    def post(self, path, data=None, format=None, content_type=None, **extra):
        data, content_type = self._encode_data(data, format, content_type)
        return self.generic('POST', path, data, content_type, **extra)

    def put(self, path, data=None, format=None, content_type=None, **extra):
        return self.generic('PUT', path, data, content_type, **extra)

    def patch(self, path, data=None, format=None, content_type=None, **extra):
        return self.generic('PATCH', path, data, content_type, **extra)

    def delete(self, path, data=None, format=None, content_type=None, **extra):
        return self.generic('DELETE', path, data, content_type, **extra)

    def options(self, path, data=None, format=None, content_type=None, **extra):
        return self.generic('OPTIONS', path, data, content_type, **extra)

    def generic(self, method, path, data='',
                content_type='application/octet-stream', secure=False, **extra):
        # Include the CONTENT_TYPE, regardless of whether or not data is empty.
            extra['CONTENT_TYPE'] = str(content_type)

        return super().generic(
            method, path, data, content_type, secure, **extra)

    def request(self, **kwargs):
        request = super().request(**kwargs)
        request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
        return request

class ForceAuthClientHandler(ClientHandler):
    """
    A patched version of ClientHandler that can enforce authentication
    on the outgoing requests.
    """

        self._force_user = None
        self._force_token = None

    def get_response(self, request):
        # This is the simplest place we can hook into to patch the
        # request object.
        force_authenticate(request, self._force_user, self._force_token)
        return super().get_response(request)

class APIClient(APIRequestFactory, DjangoClient):
        self.handler = ForceAuthClientHandler(enforce_csrf_checks)
        self._credentials = {}

    def credentials(self, **kwargs):
        """
        Sets headers that will be used on every outgoing request.
        """
        self._credentials = kwargs

    def force_authenticate(self, user=None, token=None):
        """
        Forcibly authenticates outgoing requests with the given
        user and/or token.
        """
        self.handler._force_user = user
        self.handler._force_token = token
        if user is None and token is None:
            self.logout()  # Also clear any possible session info if required

        # Ensure that any credentials set get added to every request.
        kwargs.update(self._credentials)
        return super().request(**kwargs)

    def get(self, path, data=None, follow=False, **extra):
        response = super().get(path, data=data, **extra)
        if follow:
            response = self._handle_redirects(response, data=data, **extra)
        return response

    def post(self, path, data=None, format=None, content_type=None,
             follow=False, **extra):
        response = super().post(
            path, data=data, format=format, content_type=content_type, **extra)
        if follow:
            response = self._handle_redirects(response, data=data, format=format, content_type=content_type, **extra)

    def put(self, path, data=None, format=None, content_type=None,
            follow=False, **extra):
        response = super().put(
        if follow:

    def patch(self, path, data=None, format=None, content_type=None,
              follow=False, **extra):
        response = super().patch(
        if follow:

    def delete(self, path, data=None, format=None, content_type=None,
               follow=False, **extra):
        response = super().delete(
        if follow:

    def options(self, path, data=None, format=None, content_type=None,
                follow=False, **extra):
        response = super().options(
        if follow:

        # Also clear any `force_authenticate`
        self.handler._force_user = None
        self.handler._force_token = None

            super().logout()

class APITransactionTestCase(testcases.TransactionTestCase):

class APITestCase(testcases.TestCase):

class APISimpleTestCase(testcases.SimpleTestCase):

class APILiveServerTestCase(testcases.LiveServerTestCase):

def cleanup_url_patterns(cls):
    if hasattr(cls, '_module_urlpatterns'):
        cls._module.urlpatterns = cls._module_urlpatterns
    else:
        del cls._module.urlpatterns

class URLPatternsTestCase(testcases.SimpleTestCase):
    """
    Isolate URL patterns on a per-TestCase basis. For example,

    class ATestCase(URLPatternsTestCase):
        urlpatterns = [...]

        def test_something(self):
            ...

    class AnotherTestCase(URLPatternsTestCase):

        def test_something_else(self):
            ...
    """
    def setUpClass(cls):
        # Get the module of the TestCase subclass
        cls._module = import_module(cls.__module__)
        cls._override = override_settings(ROOT_URLCONF=cls.__module__)

        if hasattr(cls._module, 'urlpatterns'):
            cls._module_urlpatterns = cls._module.urlpatterns

        cls._module.urlpatterns = cls.urlpatterns

        cls._override.enable()

        if django.VERSION > (4, 0):
            cls.addClassCleanup(cls._override.disable)
            cls.addClassCleanup(cleanup_url_patterns, cls)

        super().setUpClass()

    if django.VERSION < (4, 0):
        @classmethod
        def tearDownClass(cls):
            super().tearDownClass()
            cls._override.disable()

            if hasattr(cls, '_module_urlpatterns'):
                cls._module.urlpatterns = cls._module_urlpatterns
            else:
                del cls._module.urlpatterns

"""
Views are the functions that are called by different url endpoints. The Django
Rest Framework provides collections called 'ViewSets', which can generate a
number of views for the common CRUD operations.

"""
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from evennia.objects.objects import DefaultCharacter, DefaultExit, DefaultRoom
from evennia.web.api import filters, serializers
from evennia.web.api.permissions import EvenniaPermission

class GeneralViewSetMixin:
    """
    Mixin for both typeclass- and non-typeclass entities.

    """

    def get_serializer_class(self):
        """
        Allow different serializers for certain actions.

        """
        if self.action == "list":
            if hasattr(self, "list_serializer_class"):
                return self.list_serializer_class
        return self.serializer_class

class TypeclassViewSetMixin(GeneralViewSetMixin):
    """
    This mixin adds some shared functionality to each viewset of a typeclass. They all use the same
    permission classes and filter backend. You can override any of these in your own viewsets.

    The `set_atribute` action is  an example of a custom action added to a
    viewset. Based on the name of the method, it will create a default url_name
    (used for reversing) and url_path.  The 'pk' argument is automatically
    passed to this action because it has a url path of the format <object
    type>/:pk/set-attribute. The get_object method is automatically set in the
    expected viewset classes that will inherit this, using the pk that's passed
    along to retrieve the object.

    """

    # permission classes determine who is authorized to call the view
    permission_classes = [EvenniaPermission]
    # the filter backend allows for retrieval views to have filter arguments passed to it,
    # for example: mygame.com/api/objects?db_key=bob to find matches based on objects having a db_key of bob
    filter_backends = [DjangoFilterBackend]

    @action(detail=True, methods=["put", "post"])
    def set_attribute(self, request, pk=None):
        """
        This action will set an attribute if the db_value is defined, or remove
        it if no db_value is provided.

        """
        attr = serializers.AttributeSerializer(data=request.data)
        obj = self.get_object()
        if attr.is_valid(raise_exception=True):
            key = attr.validated_data["db_key"]
            value = attr.validated_data.get("db_value")
            category = attr.validated_data.get("db_category")
            attr_type = attr.validated_data.get("db_attrtype")
            if attr_type == "nick":
                handler = obj.nicks
            else:
                handler = obj.attributes
            if value:
                handler.add(key=key, value=value, category=category)
            else:
                handler.remove(key=key, category=category)
            return Response(
                serializers.AttributeSerializer(obj.db_attributes.all(), many=True).data,
                status=status.HTTP_200_OK,
            )
        return Response(attr.errors, status=status.HTTP_400_BAD_REQUEST)

class ObjectDBViewSet(TypeclassViewSetMixin, ModelViewSet):
    """
    The Object is the parent for all in-game entities that have a location
    (rooms, exits, characters etc).

    """

    # An example of a basic viewset for all ObjectDB instances. It declares the
    # serializer to use for both retrieving and changing/creating/deleting
    # instances. Serializers are similar to django forms, used for the
    # transmitting of data (typically json).

    serializer_class = serializers.ObjectDBSerializer
    queryset = ObjectDB.objects.all()
    filterset_class = filters.ObjectDBFilterSet
    list_serializer_class = serializers.ObjectListSerializer

class CharacterViewSet(ObjectDBViewSet):
    """
    Characters are a type of Object commonly used as player avatars in-game.

    """

    queryset = DefaultCharacter.objects.all_family()

class RoomViewSet(ObjectDBViewSet):
    """
    Rooms indicate discrete locations in-game.

    """

    queryset = DefaultRoom.objects.all_family()

class ExitViewSet(ObjectDBViewSet):
    """
    Exits are objects with a destination and allows for traversing from one
    location to another.

    """

    queryset = DefaultExit.objects.all_family()

class AccountDBViewSet(TypeclassViewSetMixin, ModelViewSet):
    """
    Accounts represent the players connected to the game

    """

    serializer_class = serializers.AccountSerializer
    queryset = AccountDB.objects.all()
    filterset_class = filters.AccountDBFilterSet
    list_serializer_class = serializers.AccountListSerializer

class ScriptDBViewSet(TypeclassViewSetMixin, ModelViewSet):
    """
    Scripts are meta-objects for storing system data, running timers etc. They
    have no in-game existence.

    """

    serializer_class = serializers.ScriptDBSerializer
    queryset = ScriptDB.objects.all()
    filterset_class = filters.ScriptDBFilterSet
    list_serializer_class = serializers.ScriptListSerializer

class HelpViewSet(GeneralViewSetMixin, ModelViewSet):
    """
    Database-stored help entries.
    Note that command auto-help and file-based help entries are not accessible this way.

    """

    serializer_class = serializers.HelpSerializer
    queryset = HelpEntry.objects.all()
    filterset_class = filters.HelpFilterSet
    list_serializer_class = serializers.HelpListSerializer

"""
The main QuerySet implementation. This provides the public API for the ORM.
"""

import operator
from itertools import chain, islice

from django.core import exceptions
    DJANGO_VERSION_PICKLE_KEY,
    IntegrityError,
    router,
)
from django.db.models import AutoField, DateField, DateTimeField, Field, sql
from django.db.models.constants import LOOKUP_SEP, OnConflict
from django.db.models.deletion import Collector
from django.db.models.expressions import Case, F, Value, When
from django.db.models.functions import Cast, Trunc
from django.db.models.query_utils import FilteredRelation, Q
from django.db.models.sql.constants import CURSOR, GET_ITERATOR_CHUNK_SIZE
from django.db.models.utils import (
    AltersData,
    create_namedtuple_class,
    resolve_callables,
)
from django.utils.deprecation import RemovedInDjango50Warning
from django.utils.functional import cached_property, partition

# The maximum number of results to fetch in a get() query.
MAX_GET_RESULTS = 21

# The maximum number of items to display in a QuerySet.__repr__
REPR_OUTPUT_SIZE = 20

class BaseIterable:
        self, queryset, chunked_fetch=False, chunk_size=GET_ITERATOR_CHUNK_SIZE
    ):
        self.queryset = queryset
        self.chunked_fetch = chunked_fetch
        self.chunk_size = chunk_size

    async def _async_generator(self):
        # Generators don't actually start running until the first time you call
        # next() on them, so make the generator object in the async thread and
        # then repeatedly dispatch to it in a sync thread.
        sync_generator = self.__iter__()

        def next_slice(gen):
            return list(islice(gen, self.chunk_size))

            chunk = await sync_to_async(next_slice)(sync_generator)
            for item in chunk:
                yield item
            if len(chunk) < self.chunk_size:
                break

    # __aiter__() is a *synchronous* method that has to then return an
    # *asynchronous* iterator/generator. Thus, nest an async generator inside
    # it.
    # This is a generic iterable converter for now, and is going to suffer a
    # performance penalty on large sets of items due to the cost of crossing
    # over the sync barrier for each chunk. Custom __aiter__() methods should
    # be added to each Iterable subclass, but that needs some work in the
    # Compiler first.
    def __aiter__(self):
        return self._async_generator()

class ModelIterable(BaseIterable):
    """Iterable that yields a model instance for each row."""

        queryset = self.queryset
        db = queryset.db
        compiler = queryset.query.get_compiler(using=db)
        # Execute the query. This will also fill compiler.select, klass_info,
        # and annotations.
        results = compiler.execute_sql(
            chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size
        )
        select, klass_info, annotation_col_map = (
            compiler.select,
            compiler.klass_info,
            compiler.annotation_col_map,
        )
        model_cls = klass_info["model"]
        select_fields = klass_info["select_fields"]
        model_fields_start, model_fields_end = select_fields[0], select_fields[-1] + 1
        init_list = [
            f[0].target.attname for f in select[model_fields_start:model_fields_end]
        ]
        related_populators = get_related_populators(klass_info, select, db)
        known_related_objects = [
            (
                field,
                related_objs,
                operator.attrgetter(
                    *[
                        field.attname
                        if from_field == "self"
                        else queryset.model._meta.get_field(from_field).attname
                        for from_field in field.from_fields
                    ]
                ),
            )
            for field, related_objs in queryset._known_related_objects.items()
        ]
        for row in compiler.results_iter(results):
            obj = model_cls.from_db(
                db, init_list, row[model_fields_start:model_fields_end]
            )
            for rel_populator in related_populators:
                rel_populator.populate(row, obj)
            if annotation_col_map:
                for attr_name, col_pos in annotation_col_map.items():
                    setattr(obj, attr_name, row[col_pos])

            # Add the known related objects to the model.
            for field, rel_objs, rel_getter in known_related_objects:
                # Avoid overwriting objects loaded by, e.g., select_related().
                if field.is_cached(obj):
                    continue
                rel_obj_id = rel_getter(obj)
                try:
                    rel_obj = rel_objs[rel_obj_id]
                    pass  # May happen in qs1 | qs2 scenarios.
                else:
                    setattr(obj, field.name, rel_obj)

            yield obj

class RawModelIterable(BaseIterable):
    """
    Iterable that yields a model instance for each row from a raw queryset.
    """

        # Cache some things for performance reasons outside the loop.
        db = self.queryset.db
        query = self.queryset.query
        connection = connections[db]
        compiler = connection.ops.compiler("SQLCompiler")(query, connection, db)
        query_iterator = iter(query)

        try:
            (
                model_init_names,
                model_init_pos,
                annotation_fields,
            ) = self.queryset.resolve_model_init_order()
            model_cls = self.queryset.model
            if model_cls._meta.pk.attname not in model_init_names:
                raise exceptions.FieldDoesNotExist(
                    "Raw query must include the primary key"
                )
            fields = [self.queryset.model_fields.get(c) for c in self.queryset.columns]
            converters = compiler.get_converters(
                [f.get_col(f.model._meta.db_table) if f else None for f in fields]
            )
            if converters:
                query_iterator = compiler.apply_converters(query_iterator, converters)
            for values in query_iterator:
                # Associate fields to values
                model_init_values = [values[pos] for pos in model_init_pos]
                instance = model_cls.from_db(db, model_init_names, model_init_values)
                if annotation_fields:
                    for column, pos in annotation_fields:
                        setattr(instance, column, values[pos])
                yield instance
        finally:
            # Done iterating the Query. If it has its own cursor, close it.
            if hasattr(query, "cursor") and query.cursor:
                query.cursor.close()

class ValuesIterable(BaseIterable):
    """
    Iterable returned by QuerySet.values() that yields a dict for each row.
    """

        query = queryset.query
        compiler = query.get_compiler(queryset.db)

        # extra(select=...) cols are always at the start of the row.
        names = [
            *query.extra_select,
            *query.values_select,
            *query.annotation_select,
        ]
        indexes = range(len(names))
        for row in compiler.results_iter(
        ):
            yield {names[i]: row[i] for i in indexes}

class ValuesListIterable(BaseIterable):
    """
    Iterable returned by QuerySet.values_list(flat=False) that yields a tuple
    for each row.
    """

        if queryset._fields:
            # extra(select=...) cols are always at the start of the row.
            names = [
                *query.extra_select,
                *query.values_select,
                *query.annotation_select,
            ]
            fields = [
                *queryset._fields,
                *(f for f in query.annotation_select if f not in queryset._fields),
            ]
            if fields != names:
                # Reorder according to fields.
                index_map = {name: idx for idx, name in enumerate(names)}
                rowfactory = operator.itemgetter(*[index_map[f] for f in fields])
                return map(
                    rowfactory,
                    compiler.results_iter(
                        chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size
                    ),
                )
        return compiler.results_iter(
            tuple_expected=True,
            chunked_fetch=self.chunked_fetch,
            chunk_size=self.chunk_size,
        )

class NamedValuesListIterable(ValuesListIterable):
    """
    Iterable returned by QuerySet.values_list(named=True) that yields a
    namedtuple for each row.
    """

            names = queryset._fields
        else:
            query = queryset.query
            names = [
            ]
        tuple_class = create_namedtuple_class(*names)
        new = tuple.__new__
        for row in super().__iter__():
            yield new(tuple_class, row)

class FlatValuesListIterable(BaseIterable):
    """
    Iterable returned by QuerySet.values_list(flat=True) that yields single
    values.
    """

        compiler = queryset.query.get_compiler(queryset.db)
        ):
            yield row[0]

class QuerySet(AltersData):
    """Represent a lazy database lookup for a set of objects."""

    def __init__(self, model=None, query=None, using=None, hints=None):
        self._db = using
        self._hints = hints or {}
        self._query = query or sql.Query(self.model)
        self._result_cache = None
        self._sticky_filter = False
        self._for_write = False
        self._prefetch_related_lookups = ()
        self._prefetch_done = False
        self._known_related_objects = {}  # {rel_field: {pk: rel_obj}}
        self._iterable_class = ModelIterable
        self._fields = None
        self._defer_next_filter = False
        self._deferred_filter = None

    @property
    def query(self):
        if self._deferred_filter:
            negate, args, kwargs = self._deferred_filter
            self._filter_or_exclude_inplace(negate, args, kwargs)
            self._deferred_filter = None
        return self._query

    @query.setter
    def query(self, value):
        if value.values_select:
            self._iterable_class = ValuesIterable
        self._query = value

    def as_manager(cls):
        # Address the circular dependency between `Queryset` and `Manager`.
        from django.db.models.manager import Manager

        manager = Manager.from_queryset(cls)()
        manager._built_with_as_manager = True
        return manager

    as_manager.queryset_only = True
    as_manager = classmethod(as_manager)

    ########################
    # PYTHON MAGIC METHODS #

    def __deepcopy__(self, memo):
        """Don't populate the QuerySet's cache."""
        obj = self.__class__()
        for k, v in self.__dict__.items():
            if k == "_result_cache":
                obj.__dict__[k] = None
            else:
                obj.__dict__[k] = copy.deepcopy(v, memo)
        return obj

    def __getstate__(self):
        # Force the cache to be fully populated.
        self._fetch_all()
        return {**self.__dict__, DJANGO_VERSION_PICKLE_KEY: django.__version__}

        pickled_version = state.get(DJANGO_VERSION_PICKLE_KEY)
        if pickled_version:
            if pickled_version != django.__version__:
                warnings.warn(
                    "Pickled queryset instance's Django version %s does not "
                    "match the current version %s."
                    % (pickled_version, django.__version__),
                    RuntimeWarning,
                    stacklevel=2,
                )
        else:
                "Pickled queryset instance's Django version is not specified.",
                RuntimeWarning,
                stacklevel=2,
            )
        self.__dict__.update(state)

        data = list(self[: REPR_OUTPUT_SIZE + 1])
        if len(data) > REPR_OUTPUT_SIZE:
            data[-1] = "...(remaining elements truncated)..."
        return "<%s %r>" % (self.__class__.__name__, data)

        return len(self._result_cache)

        """
        The queryset iterator protocol uses three nested iterators in the
        default case:
            1. sql.compiler.execute_sql()
               - Returns 100 rows at time (constants.GET_ITERATOR_CHUNK_SIZE)
                 using cursor.fetchmany(). This part is responsible for
                 doing some column masking, and returning the rows in chunks.
            2. sql.compiler.results_iter()
               - Returns one row at time. At this point the rows are still just
                 tuples. In some cases the return values are converted to
                 Python values at this location.
            3. self.iterator()
               - Responsible for turning the rows into model objects.
        """
        return iter(self._result_cache)

        # Remember, __aiter__ itself is synchronous, it's the thing it returns
        # that is async!
        async def generator():
            await sync_to_async(self._fetch_all)()
            for item in self._result_cache:
                yield item

        return generator()

        return bool(self._result_cache)

    def __getitem__(self, k):
        """Retrieve an item or slice from the set of results."""
        if not isinstance(k, (int, slice)):
                "QuerySet indices must be integers or slices, not %s."
                % type(k).__name__
            )
        if (isinstance(k, int) and k < 0) or (
            isinstance(k, slice)
            and (
                (k.start is not None and k.start < 0)
                or (k.stop is not None and k.stop < 0)
            )
        ):
            raise ValueError("Negative indexing is not supported.")

        if self._result_cache is not None:
            return self._result_cache[k]

        if isinstance(k, slice):
            qs = self._chain()
            if k.start is not None:
                start = int(k.start)
            else:
                start = None
            if k.stop is not None:
                stop = int(k.stop)
            else:
                stop = None
            qs.query.set_limits(start, stop)
            return list(qs)[:: k.step] if k.step else qs

        qs = self._chain()
        qs.query.set_limits(k, k + 1)
        qs._fetch_all()
        return qs._result_cache[0]

        return cls

        self._check_operator_queryset(other, "&")
        self._merge_sanity_check(other)
        if isinstance(other, EmptyQuerySet):
            return other
        if isinstance(self, EmptyQuerySet):
        combined = self._chain()
        combined._merge_known_related_objects(other)
        combined.query.combine(other.query, sql.AND)
        return combined

        self._check_operator_queryset(other, "|")
        query = (
            self
            if self.query.can_filter()
            else self.model._base_manager.filter(pk__in=self.values("pk"))
        )
        combined = query._chain()
        if not other.query.can_filter():
            other = other.model._base_manager.filter(pk__in=other.values("pk"))
        combined.query.combine(other.query, sql.OR)

        self._check_operator_queryset(other, "^")
        query = (
            self
        )
        combined.query.combine(other.query, sql.XOR)

    ####################################
    # METHODS THAT DO DATABASE QUERIES #

    def _iterator(self, use_chunked_fetch, chunk_size):
        iterable = self._iterable_class(
            self,
            chunked_fetch=use_chunked_fetch,
            chunk_size=chunk_size or 2000,
        )
        if not self._prefetch_related_lookups or chunk_size is None:
            yield from iterable
            return

        iterator = iter(iterable)
        while results := list(islice(iterator, chunk_size)):
            prefetch_related_objects(results, *self._prefetch_related_lookups)
            yield from results

    def iterator(self, chunk_size=None):
        """
        An iterator over the results from applying this QuerySet to the
        database. chunk_size must be provided for QuerySets that prefetch
        related objects. Otherwise, a default chunk_size of 2000 is supplied.
        """
        if chunk_size is None:
            if self._prefetch_related_lookups:
                # When the deprecation ends, replace with:
                # raise ValueError(
                #     'chunk_size must be provided when using '
                #     'QuerySet.iterator() after prefetch_related().'
                # )
                    "Using QuerySet.iterator() after prefetch_related() "
                    "without specifying chunk_size is deprecated.",
                    category=RemovedInDjango50Warning,
                )
        elif chunk_size <= 0:
            raise ValueError("Chunk size must be strictly positive.")
        use_chunked_fetch = not connections[self.db].settings_dict.get(
            "DISABLE_SERVER_SIDE_CURSORS"
        )
        return self._iterator(use_chunked_fetch, chunk_size)

    async def aiterator(self, chunk_size=2000):
        """
        An asynchronous iterator over the results from applying this QuerySet
        to the database.
        """
        if self._prefetch_related_lookups:
                "Using QuerySet.aiterator() after prefetch_related() is not supported."
            )
        if chunk_size <= 0:
        )
        async for item in self._iterable_class(
            self, chunked_fetch=use_chunked_fetch, chunk_size=chunk_size
        ):
            yield item

    def aggregate(self, *args, **kwargs):
        """
        Return a dictionary containing the calculations (aggregation)
        over the current queryset.

        If args is present the expression is passed as a kwarg using
        the Aggregate object's default alias.
        """
        if self.query.distinct_fields:
            raise NotImplementedError("aggregate() + distinct(fields) not implemented.")
        self._validate_values_are_expressions(
            (*args, *kwargs.values()), method_name="aggregate"
        )
            # The default_alias property raises TypeError if default_alias
            # can't be set automatically or AttributeError if it isn't an
            # attribute.
            try:
                arg.default_alias
            except (AttributeError, TypeError):
                raise TypeError("Complex aggregates require an alias")
            kwargs[arg.default_alias] = arg

        return self.query.chain().get_aggregation(self.db, kwargs)

    async def aaggregate(self, *args, **kwargs):
        return await sync_to_async(self.aggregate)(*args, **kwargs)

        """
        Perform a SELECT COUNT() and return the number of records as an
        integer.

        If the QuerySet is already fully cached, return the length of the
        cached results set to avoid multiple SELECT COUNT(*) calls.
        """
            return len(self._result_cache)

        return self.query.get_count(using=self.db)

    async def acount(self):
        return await sync_to_async(self.count)()

        """
        Perform the query and return a single object matching the given
        keyword arguments.
        """
        if self.query.combinator and (args or kwargs):
                "Calling QuerySet.get(...) with filters after %s() is not "
                "supported." % self.query.combinator
            )
        clone = self._chain() if self.query.combinator else self.filter(*args, **kwargs)
        if self.query.can_filter() and not self.query.distinct_fields:
            clone = clone.order_by()
        limit = None
        if (
            not clone.query.select_for_update
            or connections[clone.db].features.supports_select_for_update_with_limit
        ):
            limit = MAX_GET_RESULTS
            clone.query.set_limits(high=limit)
        num = len(clone)
        if num == 1:
            return clone._result_cache[0]
        if not num:
                "%s matching query does not exist." % self.model._meta.object_name
            )
        raise self.model.MultipleObjectsReturned(
            "get() returned more than one %s -- it returned %s!"
            % (
                self.model._meta.object_name,
                num if not limit or num < limit else "more than %s" % (limit - 1),
            )
        )

    async def aget(self, *args, **kwargs):
        return await sync_to_async(self.get)(*args, **kwargs)

    def create(self, **kwargs):
        """
        Create a new object with the given kwargs, saving it to the database
        and returning the created object.
        """
        obj = self.model(**kwargs)
        self._for_write = True
        obj.save(force_insert=True, using=self.db)
        return obj

    async def acreate(self, **kwargs):
        return await sync_to_async(self.create)(**kwargs)

    def _prepare_for_bulk_create(self, objs):
            if obj.pk is None:
                # Populate new PK values.
                obj.pk = obj._meta.pk.get_pk_value_on_save(obj)
            obj._prepare_related_fields_for_save(operation_name="bulk_create")

    def _check_bulk_create_options(
        self, ignore_conflicts, update_conflicts, update_fields, unique_fields
    ):
        if ignore_conflicts and update_conflicts:
                "ignore_conflicts and update_conflicts are mutually exclusive."
            )
        db_features = connections[self.db].features
        if ignore_conflicts:
            if not db_features.supports_ignore_conflicts:
                raise NotSupportedError(
                    "This database backend does not support ignoring conflicts."
                )
            return OnConflict.IGNORE
        elif update_conflicts:
            if not db_features.supports_update_conflicts:
                    "This database backend does not support updating conflicts."
                )
            if not update_fields:
                    "Fields that will be updated when a row insertion fails "
                    "on conflicts must be provided."
                )
            if unique_fields and not db_features.supports_update_conflicts_with_target:
                    "This database backend does not support updating "
                    "conflicts with specifying unique fields that can trigger "
                    "the upsert."
                )
            if not unique_fields and db_features.supports_update_conflicts_with_target:
                    "Unique fields that can trigger the upsert must be provided."
                )
            # Updating primary keys and non-concrete fields is forbidden.
            if any(not f.concrete or f.many_to_many for f in update_fields):
                    "bulk_create() can only be used with concrete fields in "
                    "update_fields."
                )
            if any(f.primary_key for f in update_fields):
                    "bulk_create() cannot be used with primary keys in "
                )
            if unique_fields:
                if any(not f.concrete or f.many_to_many for f in unique_fields):
                        "bulk_create() can only be used with concrete fields "
                        "in unique_fields."
                    )
            return OnConflict.UPDATE

    def bulk_create(
        self,
        objs,
        batch_size=None,
        ignore_conflicts=False,
        update_conflicts=False,
        update_fields=None,
        unique_fields=None,
    ):
        """
        Insert each of the instances into the database. Do *not* call
        save() on each of the instances, do not send any pre/post_save
        signals, and do not set the primary key attribute if it is an
        autoincrement field (except if features.can_return_rows_from_bulk_insert=True).
        Multi-table models are not supported.
        """
        # When you bulk insert you don't get the primary keys back (if it's an
        # autoincrement, except if can_return_rows_from_bulk_insert=True), so
        # you can't insert into the child tables which references this. There
        # are two workarounds:
        # 1) This could be implemented if you didn't have an autoincrement pk
        # 2) You could do it by doing O(n) normal inserts into the parent
        #    tables to get the primary keys back and then doing a single bulk
        #    insert into the childmost table.
        # We currently set the primary keys on the objects when using
        # PostgreSQL via the RETURNING ID clause. It should be possible for
        # Oracle as well, but the semantics for extracting the primary keys is
        # trickier so it's not done yet.
        if batch_size is not None and batch_size <= 0:
            raise ValueError("Batch size must be a positive integer.")
        # Check that the parents share the same concrete model with the our
        # model to detect the inheritance pattern ConcreteGrandParent ->
        # MultiTableParent -> ProxyChild. Simply checking self.model._meta.proxy
        # would not identify that case as involving multiple tables.
        for parent in self.model._meta.get_parent_list():
            if parent._meta.concrete_model is not self.model._meta.concrete_model:
                raise ValueError("Can't bulk create a multi-table inherited model")
        if not objs:
            return objs
        opts = self.model._meta
        if unique_fields:
            # Primary key is allowed in unique_fields.
            unique_fields = [
                self.model._meta.get_field(opts.pk.name if name == "pk" else name)
                for name in unique_fields
            ]
        if update_fields:
            update_fields = [self.model._meta.get_field(name) for name in update_fields]
        on_conflict = self._check_bulk_create_options(
            ignore_conflicts,
            update_conflicts,
            update_fields,
            unique_fields,
        )
        fields = opts.concrete_fields
        objs = list(objs)
        self._prepare_for_bulk_create(objs)
        with transaction.atomic(using=self.db, savepoint=False):
            objs_with_pk, objs_without_pk = partition(lambda o: o.pk is None, objs)
            if objs_with_pk:
                returned_columns = self._batched_insert(
                    objs_with_pk,
                    fields,
                    batch_size,
                    on_conflict=on_conflict,
                    update_fields=update_fields,
                    unique_fields=unique_fields,
                )
                for obj_with_pk, results in zip(objs_with_pk, returned_columns):
                    for result, field in zip(results, opts.db_returning_fields):
                        if field != opts.pk:
                            setattr(obj_with_pk, field.attname, result)
                for obj_with_pk in objs_with_pk:
                    obj_with_pk._state.adding = False
                    obj_with_pk._state.db = self.db
            if objs_without_pk:
                fields = [f for f in fields if not isinstance(f, AutoField)]
                    objs_without_pk,
                    fields,
                )
                connection = connections[self.db]
                if (
                    connection.features.can_return_rows_from_bulk_insert
                    and on_conflict is None
                ):
                    assert len(returned_columns) == len(objs_without_pk)
                for obj_without_pk, results in zip(objs_without_pk, returned_columns):
                        setattr(obj_without_pk, field.attname, result)
                    obj_without_pk._state.adding = False
                    obj_without_pk._state.db = self.db

    async def abulk_create(
        self,
        objs,
    ):
        return await sync_to_async(self.bulk_create)(
            objs=objs,
            batch_size=batch_size,
            ignore_conflicts=ignore_conflicts,
            update_conflicts=update_conflicts,
            update_fields=update_fields,
            unique_fields=unique_fields,
        )

    def bulk_update(self, objs, fields, batch_size=None):
        """
        Update the given fields in each of the given objects in the database.
        """
        if not fields:
            raise ValueError("Field names must be given to bulk_update().")
        objs = tuple(objs)
        if any(obj.pk is None for obj in objs):
            raise ValueError("All bulk_update() objects must have a primary key set.")
        fields = [self.model._meta.get_field(name) for name in fields]
        if any(not f.concrete or f.many_to_many for f in fields):
            raise ValueError("bulk_update() can only be used with concrete fields.")
        if any(f.primary_key for f in fields):
            raise ValueError("bulk_update() cannot be used with primary key fields.")
            return 0
            obj._prepare_related_fields_for_save(
                operation_name="bulk_update", fields=fields
            )
        # PK is used twice in the resulting update query, once in the filter
        # and once in the WHEN. Each field will also have one CAST.
        connection = connections[self.db]
        max_batch_size = connection.ops.bulk_batch_size(["pk", "pk"] + fields, objs)
        batch_size = min(batch_size, max_batch_size) if batch_size else max_batch_size
        requires_casting = connection.features.requires_casted_case_in_updates
        batches = (objs[i : i + batch_size] for i in range(0, len(objs), batch_size))
        updates = []
        for batch_objs in batches:
            update_kwargs = {}
            for field in fields:
                when_statements = []
                for obj in batch_objs:
                    attr = getattr(obj, field.attname)
                    if not hasattr(attr, "resolve_expression"):
                        attr = Value(attr, output_field=field)
                    when_statements.append(When(pk=obj.pk, then=attr))
                case_statement = Case(*when_statements, output_field=field)
                if requires_casting:
                    case_statement = Cast(case_statement, output_field=field)
                update_kwargs[field.attname] = case_statement
            updates.append(([obj.pk for obj in batch_objs], update_kwargs))
        rows_updated = 0
        queryset = self.using(self.db)
            for pks, update_kwargs in updates:
                rows_updated += queryset.filter(pk__in=pks).update(**update_kwargs)
        return rows_updated

    bulk_update.alters_data = True

    async def abulk_update(self, objs, fields, batch_size=None):
        return await sync_to_async(self.bulk_update)(
            fields=fields,
        )

    abulk_update.alters_data = True

    def get_or_create(self, defaults=None, **kwargs):
        """
        Look up an object with the given kwargs, creating one if necessary.
        Return a tuple of (object, created), where created is a boolean
        specifying whether an object was created.
        """
        # The get() needs to be targeted at the write database in order
        # to avoid potential transaction consistency problems.
        try:
            return self.get(**kwargs), False
            params = self._extract_model_params(defaults, **kwargs)
            # Try to create an object using passed params.
            try:
                with transaction.atomic(using=self.db):
                    params = dict(resolve_callables(params))
                    return self.create(**params), True
            except IntegrityError:
                try:
                    return self.get(**kwargs), False
                except self.model.DoesNotExist:
                    pass
                raise

    async def aget_or_create(self, defaults=None, **kwargs):
        return await sync_to_async(self.get_or_create)(
            defaults=defaults,
            **kwargs,
        )

    def update_or_create(self, defaults=None, **kwargs):
        """
        Look up an object with the given kwargs, updating one with defaults
        if it exists, otherwise create a new one.
        Return a tuple (object, created), where created is a boolean
        """
        defaults = defaults or {}
        with transaction.atomic(using=self.db):
            # Lock the row so that a concurrent update is blocked until
            # update_or_create() has performed its save.
            obj, created = self.select_for_update().get_or_create(defaults, **kwargs)
                return obj, created
            for k, v in resolve_callables(defaults):
                setattr(obj, k, v)

            update_fields = set(defaults)
            concrete_field_names = self.model._meta._non_pk_concrete_field_names
            # update_fields does not support non-concrete fields.
            if concrete_field_names.issuperset(update_fields):
                # Add fields which are set on pre_save(), e.g. auto_now fields.
                # This is to maintain backward compatibility as these fields
                # are not updated unless explicitly specified in the
                # update_fields list.
                for field in self.model._meta.local_concrete_fields:
                    if not (
                        field.primary_key or field.__class__.pre_save is Field.pre_save
                    ):
                        update_fields.add(field.name)
                        if field.name != field.attname:
                            update_fields.add(field.attname)
                obj.save(using=self.db, update_fields=update_fields)
            else:
                obj.save(using=self.db)
        return obj, False

    async def aupdate_or_create(self, defaults=None, **kwargs):
        return await sync_to_async(self.update_or_create)(
            **kwargs,
        )

    def _extract_model_params(self, defaults, **kwargs):
        """
        Prepare `params` for creating a model instance based on the given
        kwargs; for use by get_or_create().
        """
        params = {k: v for k, v in kwargs.items() if LOOKUP_SEP not in k}
        params.update(defaults)
        property_names = self.model._meta._property_names
        invalid_params = []
        for param in params:
            try:
                self.model._meta.get_field(param)
            except exceptions.FieldDoesNotExist:
                # It's okay to use a model's property if it has a setter.
                if not (param in property_names and getattr(self.model, param).fset):
                    invalid_params.append(param)
        if invalid_params:
            raise exceptions.FieldError(
                "Invalid field name(s) for model %s: '%s'."
                % (
                    self.model._meta.object_name,
                    "', '".join(sorted(invalid_params)),
                )
            )
        return params

    def _earliest(self, *fields):
        """
        Return the earliest object according to fields (if given) or by the
        model's Meta.get_latest_by.
        """
        if fields:
            order_by = fields
        else:
            order_by = getattr(self.model._meta, "get_latest_by")
            if order_by and not isinstance(order_by, (tuple, list)):
                order_by = (order_by,)
        if order_by is None:
                "earliest() and latest() require either fields as positional "
                "arguments or 'get_latest_by' in the model's Meta."
            )
        obj = self._chain()
        obj.query.set_limits(high=1)
        obj.query.clear_ordering(force=True)
        obj.query.add_ordering(*order_by)
        return obj.get()

    def earliest(self, *fields):
        if self.query.is_sliced:
            raise TypeError("Cannot change a query once a slice has been taken.")
        return self._earliest(*fields)

    async def aearliest(self, *fields):
        return await sync_to_async(self.earliest)(*fields)

    def latest(self, *fields):
        """
        Return the latest object according to fields (if given) or by the
        """
        return self.reverse()._earliest(*fields)

    async def alatest(self, *fields):
        return await sync_to_async(self.latest)(*fields)

        """Return the first object of a query or None if no match is found."""
        if self.ordered:
            queryset = self
        else:
            self._check_ordering_first_last_queryset_aggregation(method="first")
            queryset = self.order_by("pk")
        for obj in queryset[:1]:
            return obj

    async def afirst(self):
        return await sync_to_async(self.first)()

        """Return the last object of a query or None if no match is found."""
            queryset = self.reverse()
        else:
            self._check_ordering_first_last_queryset_aggregation(method="last")
            queryset = self.order_by("-pk")
            return obj

    async def alast(self):
        return await sync_to_async(self.last)()

    def in_bulk(self, id_list=None, *, field_name="pk"):
        """
        Return a dictionary mapping each of the given IDs to the object with
        that ID. If `id_list` isn't provided, evaluate the entire QuerySet.
        """
            raise TypeError("Cannot use 'limit' or 'offset' with in_bulk().")
        unique_fields = [
            constraint.fields[0]
            for constraint in opts.total_unique_constraints
            if len(constraint.fields) == 1
        ]
        if (
            field_name != "pk"
            and not opts.get_field(field_name).unique
            and field_name not in unique_fields
            and self.query.distinct_fields != (field_name,)
        ):
                "in_bulk()'s field_name must be a unique field but %r isn't."
                % field_name
            )
        if id_list is not None:
            if not id_list:
                return {}
            filter_key = "{}__in".format(field_name)
            batch_size = connections[self.db].features.max_query_params
            id_list = tuple(id_list)
            # If the database has a limit on the number of query parameters
            # (e.g. SQLite), retrieve objects in batches if necessary.
            if batch_size and batch_size < len(id_list):
                qs = ()
                for offset in range(0, len(id_list), batch_size):
                    batch = id_list[offset : offset + batch_size]
                    qs += tuple(self.filter(**{filter_key: batch}).order_by())
            else:
                qs = self.filter(**{filter_key: id_list}).order_by()
        else:
        return {getattr(obj, field_name): obj for obj in qs}

    async def ain_bulk(self, id_list=None, *, field_name="pk"):
        return await sync_to_async(self.in_bulk)(
            id_list=id_list,
            field_name=field_name,
        )

        """Delete the records in the current QuerySet."""
        self._not_support_combined_queries("delete")
            raise TypeError("Cannot use 'limit' or 'offset' with delete().")
        if self.query.distinct or self.query.distinct_fields:
            raise TypeError("Cannot call delete() after .distinct().")
        if self._fields is not None:
            raise TypeError("Cannot call delete() after .values() or .values_list()")

        del_query = self._chain()

        # The delete is actually 2 queries - one to find related objects,
        # and one to delete. Make sure that the discovery of related
        # objects is performed on the same database as the deletion.
        del_query._for_write = True

        # Disable non-supported fields.
        del_query.query.select_for_update = False
        del_query.query.select_related = False
        del_query.query.clear_ordering(force=True)

        collector = Collector(using=del_query.db, origin=self)
        collector.collect(del_query)
        deleted, _rows_count = collector.delete()

        # Clear the result cache, in case this QuerySet gets reused.
        return deleted, _rows_count

    delete.alters_data = True
    delete.queryset_only = True

    async def adelete(self):
        return await sync_to_async(self.delete)()

    adelete.alters_data = True
    adelete.queryset_only = True

    def _raw_delete(self, using):
        """
        Delete objects found from the given queryset in single direct SQL
        query. No signals are sent and there is no protection for cascades.
        """
        query = self.query.clone()
        query.__class__ = sql.DeleteQuery
        cursor = query.get_compiler(using).execute_sql(CURSOR)
        if cursor:
            with cursor:
                return cursor.rowcount
        return 0

    _raw_delete.alters_data = True

    def update(self, **kwargs):
        """
        Update all elements in the current QuerySet, setting all the given
        fields to the appropriate values.
        """
        self._not_support_combined_queries("update")
            raise TypeError("Cannot update a query once a slice has been taken.")
        query = self.query.chain(sql.UpdateQuery)
        query.add_update_values(kwargs)

        # Inline annotations in order_by(), if possible.
        new_order_by = []
        for col in query.order_by:
            if annotation := query.annotations.get(col):
                if getattr(annotation, "contains_aggregate", False):
                    raise exceptions.FieldError(
                        f"Cannot update when ordering by an aggregate: {annotation}"
                    )
                new_order_by.append(annotation)
            else:
                new_order_by.append(col)
        query.order_by = tuple(new_order_by)

        # Clear any annotations so that they won't be present in subqueries.
        query.annotations = {}
        with transaction.mark_for_rollback_on_error(using=self.db):
            rows = query.get_compiler(self.db).execute_sql(CURSOR)

    update.alters_data = True

    async def aupdate(self, **kwargs):
        return await sync_to_async(self.update)(**kwargs)

    aupdate.alters_data = True

    def _update(self, values):
        """
        A version of update() that accepts field objects instead of field names.
        Used primarily for model saving and not intended for use by general
        code (it requires too much poking around at model internals to be
        useful at that level).
        """
        query.add_update_fields(values)
        return query.get_compiler(self.db).execute_sql(CURSOR)

    _update.alters_data = True
    _update.queryset_only = False

        """
        Return True if the QuerySet would have any results, False otherwise.
        """
        if self._result_cache is None:
            return self.query.has_results(using=self.db)

    async def aexists(self):
        return await sync_to_async(self.exists)()

    def contains(self, obj):
        """
        Return True if the QuerySet contains the provided obj,
        False otherwise.
        """
        self._not_support_combined_queries("contains")
                "Cannot call QuerySet.contains() after .values() or .values_list()."
            )
        try:
            if obj._meta.concrete_model != self.model._meta.concrete_model:
            raise TypeError("'obj' must be a model instance.")
        if obj.pk is None:
            raise ValueError("QuerySet.contains() cannot be used on unsaved objects.")
            return obj in self._result_cache
        return self.filter(pk=obj.pk).exists()

    async def acontains(self, obj):
        return await sync_to_async(self.contains)(obj=obj)

    def _prefetch_related_objects(self):
        # This method can only be called once the result cache has been filled.
        prefetch_related_objects(self._result_cache, *self._prefetch_related_lookups)
        self._prefetch_done = True

    def explain(self, *, format=None, **options):
        """
        Runs an EXPLAIN on the SQL query this QuerySet would perform, and
        returns the results.
        """
        return self.query.explain(using=self.db, format=format, **options)

    async def aexplain(self, *, format=None, **options):
        return await sync_to_async(self.explain)(format=format, **options)

    ##################################################
    # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS #

    def raw(self, raw_query, params=(), translations=None, using=None):
        if using is None:
            using = self.db
        qs = RawQuerySet(
            raw_query,
            model=self.model,
            params=params,
            translations=translations,
            using=using,
        )
        qs._prefetch_related_lookups = self._prefetch_related_lookups[:]
        return qs

    def _values(self, *fields, **expressions):
        clone = self._chain()
        if expressions:
            clone = clone.annotate(**expressions)
        clone._fields = fields
        clone.query.set_values(fields)

    def values(self, *fields, **expressions):
        fields += tuple(expressions)
        clone = self._values(*fields, **expressions)
        clone._iterable_class = ValuesIterable

    def values_list(self, *fields, flat=False, named=False):
        if flat and named:
            raise TypeError("'flat' and 'named' can't be used together.")
        if flat and len(fields) > 1:
                "'flat' is not valid when values_list is called with more than one "
                "field."
            )

        field_names = {f for f in fields if not hasattr(f, "resolve_expression")}
        _fields = []
        expressions = {}
        counter = 1
        for field in fields:
            if hasattr(field, "resolve_expression"):
                field_id_prefix = getattr(
                    field, "default_alias", field.__class__.__name__.lower()
                )
                    field_id = field_id_prefix + str(counter)
                    counter += 1
                    if field_id not in field_names:
                        break
                expressions[field_id] = field
                _fields.append(field_id)
            else:
                _fields.append(field)

        clone = self._values(*_fields, **expressions)
        clone._iterable_class = (
            NamedValuesListIterable
            if named
            else FlatValuesListIterable
            if flat
            else ValuesListIterable
        )

    def dates(self, field_name, kind, order="ASC"):
        """
        Return a list of date objects representing all available dates for
        the given field_name, scoped to 'kind'.
        """
        if kind not in ("year", "month", "week", "day"):
            raise ValueError("'kind' must be one of 'year', 'month', 'week', or 'day'.")
        if order not in ("ASC", "DESC"):
            raise ValueError("'order' must be either 'ASC' or 'DESC'.")
        return (
            self.annotate(
                datefield=Trunc(field_name, kind, output_field=DateField()),
                plain_field=F(field_name),
            )
            .values_list("datefield", flat=True)
            .filter(plain_field__isnull=False)
            .order_by(("-" if order == "DESC" else "") + "datefield")
        )

    # RemovedInDjango50Warning: when the deprecation ends, remove is_dst
    # argument.
    def datetimes(
        self, field_name, kind, order="ASC", tzinfo=None, is_dst=timezone.NOT_PASSED
    ):
        """
        Return a list of datetime objects representing all available
        datetimes for the given field_name, scoped to 'kind'.
        """
        if kind not in ("year", "month", "week", "day", "hour", "minute", "second"):
                "'kind' must be one of 'year', 'month', 'week', 'day', "
                "'hour', 'minute', or 'second'."
            )
            if tzinfo is None:
                tzinfo = timezone.get_current_timezone()
        else:
            tzinfo = None
        return (
                datetimefield=Trunc(
                    field_name,
                    kind,
                    output_field=DateTimeField(),
                    tzinfo=tzinfo,
                    is_dst=is_dst,
                ),
            )
            .values_list("datetimefield", flat=True)
            .order_by(("-" if order == "DESC" else "") + "datetimefield")
        )

    def none(self):
        """Return an empty QuerySet."""
        clone.query.set_empty()

    ##################################################################
    # PUBLIC METHODS THAT ALTER ATTRIBUTES AND RETURN A NEW QUERYSET #

        """
        Return a new QuerySet that is a copy of the current one. This allows a
        QuerySet to proxy for a model manager in some cases.
        """
        return self._chain()

        """
        Return a new QuerySet instance with the args ANDed to the existing
        set.
        """
        self._not_support_combined_queries("filter")
        return self._filter_or_exclude(False, args, kwargs)

    def exclude(self, *args, **kwargs):
        """
        Return a new QuerySet instance with NOT (args) ANDed to the existing
        set.
        """
        self._not_support_combined_queries("exclude")
        return self._filter_or_exclude(True, args, kwargs)

    def _filter_or_exclude(self, negate, args, kwargs):
        if (args or kwargs) and self.query.is_sliced:
            raise TypeError("Cannot filter a query once a slice has been taken.")
        if self._defer_next_filter:
            self._defer_next_filter = False
            clone._deferred_filter = negate, args, kwargs
        else:
            clone._filter_or_exclude_inplace(negate, args, kwargs)

    def _filter_or_exclude_inplace(self, negate, args, kwargs):
        if negate:
            self._query.add_q(~Q(*args, **kwargs))
        else:
            self._query.add_q(Q(*args, **kwargs))

    def complex_filter(self, filter_obj):
        """
        Return a new QuerySet instance with filter_obj added to the filters.

        filter_obj can be a Q object or a dictionary of keyword lookup
        arguments.

        This exists to support framework features such as 'limit_choices_to',
        and usually it will be more natural to use other methods.
        """
        if isinstance(filter_obj, Q):
            clone = self._chain()
            clone.query.add_q(filter_obj)
            return clone
        else:
            return self._filter_or_exclude(False, args=(), kwargs=filter_obj)

    def _combinator_query(self, combinator, *other_qs, all=False):
        # Clone the query to inherit the select list and everything
        # Clear limits and ordering so they can be reapplied
        clone.query.clear_ordering(force=True)
        clone.query.clear_limits()
        clone.query.combined_queries = (self.query,) + tuple(
            qs.query for qs in other_qs
        )
        clone.query.combinator = combinator
        clone.query.combinator_all = all

    def union(self, *other_qs, all=False):
        # If the query is an EmptyQuerySet, combine all nonempty querysets.
            qs = [q for q in other_qs if not isinstance(q, EmptyQuerySet)]
            if not qs:
            if len(qs) == 1:
                return qs[0]
            return qs[0]._combinator_query("union", *qs[1:], all=all)
        return self._combinator_query("union", *other_qs, all=all)

    def intersection(self, *other_qs):
        # If any query is an EmptyQuerySet, return it.
        for other in other_qs:
            if isinstance(other, EmptyQuerySet):
                return other
        return self._combinator_query("intersection", *other_qs)

    def difference(self, *other_qs):
        # If the query is an EmptyQuerySet, return it.
        return self._combinator_query("difference", *other_qs)

    def select_for_update(self, nowait=False, skip_locked=False, of=(), no_key=False):
        """
        Return a new QuerySet instance that will select objects with a
        FOR UPDATE lock.
        """
        if nowait and skip_locked:
            raise ValueError("The nowait option cannot be used with skip_locked.")
        obj._for_write = True
        obj.query.select_for_update = True
        obj.query.select_for_update_nowait = nowait
        obj.query.select_for_update_skip_locked = skip_locked
        obj.query.select_for_update_of = of
        obj.query.select_for_no_key_update = no_key
        return obj

    def select_related(self, *fields):
        """
        Return a new QuerySet instance that will select related objects.

        If fields are specified, they must be ForeignKey fields and only those
        related objects are included in the selection.

        If select_related(None) is called, clear the list.
        """
        self._not_support_combined_queries("select_related")
                "Cannot call select_related() after .values() or .values_list()"
            )

        if fields == (None,):
            obj.query.select_related = False
        elif fields:
            obj.query.add_select_related(fields)
        else:
            obj.query.select_related = True
        return obj

    def prefetch_related(self, *lookups):
        """
        Return a new QuerySet instance that will prefetch the specified
        Many-To-One and Many-To-Many related objects when the QuerySet is
        evaluated.

        When prefetch_related() is called more than once, append to the list of
        prefetch lookups. If prefetch_related(None) is called, clear the list.
        """
        self._not_support_combined_queries("prefetch_related")
        if lookups == (None,):
            clone._prefetch_related_lookups = ()
        else:
            for lookup in lookups:
                if isinstance(lookup, Prefetch):
                    lookup = lookup.prefetch_to
                lookup = lookup.split(LOOKUP_SEP, 1)[0]
                if lookup in self.query._filtered_relations:
                        "prefetch_related() is not supported with FilteredRelation."
                    )
            clone._prefetch_related_lookups = clone._prefetch_related_lookups + lookups

        """
        Return a query set in which the returned objects have been annotated
        with extra data or aggregations.
        """
        self._not_support_combined_queries("annotate")
        return self._annotate(args, kwargs, select=True)

    def alias(self, *args, **kwargs):
        """
        Return a query set with added aliases for extra data or aggregations.
        """
        self._not_support_combined_queries("alias")
        return self._annotate(args, kwargs, select=False)

    def _annotate(self, args, kwargs, select=True):
            args + tuple(kwargs.values()), method_name="annotate"
        )
        annotations = {}
            # The default_alias property may raise a TypeError.
            try:
                if arg.default_alias in kwargs:
                        "The named annotation '%s' conflicts with the "
                        "default name for another annotation." % arg.default_alias
                    )
                raise TypeError("Complex annotations require an alias")
            annotations[arg.default_alias] = arg
        annotations.update(kwargs)

        names = self._fields
        if names is None:
            names = set(
                chain.from_iterable(
                    (field.name, field.attname)
                    if hasattr(field, "attname")
                    else (field.name,)
                    for field in self.model._meta.get_fields()
                )
            )

        for alias, annotation in annotations.items():
            if alias in names:
                    "The annotation '%s' conflicts with a field on "
                    "the model." % alias
                )
            if isinstance(annotation, FilteredRelation):
                clone.query.add_filtered_relation(annotation, alias)
            else:
                clone.query.add_annotation(
                    annotation,
                    alias,
                    select=select,
                )
        for alias, annotation in clone.query.annotations.items():
            if alias in annotations and annotation.contains_aggregate:
                if clone._fields is None:
                    clone.query.group_by = True
                else:
                    clone.query.set_group_by()
                break

    def order_by(self, *field_names):
        """Return a new QuerySet instance with the ordering changed."""
            raise TypeError("Cannot reorder a query once a slice has been taken.")
        obj.query.clear_ordering(force=True, clear_default=False)
        obj.query.add_ordering(*field_names)
        return obj

    def distinct(self, *field_names):
        """
        Return a new QuerySet instance that will select only distinct results.
        """
        self._not_support_combined_queries("distinct")
                "Cannot create distinct fields once a slice has been taken."
            )
        obj.query.add_distinct_fields(*field_names)
        return obj

    def extra(
        self,
        select=None,
        where=None,
        params=None,
        tables=None,
        order_by=None,
        select_params=None,
    ):
        """Add extra SQL fragments to the query."""
        self._not_support_combined_queries("extra")
        clone.query.add_extra(select, select_params, where, params, tables, order_by)

        """Reverse the ordering of the QuerySet."""
            raise TypeError("Cannot reverse a query once a slice has been taken.")
        clone.query.standard_ordering = not clone.query.standard_ordering

    def defer(self, *fields):
        """
        Defer the loading of data for certain fields until they are accessed.
        Add the set of deferred fields to any existing set of deferred fields.
        The only exception to this is if None is passed in as the only
        parameter, in which case removal all deferrals.
        """
        self._not_support_combined_queries("defer")
            raise TypeError("Cannot call defer() after .values() or .values_list()")
            clone.query.clear_deferred_loading()
        else:
            clone.query.add_deferred_loading(fields)

    def only(self, *fields):
        """
        Essentially, the opposite of defer(). Only the fields passed into this
        method and that are not already specified as deferred are loaded
        immediately when the queryset is evaluated.
        """
        self._not_support_combined_queries("only")
            raise TypeError("Cannot call only() after .values() or .values_list()")
            # Can only pass None to defer(), not only(), as the rest option.
            # That won't stop people trying to do this, so let's be explicit.
            raise TypeError("Cannot pass None as an argument to only().")
            field = field.split(LOOKUP_SEP, 1)[0]
            if field in self.query._filtered_relations:
                raise ValueError("only() is not supported with FilteredRelation.")
        clone.query.add_immediate_loading(fields)

    def using(self, alias):
        """Select which database this QuerySet should execute against."""
        clone._db = alias

    ###################################
    # PUBLIC INTROSPECTION ATTRIBUTES #

    @property
    def ordered(self):
        """
        Return True if the QuerySet is ordered -- i.e. has an order_by()
        clause or a default ordering on the model (or is empty).
        """
        if self.query.extra_order_by or self.query.order_by:
        elif (
            self.query.default_ordering
            and self.query.get_meta().ordering
            and
            # A default ordering doesn't affect GROUP BY queries.
            not self.query.group_by
        ):
        else:

    @property
        """Return the database used if this query is executed now."""
        if self._for_write:
            return self._db or router.db_for_write(self.model, **self._hints)

    ###################
    # PRIVATE METHODS #

    def _insert(
        self,
        objs,
        fields,
        returning_fields=None,
        raw=False,
        using=None,
        on_conflict=None,
    ):
        """
        Insert a new record for the given model. This provides an interface to
        the InsertQuery class and is how Model.save() is implemented.
        """
        query = sql.InsertQuery(
            self.model,
            on_conflict=on_conflict,
        )
        query.insert_values(fields, objs, raw=raw)
        return query.get_compiler(using=using).execute_sql(returning_fields)

    _insert.alters_data = True
    _insert.queryset_only = False

    def _batched_insert(
        self,
        objs,
        fields,
        batch_size,
    ):
        """
        Helper method for bulk_create() to insert objs one batch at a time.
        """
        ops = connection.ops
        max_batch_size = max(ops.bulk_batch_size(fields, objs), 1)
        inserted_rows = []
        bulk_return = connection.features.can_return_rows_from_bulk_insert
        for item in [objs[i : i + batch_size] for i in range(0, len(objs), batch_size)]:
            if bulk_return and on_conflict is None:
                inserted_rows.extend(
                    self._insert(
                        item,
                        fields=fields,
                        using=self.db,
                        returning_fields=self.model._meta.db_returning_fields,
                    )
                )
            else:
                self._insert(
                    item,
                    fields=fields,
                    using=self.db,
                )
        return inserted_rows

    def _chain(self):
        """
        Return a copy of the current QuerySet that's ready for another
        """
        obj = self._clone()
        if obj._sticky_filter:
            obj.query.filter_is_sticky = True
            obj._sticky_filter = False
        return obj

    def _clone(self):
        """
        Return a copy of the current QuerySet. A lightweight alternative
        to deepcopy().
        """
        c = self.__class__(
            query=self.query.chain(),
            using=self._db,
            hints=self._hints,
        )
        c._sticky_filter = self._sticky_filter
        c._for_write = self._for_write
        c._prefetch_related_lookups = self._prefetch_related_lookups[:]
        c._known_related_objects = self._known_related_objects
        c._iterable_class = self._iterable_class
        c._fields = self._fields
        return c

    def _fetch_all(self):
            self._result_cache = list(self._iterable_class(self))
        if self._prefetch_related_lookups and not self._prefetch_done:
            self._prefetch_related_objects()

    def _next_is_sticky(self):
        """
        Indicate that the next filter call and the one following that should
        be treated as a single filter. This is only important when it comes to
        determining when to reuse tables for many-to-many filters. Required so
        that we can filter naturally on the results of related managers.

        This doesn't return a clone of the current QuerySet (it returns
        "self"). The method is only used internally and should be immediately
        followed by a filter() that does create a clone.
        """
        self._sticky_filter = True

    def _merge_sanity_check(self, other):
        """Check that two QuerySet classes may be merged."""
        if self._fields is not None and (
            set(self.query.values_select) != set(other.query.values_select)
            or set(self.query.extra_select) != set(other.query.extra_select)
            or set(self.query.annotation_select) != set(other.query.annotation_select)
        ):
                "Merging '%s' classes must involve the same values in each case."
                % self.__class__.__name__
            )

    def _merge_known_related_objects(self, other):
        """
        Keep track of all known related objects from either QuerySet instance.
        """
        for field, objects in other._known_related_objects.items():
            self._known_related_objects.setdefault(field, {}).update(objects)

        if self._fields and len(self._fields) > 1:
            # values() queryset can only be used as nested queries
            # if they are set up to select only a single field.
            raise TypeError("Cannot use multi-field values as a filter value.")
        query = self.query.resolve_expression(*args, **kwargs)
        query._db = self._db

    resolve_expression.queryset_only = True

    def _add_hints(self, **hints):
        """
        Update hinting information for use by routers. Add new key/values or
        overwrite existing key/values.
        """
        self._hints.update(hints)

    def _has_filters(self):
        """
        Check if this QuerySet has any filtering going on. This isn't
        equivalent with checking if all objects are present in results, for
        example, qs[1:]._has_filters() -> False.
        """
        return self.query.has_filters()

    def _validate_values_are_expressions(values, method_name):
        invalid_args = sorted(
            str(arg) for arg in values if not hasattr(arg, "resolve_expression")
        )
        if invalid_args:
                "QuerySet.%s() received non-expression(s): %s."
                % (
                    method_name,
                    ", ".join(invalid_args),
                )
            )

    def _not_support_combined_queries(self, operation_name):
        if self.query.combinator:
                "Calling QuerySet.%s() after %s() is not supported."
                % (operation_name, self.query.combinator)
            )

    def _check_operator_queryset(self, other, operator_):
        if self.query.combinator or other.query.combinator:
            raise TypeError(f"Cannot use {operator_} operator with combined queryset.")

    def _check_ordering_first_last_queryset_aggregation(self, method):
        if isinstance(self.query.group_by, tuple) and not any(
            col.output_field is self.model._meta.pk for col in self.query.group_by
        ):
                f"Cannot use QuerySet.{method}() on an unordered queryset performing "
                f"aggregation. Add an ordering with order_by()."
            )

class InstanceCheckMeta(type):
    def __instancecheck__(self, instance):
        return isinstance(instance, QuerySet) and instance.query.is_empty()

class EmptyQuerySet(metaclass=InstanceCheckMeta):
    """
    Marker class to checking if a queryset is empty by .none():
        isinstance(qs.none(), EmptyQuerySet) -> True
    """

        raise TypeError("EmptyQuerySet can't be instantiated")

class RawQuerySet:
    """
    Provide an iterator which converts the results of raw SQL queries into
    annotated model instances.
    """

        self,
        raw_query,
        model=None,
        query=None,
        params=(),
        translations=None,
        hints=None,
    ):
        self.raw_query = raw_query
        self.query = query or sql.RawQuery(sql=raw_query, using=self.db, params=params)
        self.params = params
        self.translations = translations or {}

    def resolve_model_init_order(self):
        """Resolve the init field names and value positions."""
        converter = connections[self.db].introspection.identifier_converter
        model_init_fields = [
            f for f in self.model._meta.fields if converter(f.column) in self.columns
        ]
        annotation_fields = [
            (column, pos)
            for pos, column in enumerate(self.columns)
            if column not in self.model_fields
        ]
        model_init_order = [
            self.columns.index(converter(f.column)) for f in model_init_fields
        ]
        model_init_names = [f.attname for f in model_init_fields]
        return model_init_names, model_init_order, annotation_fields

        """Same as QuerySet.prefetch_related()"""
        clone = self._clone()
        else:

        """Same as QuerySet._clone()"""
            self.raw_query,
            query=self.query,
            params=self.params,
            translations=self.translations,
        )
        return c

            self._result_cache = list(self.iterator())

                yield item

    def iterator(self):
        yield from RawModelIterable(self)

        return "<%s: %s>" % (self.__class__.__name__, self.query)

        return list(self)[k]

    @property

        """Select the database this RawQuerySet should execute against."""
        return RawQuerySet(
            query=self.query.chain(using=alias),
            using=alias,
        )

    def columns(self):
        """
        A list of model field names in the order they'll appear in the
        query results.
        """
        columns = self.query.get_columns()
        # Adjust any column names which don't match field names
        for query_name, model_name in self.translations.items():
            # Ignore translations for nonexistent column names
            try:
                index = columns.index(query_name)
                pass
            else:
                columns[index] = model_name
        return columns

    def model_fields(self):
        """A dict mapping column names to model field names."""
        model_fields = {}
        for field in self.model._meta.fields:
            name, column = field.get_attname_column()
            model_fields[converter(column)] = field
        return model_fields

class Prefetch:
    def __init__(self, lookup, queryset=None, to_attr=None):
        # `prefetch_through` is the path we traverse to perform the prefetch.
        self.prefetch_through = lookup
        # `prefetch_to` is the path to the attribute that stores the result.
        self.prefetch_to = lookup
        if queryset is not None and (
            isinstance(queryset, RawQuerySet)
            or (
                hasattr(queryset, "_iterable_class")
                and not issubclass(queryset._iterable_class, ModelIterable)
            )
        ):
                "Prefetch querysets cannot use raw(), values(), and values_list()."
            )
        if to_attr:
            self.prefetch_to = LOOKUP_SEP.join(
                lookup.split(LOOKUP_SEP)[:-1] + [to_attr]
            )

        self.to_attr = to_attr

        obj_dict = self.__dict__.copy()
        if self.queryset is not None:
            queryset = self.queryset._chain()
            # Prevent the QuerySet from being evaluated
            queryset._result_cache = []
            queryset._prefetch_done = True
            obj_dict["queryset"] = queryset
        return obj_dict

    def add_prefix(self, prefix):
        self.prefetch_through = prefix + LOOKUP_SEP + self.prefetch_through
        self.prefetch_to = prefix + LOOKUP_SEP + self.prefetch_to

    def get_current_prefetch_to(self, level):
        return LOOKUP_SEP.join(self.prefetch_to.split(LOOKUP_SEP)[: level + 1])

    def get_current_to_attr(self, level):
        parts = self.prefetch_to.split(LOOKUP_SEP)
        to_attr = parts[level]
        as_attr = self.to_attr and level == len(parts) - 1
        return to_attr, as_attr

    def get_current_queryset(self, level):
        if self.get_current_prefetch_to(level) == self.prefetch_to:
            return self.queryset

        if not isinstance(other, Prefetch):
        return self.prefetch_to == other.prefetch_to

        return hash((self.__class__, self.prefetch_to))

def normalize_prefetch_lookups(lookups, prefix=None):
    """Normalize lookups into Prefetch objects."""
    ret = []
    for lookup in lookups:
        if not isinstance(lookup, Prefetch):
            lookup = Prefetch(lookup)
        if prefix:
            lookup.add_prefix(prefix)
        ret.append(lookup)
    return ret

def prefetch_related_objects(model_instances, *related_lookups):
    """
    Populate prefetched object caches for a list of model instances based on
    the lookups/Prefetch instances given.
    """
    if not model_instances:
        return  # nothing to do

    # We need to be able to dynamically add to the list of prefetch_related
    # lookups that we look up (see below).  So we need some book keeping to
    # ensure we don't do duplicate work.
    done_queries = {}  # dictionary of things like 'foo__bar': [results]

    auto_lookups = set()  # we add to this as we go through.
    followed_descriptors = set()  # recursion protection

    all_lookups = normalize_prefetch_lookups(reversed(related_lookups))
    while all_lookups:
        lookup = all_lookups.pop()
        if lookup.prefetch_to in done_queries:
            if lookup.queryset is not None:
                    "'%s' lookup was already seen with a different queryset. "
                    "You may need to adjust the ordering of your lookups."
                    % lookup.prefetch_to
                )

            continue

        # Top level, the list of objects to decorate is the result cache
        # from the primary QuerySet. It won't be for deeper levels.
        obj_list = model_instances

        through_attrs = lookup.prefetch_through.split(LOOKUP_SEP)
        for level, through_attr in enumerate(through_attrs):
            # Prepare main instances
            if not obj_list:
                break

            prefetch_to = lookup.get_current_prefetch_to(level)
            if prefetch_to in done_queries:
                # Skip any prefetching, and any object preparation
                obj_list = done_queries[prefetch_to]
                continue

            # Prepare objects:
            good_objects = True
            for obj in obj_list:
                # Since prefetching can re-use instances, it is possible to have
                # the same instance multiple times in obj_list, so obj might
                # already be prepared.
                if not hasattr(obj, "_prefetched_objects_cache"):
                    try:
                        obj._prefetched_objects_cache = {}
                    except (AttributeError, TypeError):
                        # Must be an immutable object from
                        # values_list(flat=True), for example (TypeError) or
                        # a QuerySet subclass that isn't returning Model
                        # instances (AttributeError), either in Django or a 3rd
                        # party. prefetch_related() doesn't make sense, so quit.
                        good_objects = False
                        break
            if not good_objects:
                break

            # Descend down tree

            # We assume that objects retrieved are homogeneous (which is the premise
            # of prefetch_related), so what applies to first object applies to all.
            first_obj = obj_list[0]
            to_attr = lookup.get_current_to_attr(level)[0]
            prefetcher, descriptor, attr_found, is_fetched = get_prefetcher(
                first_obj, through_attr, to_attr
            )

            if not attr_found:
                raise AttributeError(
                    "Cannot find '%s' on %s object, '%s' is an invalid "
                    "parameter to prefetch_related()"
                    % (
                        through_attr,
                        first_obj.__class__.__name__,
                        lookup.prefetch_through,
                    )
                )

            if level == len(through_attrs) - 1 and prefetcher is None:
                # Last one, this *must* resolve to something that supports
                # prefetching, otherwise there is no point adding it and the
                # developer asking for it has made a mistake.
                    "'%s' does not resolve to an item that supports "
                    "prefetching - this is an invalid parameter to "
                    "prefetch_related()." % lookup.prefetch_through
                )

            obj_to_fetch = None
            if prefetcher is not None:
                obj_to_fetch = [obj for obj in obj_list if not is_fetched(obj)]

            if obj_to_fetch:
                obj_list, additional_lookups = prefetch_one_level(
                    obj_to_fetch,
                    prefetcher,
                    lookup,
                    level,
                )
                # We need to ensure we don't keep adding lookups from the
                # same relationships to stop infinite recursion. So, if we
                # are already on an automatically added lookup, don't add
                # the new lookups from relationships we've seen already.
                if not (
                    prefetch_to in done_queries
                    and lookup in auto_lookups
                    and descriptor in followed_descriptors
                ):
                    done_queries[prefetch_to] = obj_list
                    new_lookups = normalize_prefetch_lookups(
                        reversed(additional_lookups), prefetch_to
                    )
                    auto_lookups.update(new_lookups)
                    all_lookups.extend(new_lookups)
                followed_descriptors.add(descriptor)
            else:
                # Either a singly related object that has already been fetched
                # (e.g. via select_related), or hopefully some other property
                # that doesn't support prefetching but needs to be traversed.

                # We replace the current list of parent objects with the list
                # of related objects, filtering out empty or missing values so
                # that we can continue with nullable or reverse relations.
                new_obj_list = []
                for obj in obj_list:
                    if through_attr in getattr(obj, "_prefetched_objects_cache", ()):
                        # If related objects have been prefetched, use the
                        # cache rather than the object's through_attr.
                        new_obj = list(obj._prefetched_objects_cache.get(through_attr))
                    else:
                        try:
                            new_obj = getattr(obj, through_attr)
                        except exceptions.ObjectDoesNotExist:
                            continue
                    if new_obj is None:
                        continue
                    # We special-case `list` rather than something more generic
                    # like `Iterable` because we don't want to accidentally match
                    # user models that define __iter__.
                    if isinstance(new_obj, list):
                        new_obj_list.extend(new_obj)
                    else:
                        new_obj_list.append(new_obj)
                obj_list = new_obj_list

def get_prefetcher(instance, through_attr, to_attr):
    """
    For the attribute 'through_attr' on the given instance, find
    an object that has a get_prefetch_queryset().
    Return a 4 tuple containing:
    (the object with get_prefetch_queryset (or None),
     the descriptor object representing this relationship (or None),
     a boolean that is False if the attribute was not found at all,
     a function that takes an instance and returns a boolean that is True if
     the attribute has already been fetched for that instance)
    """

    def has_to_attr_attribute(instance):
        return hasattr(instance, to_attr)

    prefetcher = None
    is_fetched = has_to_attr_attribute

    # For singly related objects, we have to avoid getting the attribute
    # from the object, as this will trigger the query. So we first try
    # on the class, in order to get the descriptor object.
    rel_obj_descriptor = getattr(instance.__class__, through_attr, None)
    if rel_obj_descriptor is None:
        attr_found = hasattr(instance, through_attr)
    else:
        attr_found = True
        if rel_obj_descriptor:
            # singly related object, descriptor object has the
            # get_prefetch_queryset() method.
            if hasattr(rel_obj_descriptor, "get_prefetch_queryset"):
                prefetcher = rel_obj_descriptor
                is_fetched = rel_obj_descriptor.is_cached
            else:
                # descriptor doesn't support prefetching, so we go ahead and get
                # the attribute on the instance rather than the class to
                # support many related managers
                rel_obj = getattr(instance, through_attr)
                if hasattr(rel_obj, "get_prefetch_queryset"):
                    prefetcher = rel_obj
                if through_attr != to_attr:
                    # Special case cached_property instances because hasattr
                    # triggers attribute computation and assignment.
                    if isinstance(
                        getattr(instance.__class__, to_attr, None), cached_property
                    ):

                        def has_cached_property(instance):
                            return to_attr in instance.__dict__

                        is_fetched = has_cached_property
                else:

                    def in_prefetched_cache(instance):
                        return through_attr in instance._prefetched_objects_cache

                    is_fetched = in_prefetched_cache
    return prefetcher, rel_obj_descriptor, attr_found, is_fetched

def prefetch_one_level(instances, prefetcher, lookup, level):
    """
    Helper function for prefetch_related_objects().

    Run prefetches on all instances using the prefetcher object,
    assigning results to relevant caches in instance.

    Return the prefetched objects along with any additional prefetches that
    must be done due to prefetch_related lookups found from default managers.
    """
    # prefetcher must have a method get_prefetch_queryset() which takes a list
    # of instances, and returns a tuple:

    # (queryset of instances of self.model that are related to passed in instances,
    #  callable that gets value to be matched for returned instances,
    #  callable that gets value to be matched for passed in instances,
    #  boolean that is True for singly related objects,
    #  cache or field name to assign to,
    #  boolean that is True when the previous argument is a cache name vs a field name).

    # The 'values to be matched' must be hashable as they will be used
    # in a dictionary.

    (
        rel_qs,
        rel_obj_attr,
        instance_attr,
        single,
        cache_name,
        is_descriptor,
    ) = prefetcher.get_prefetch_queryset(instances, lookup.get_current_queryset(level))
    # We have to handle the possibility that the QuerySet we just got back
    # contains some prefetch_related lookups. We don't want to trigger the
    # prefetch_related functionality by evaluating the query. Rather, we need
    # to merge in the prefetch_related lookups.
    # Copy the lookups in case it is a Prefetch object which could be reused
    # later (happens in nested prefetch_related).
    additional_lookups = [
        copy.copy(additional_lookup)
        for additional_lookup in getattr(rel_qs, "_prefetch_related_lookups", ())
    ]
    if additional_lookups:
        # Don't need to clone because the manager should have given us a fresh
        # instance, so we access an internal instead of using public interface
        # for performance reasons.
        rel_qs._prefetch_related_lookups = ()

    all_related_objects = list(rel_qs)

    rel_obj_cache = {}
    for rel_obj in all_related_objects:
        rel_attr_val = rel_obj_attr(rel_obj)
        rel_obj_cache.setdefault(rel_attr_val, []).append(rel_obj)

    to_attr, as_attr = lookup.get_current_to_attr(level)
    # Make sure `to_attr` does not conflict with a field.
    if as_attr and instances:
        # We assume that objects retrieved are homogeneous (which is the premise
        # of prefetch_related), so what applies to first object applies to all.
        model = instances[0].__class__
        try:
            model._meta.get_field(to_attr)
        except exceptions.FieldDoesNotExist:
            pass
        else:
            msg = "to_attr={} conflicts with a field on the {} model."
            raise ValueError(msg.format(to_attr, model.__name__))

    # Whether or not we're prefetching the last part of the lookup.
    leaf = len(lookup.prefetch_through.split(LOOKUP_SEP)) - 1 == level

    for obj in instances:
        instance_attr_val = instance_attr(obj)
        vals = rel_obj_cache.get(instance_attr_val, [])

        if single:
            val = vals[0] if vals else None
            if as_attr:
                # A to_attr has been given for the prefetch.
                setattr(obj, to_attr, val)
            elif is_descriptor:
                # cache_name points to a field name in obj.
                # This field is a descriptor for a related object.
                setattr(obj, cache_name, val)
            else:
                # No to_attr has been given for this prefetch operation and the
                # cache_name does not point to a descriptor. Store the value of
                # the field in the object's field cache.
                obj._state.fields_cache[cache_name] = val
        else:
                setattr(obj, to_attr, vals)
            else:
                manager = getattr(obj, to_attr)
                if leaf and lookup.queryset is not None:
                    qs = manager._apply_rel_filters(lookup.queryset)
                else:
                    qs = manager.get_queryset()
                qs._result_cache = vals
                # We don't want the individual qs doing prefetch_related now,
                # since we have merged this into the current work.
                qs._prefetch_done = True
                obj._prefetched_objects_cache[cache_name] = qs
    return all_related_objects, additional_lookups

class RelatedPopulator:
    """
    RelatedPopulator is used for select_related() object instantiation.

    The idea is that each select_related() model will be populated by a
    different RelatedPopulator instance. The RelatedPopulator instances get
    klass_info and select (computed in SQLCompiler) plus the used db as
    input for initialization. That data is used to compute which columns
    to use, how to instantiate the model, and how to populate the links
    between the objects.

    The actual creation of the objects is done in populate() method. This
    method gets row and from_obj as input and populates the select_related()
    model instance.
    """

    def __init__(self, klass_info, select, db):
        self.db = db
        # Pre-compute needed attributes. The attributes are:
        #  - model_cls: the possibly deferred model class to instantiate
        #  - either:
        #    - cols_start, cols_end: usually the columns in the row are
        #      in the same order model_cls.__init__ expects them, so we
        #      can instantiate by model_cls(*row[cols_start:cols_end])
        #    - reorder_for_init: When select_related descends to a child
        #      class, then we want to reuse the already selected parent
        #      data. However, in this case the parent data isn't necessarily
        #      in the same order that Model.__init__ expects it to be, so
        #      we have to reorder the parent data. The reorder_for_init
        #      attribute contains a function used to reorder the field data
        #      in the order __init__ expects it.
        #  - pk_idx: the index of the primary key field in the reordered
        #    model data. Used to check if a related object exists at all.
        #  - init_list: the field attnames fetched from the database. For
        #    deferred models this isn't the same as all attnames of the
        #    model's fields.
        #  - related_populators: a list of RelatedPopulator instances if
        #    select_related() descends to related models from this model.
        #  - local_setter, remote_setter: Methods to set cached values on
        #    the object being populated and on the remote object. Usually
        #    these are Field.set_cached_value() methods.
        from_parent = klass_info["from_parent"]
        if not from_parent:
            self.cols_start = select_fields[0]
            self.cols_end = select_fields[-1] + 1
            self.init_list = [
                f[0].target.attname for f in select[self.cols_start : self.cols_end]
            ]
            self.reorder_for_init = None
        else:
            attname_indexes = {
                select[idx][0].target.attname: idx for idx in select_fields
            }
            model_init_attnames = (
                f.attname for f in klass_info["model"]._meta.concrete_fields
            )
                attname for attname in model_init_attnames if attname in attname_indexes
            ]
            self.reorder_for_init = operator.itemgetter(
                *[attname_indexes[attname] for attname in self.init_list]
            )

        self.model_cls = klass_info["model"]
        self.pk_idx = self.init_list.index(self.model_cls._meta.pk.attname)
        self.related_populators = get_related_populators(klass_info, select, self.db)
        self.local_setter = klass_info["local_setter"]
        self.remote_setter = klass_info["remote_setter"]

    def populate(self, row, from_obj):
        if self.reorder_for_init:
            obj_data = self.reorder_for_init(row)
        else:
            obj_data = row[self.cols_start : self.cols_end]
        if obj_data[self.pk_idx] is None:
            obj = None
        else:
            obj = self.model_cls.from_db(self.db, self.init_list, obj_data)
            for rel_iter in self.related_populators:
                rel_iter.populate(row, obj)
        self.local_setter(from_obj, obj)
        if obj is not None:
            self.remote_setter(obj, from_obj)

def get_related_populators(klass_info, select, db):
    iterators = []
    related_klass_infos = klass_info.get("related_klass_infos", [])
    for rel_klass_info in related_klass_infos:
        rel_cls = RelatedPopulator(rel_klass_info, select, db)
        iterators.append(rel_cls)
    return iterators

from django import template

register = template.Library()

@register.filter(name="addclass")
def addclass(field, given_class):
    existing_classes = field.field.widget.attrs.get("class", None)
    if existing_classes:
        if existing_classes.find(given_class) == -1:
            # if the given class doesn't exist in the existing classes
            classes = existing_classes + " " + given_class
        else:
            classes = existing_classes
    else:
        classes = given_class
    return field.as_widget(attrs={"class": classes})

"""
Custom Evennia admin-site, for better customization of the admin-site
as a whole.

This must be located outside of the admin/ folder because it must be imported
before any of the app-data (which in turn must be imported in the `__init__.py`
of that folder for Django to find them).

"""

from django.contrib.admin import apps

class EvenniaAdminApp(apps.AdminConfig):
    """
    This is imported in INSTALLED_APPS instead of django.contrib.admin.

    """

    default_site = "evennia.web.utils.adminsite.EvenniaAdminSite"

class EvenniaAdminSite(admin.AdminSite):
    """
    The main admin site root (replacing the default from Django). When doing
    admin.register in the admin/ folder, this is what is being registered to.

    """

    site_header = "Evennia web admin"

    def get_app_list(self, request, app_label=None):
        app_list = super().get_app_list(request, app_label=app_label)
        app_mapping = {app["app_label"]: app for app in app_list}
        out = [
            app_mapping.pop(app_label)
            for app_label in settings.DJANGO_ADMIN_APP_ORDER
            if app_label in app_mapping
        ]
        for app in settings.DJANGO_ADMIN_APP_EXCLUDE:
            app_mapping.pop(app, None)
        out += app_mapping.values()
        return out

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend

class CaseInsensitiveModelBackend(ModelBackend):
    """
    By default ModelBackend does case _sensitive_ username
    authentication, which isn't what is generally expected.  This
    backend supports case insensitive username authentication.

    """

    def authenticate(self, request, username=None, password=None, autologin=None):
        """
        Custom authenticate with bypass for auto-logins

        Args:
            request (Request): Request object.
            username (str, optional): Name of user to authenticate.
            password (str, optional): Password of user
            autologin (Account, optional): If given, assume this is
              an already authenticated account and bypass authentication.
        """
        if autologin:
            # Note: Setting .backend on account is critical in order to
            # be allowed to call django.auth.login(account) later. This
            # is necessary for the auto-login feature of the webclient,
            # but it's important to make sure Django doesn't change this
            # requirement or the name of the property down the line. /Griatch
            autologin.backend = "evennia.web.utils.backends.CaseInsensitiveModelBackend"
            return autologin
        else:
            # In this case .backend will be assigned automatically
            # somewhere along the way.
            Account = get_user_model()
            try:
                account = Account.objects.get(username__iexact=username)
                if account.check_password(password):
                    return account
                else:
            except Account.DoesNotExist:

"""
This file defines global variables that will always be available in a view
context without having to repeatedly include it.

For this to work, this file is included in the settings file, in the
TEMPLATES["OPTIONS"]["context_processors"] list.

"""

import os

from evennia.utils.utils import get_evennia_version

# Setup lists of the most relevant apps so
# the adminsite becomes more readable.

GAME_NAME = None
GAME_SLOGAN = None
SERVER_VERSION = None
SERVER_HOSTNAME = None

REGISTER_ENABLED = None

TELNET_ENABLED = None
TELNET_PORTS = None
TELNET_SSL_ENABLED = None
TELNET_SSL_PORTS = None

SSH_ENABLED = None
SSH_PORTS = None

WEBCLIENT_ENABLED = None
WEBSOCKET_CLIENT_ENABLED = None
WEBSOCKET_PORT = None
WEBSOCKET_URL = None

REST_API_ENABLED = False

ACCOUNT_RELATED = ["Accounts"]
GAME_ENTITIES = ["Objects", "Scripts", "Comms", "Help"]
GAME_SETUP = ["Permissions", "Config"]
CONNECTIONS = ["Irc"]
WEBSITE = ["Flatpages", "News", "Sites"]

def load_game_settings():
    """
    Load and cache game settings.

    """
    global GAME_NAME, GAME_SLOGAN, SERVER_VERSION, SERVER_HOSTNAME
    global REGISTER_ENABLED
    global TELNET_ENABLED, TELNET_PORTS
    global TELNET_SSL_ENABLED, TELNET_SSL_PORTS
    global SSH_ENABLED, SSH_PORTS
    global WEBCLIENT_ENABLED, WEBSOCKET_CLIENT_ENABLED, WEBSOCKET_PORT, WEBSOCKET_URL
    global REST_API_ENABLED

    try:
        GAME_NAME = settings.SERVERNAME.strip()
        GAME_NAME = "Evennia"
    SERVER_VERSION = get_evennia_version()
    try:
        GAME_SLOGAN = settings.GAME_SLOGAN.strip()
        GAME_SLOGAN = SERVER_VERSION
    SERVER_HOSTNAME = settings.SERVER_HOSTNAME

    REGISTER_ENABLED = settings.NEW_ACCOUNT_REGISTRATION_ENABLED

    TELNET_ENABLED = settings.TELNET_ENABLED
    TELNET_PORTS = settings.TELNET_PORTS
    TELNET_SSL_ENABLED = settings.SSL_ENABLED
    TELNET_SSL_PORTS = settings.SSL_PORTS

    SSH_ENABLED = settings.SSH_ENABLED
    SSH_PORTS = settings.SSH_PORTS

    WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
    WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED
    # if we are working through a proxy or uses docker port-remapping, the webclient port encoded
    # in the webclient should be different than the one the server expects. Use the environment
    # variable WEBSOCKET_CLIENT_PROXY_PORT if this is the case.
    WEBSOCKET_PORT = int(
        os.environ.get("WEBSOCKET_CLIENT_PROXY_PORT", settings.WEBSOCKET_CLIENT_PORT)
    )
    # this is determined dynamically by the client and is less of an issue
    WEBSOCKET_URL = settings.WEBSOCKET_CLIENT_URL

    REST_API_ENABLED = settings.REST_API_ENABLED

load_game_settings()

# The main context processor function
def general_context(request):
    """
    Returns common Evennia-related context stuff, which is automatically added
    to context of all views.

    """
    account = None
    if request.user.is_authenticated:
        account = request.user

    puppet = None
    if account and request.session.get("puppet"):
        pk = int(request.session.get("puppet"))
        puppet = next((x for x in account.characters if x.pk == pk), None)

    return {
        "account": account,
        "puppet": puppet,
        "game_name": GAME_NAME,
        "game_slogan": GAME_SLOGAN,
        "server_hostname": SERVER_HOSTNAME,
        "evennia_userapps": ACCOUNT_RELATED,
        "evennia_entityapps": GAME_ENTITIES,
        "evennia_setupapps": GAME_SETUP,
        "evennia_connectapps": CONNECTIONS,
        "evennia_websiteapps": WEBSITE,
        "register_enabled": REGISTER_ENABLED,
        "telnet_enabled": TELNET_ENABLED,
        "telnet_ports": TELNET_PORTS,
        "telnet_ssl_enabled": TELNET_SSL_ENABLED,
        "telnet_ssl_ports": TELNET_SSL_PORTS,
        "ssh_enabled": SSH_ENABLED,
        "ssh_ports": SSH_ENABLED,
        "webclient_enabled": WEBCLIENT_ENABLED,
        "websocket_enabled": WEBSOCKET_CLIENT_ENABLED,
        "websocket_port": WEBSOCKET_PORT,
        "websocket_url": WEBSOCKET_URL,
        "rest_api_enabled": REST_API_ENABLED,
    }

from django.contrib.auth import authenticate, login

from evennia.utils import logger, ip_from_request

class OriginIpMiddleware:
    """
    This Django Middleware simply sets the request.origin_ip attribute to what is
    respected by the Evennia Server, taking into account settings.UPSTREAM_IPS.
    """

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        request.origin_ip = ip_from_request(request)
        return self.get_response(request)

class SharedLoginMiddleware(object):
    """
    Handle the shared login between website and webclient.

    """

        # One-time configuration and initialization.

        # Code to be executed for each request before
        # the view (and later middleware) are called.

        # Synchronize credentials between webclient and website
        # Must be performed *before* rendering the view (issue #1723)
        self.make_shared_login(request)

        # Process view
        response = self.get_response(request)

        # Code to be executed for each request/response after
        # the view is called.

        # Return processed view

    def make_shared_login(cls, request):
        csession = request.session
        website_uid = csession.get("website_authenticated_uid", None)
        webclient_uid = csession.get("webclient_authenticated_uid", None)

        if not csession.session_key:
            # this is necessary to build the sessid key

        if account.is_authenticated:
            # Logged into website
            if website_uid is None:
                # fresh website login (just from login page)
                csession["website_authenticated_uid"] = account.id
            if webclient_uid is None:
                # auto-login web client
                csession["webclient_authenticated_uid"] = account.id

        elif webclient_uid:
            # Not logged into website, but logged into webclient
                account = AccountDB.objects.get(id=webclient_uid)
                try:
                    # calls our custom authenticate, in web/utils/backend.py
                    authenticate(autologin=account)
                    login(request, account)

        if csession.get("webclient_authenticated_uid", None):
            # set a nonce to prevent the webclient from erasing the webclient_authenticated_uid value
            csession["webclient_authenticated_nonce"] = (
                csession.get("webclient_authenticated_nonce", 0) + 1
            )
            # wrap around to prevent integer overflows
            if csession["webclient_authenticated_nonce"] > 32:

from django.contrib.auth.models import AnonymousUser
from django.test import RequestFactory, TestCase

from . import general_context

class TestGeneralContext(TestCase):

    @patch("evennia.web.utils.general_context.GAME_NAME", "test_name")
    @patch("evennia.web.utils.general_context.GAME_SLOGAN", "test_game_slogan")
    @patch("evennia.web.utils.general_context.REGISTER_ENABLED", "register_enabled_testvalue")
    @patch(
        "evennia.web.utils.general_context.WEBSOCKET_CLIENT_ENABLED",
        "websocket_client_enabled_testvalue",
    )
    @patch("evennia.web.utils.general_context.WEBCLIENT_ENABLED", "webclient_enabled_testvalue")
    @patch("evennia.web.utils.general_context.WEBSOCKET_PORT", "websocket_client_port_testvalue")
    @patch("evennia.web.utils.general_context.WEBSOCKET_URL", "websocket_client_url_testvalue")
    @patch("evennia.web.utils.general_context.REST_API_ENABLED", True)
    def test_general_context(self):
        request = RequestFactory().get("/")
        request.user = AnonymousUser()
        request.session = {"account": None, "puppet": None}

        response = general_context.general_context(request)

            response,
            {
                "account": None,
                "puppet": None,
                "game_name": "test_name",
                "game_slogan": "test_game_slogan",
                "evennia_userapps": ["Accounts"],
                "evennia_entityapps": ["Objects", "Scripts", "Comms", "Help"],
                "evennia_setupapps": ["Permissions", "Config"],
                "evennia_connectapps": ["Irc"],
                "evennia_websiteapps": ["Flatpages", "News", "Sites"],
                "register_enabled": "register_enabled_testvalue",
                "webclient_enabled": "webclient_enabled_testvalue",
                "websocket_enabled": "websocket_client_enabled_testvalue",
                "websocket_port": "websocket_client_port_testvalue",
                "websocket_url": "websocket_client_url_testvalue",
                "rest_api_enabled": True,
                "server_hostname": "localhost",
                "ssh_enabled": False,
                "ssh_ports": False,
                "telnet_enabled": True,
                "telnet_ports": [4000],
                "telnet_ssl_enabled": False,
                "telnet_ssl_ports": [4003],
            },
        )

"""
This contains a simple view for rendering the webclient
page and serve it eventual static content.

"""

from django.http import Http404

def webclient(request):
    """
    Webclient page template loading.

    """
    # auto-login is now handled by evennia.web.utils.middleware

    # check if webclient should be enabled
    if not settings.WEBCLIENT_ENABLED:
        raise Http404

    # make sure to store the browser session's hash so the webclient can get to it!
    pagevars = {"browser_sessid": request.session.session_key}

    return render(request, "webclient.html", pagevars)

from django.contrib.auth.forms import UserCreationForm, UsernameField
from django.forms import ModelForm

from evennia.utils import class_from_module

class EvenniaForm(forms.Form):
    """
    This is a stock Django form, but modified so that all values provided
    through it are escaped (sanitized). Validation is performed by the fields
    you define in the form.

    This has little to do with Evennia itself and is more general web security-
    related.

    https://www.owasp.org/index.php/Input_Validation_Cheat_Sheet#Goals_of_Input_Validation

    """

        """
        Django hook. Performed on form submission.

        Returns:
            cleaned (dict): Dictionary of key:value pairs submitted on the form.

        """
        # Call parent function
        cleaned = super().clean()

        # Escape all values provided by user
        cleaned = {k: escape(v) for k, v in cleaned.items()}
        return cleaned

class AccountForm(UserCreationForm):
    """
    This is a generic Django form tailored to the Account model.

    In this incarnation it does not allow getting/setting of attributes, only
    core User model fields (username, email, password).

    """

        """
        This is a Django construct that provides additional configuration to
        the form.

        """

        # The model/typeclass this form creates
        model = class_from_module(
            settings.BASE_ACCOUNT_TYPECLASS, fallback=settings.FALLBACK_ACCOUNT_TYPECLASS
        )

        # The fields to display on the form, in the given order
        fields = ("username", "email")

        # Any overrides of field classes
        field_classes = {"username": UsernameField}

    # Username is collected as part of the core UserCreationForm, so we just need
    # to add a field to (optionally) capture email.
    email = forms.EmailField(
        help_text="A valid email address. Optional; used for password resets.", required=False
    )

class ObjectForm(EvenniaForm, ModelForm):
    """
    This is a Django form for generic Evennia Objects that allows modification
    of attributes when called from a descendent of ObjectUpdate or ObjectCreate
    views.

    It defines no fields by default; you have to do that by extending this class
    and defining what fields you want to be recorded. See the CharacterForm for
    a simple example of how to do this.

    """

        """
        the form.

        """

            settings.BASE_OBJECT_TYPECLASS, fallback=settings.FALLBACK_OBJECT_TYPECLASS
        )

        fields = ("db_key",)

        # This lets us rename ugly db-specific keys to something more human
        labels = {"db_key": "Name"}

class CharacterForm(ObjectForm):
    """
    This is a Django form for Evennia Character objects.

    Since Evennia characters only have one attribute by default, this form only
    defines a field for that single attribute. The names of fields you define should
    correspond to their names as stored in the dbhandler; you can display
    'prettier' versions of the fieldname on the form using the 'label' kwarg.

    The basic field types are CharFields and IntegerFields, which let you enter
    text and numbers respectively. IntegerFields have some neat validation tricks
    they can do, like mandating values fall within a certain range.

    For example, a complete "age" field (which stores its value to
    `character.db.age` might look like:

    age = forms.IntegerField(
        label="Your Age",
        min_value=18, max_value=9000,
        help_text="Years since your birth.")

    Default input fields are generic single-line text boxes. You can control what
    sort of input field users will see by specifying a "widget." An example of
    this is used for the 'desc' field to show a Textarea box instead of a Textbox.

    For help in building out your form, please see:
    https://docs.djangoproject.com/en/4.1/topics/forms/#building-a-form-in-django

    For more information on fields and their capabilities, see:
    https://docs.djangoproject.com/en/4.1/ref/forms/fields/

    For more on widgets, see:
    https://docs.djangoproject.com/en/4.1/ref/forms/widgets/

    """

        """
        the form.

        """

        # Get the correct object model
            settings.BASE_CHARACTER_TYPECLASS, fallback=settings.FALLBACK_CHARACTER_TYPECLASS
        )

        # Allow entry of the 'key' field

        # Rename 'key' to something more intelligible

    # Fields pertaining to configurable attributes on the Character object.
    desc = forms.CharField(
        label="Description",
        max_length=2048,
        widget=forms.Textarea(attrs={"rows": 3}),
        help_text="A brief description of your character.",
    )

class CharacterUpdateForm(CharacterForm):
    """
    This is a Django form for updating Evennia Character objects.

    By default it is the same as the CharacterForm, but if there are circumstances
    in which you don't want to let players edit all the same attributes they had
    access to during creation, you can redefine this form with those fields you do
    wish to allow.

    """

    pass

from django.test import Client, override_settings

from evennia.help import filehelp
from evennia.utils.create import create_help_entry

_FILE_HELP_ENTRIES = None

class EvenniaWebTest(BaseEvenniaTest):
    # Use the same classes the views are expecting
    channel_typeclass = settings.BASE_CHANNEL_TYPECLASS

    # Default named url
    url_name = "index"

    # Response to expect for unauthenticated requests
    unauthenticated_response = 200

    # Response to expect for authenticated requests
    authenticated_response = 200

        # Add chars to account rosters
        self.account2.characters.add(self.char2)

        for account in (self.account, self.account2):
            # Demote accounts to Player permissions
            account.permissions.add("Player")
            account.permissions.remove("Developer")

            # Grant permissions to chars
            for char in account.characters:
                char.locks.add("edit:id(%s) or perm(Admin)" % account.pk)
                char.locks.add("delete:id(%s) or perm(Admin)" % account.pk)
                char.locks.add("view:all()")

    def test_valid_chars(self):
        "Make sure account has playable characters"
        self.assertTrue(self.char1 in self.account.characters)
        self.assertTrue(self.char2 in self.account2.characters)

    def get_kwargs(self):
        return {}

    def test_get(self):
        # Try accessing page while not logged in
        response = self.client.get(reverse(self.url_name, kwargs=self.get_kwargs()))
        self.assertEqual(response.status_code, self.unauthenticated_response)

    def login(self):
        return self.client.login(username="TestAccount", password="testpassword")

    def test_get_authenticated(self):
        logged_in = self.login()
        self.assertTrue(logged_in, "Account failed to log in!")

        # Try accessing page while logged in
        response = self.client.get(reverse(self.url_name, kwargs=self.get_kwargs()), follow=True)

        self.assertEqual(response.status_code, self.authenticated_response)

class AdminTest(EvenniaWebTest):
    url_name = "django_admin"
    unauthenticated_response = 302

class IndexTest(EvenniaWebTest):

class RegisterTest(EvenniaWebTest):
    url_name = "register"

class LoginTest(EvenniaWebTest):
    url_name = "login"

class LogoutTest(EvenniaWebTest):
    url_name = "logout"

class PasswordResetTest(EvenniaWebTest):
    url_name = "password_change"

class WebclientTest(EvenniaWebTest):
    url_name = "webclient:index"

    @override_settings(WEBCLIENT_ENABLED=True)
        self.authenticated_response = 200
        self.unauthenticated_response = 200
        super().test_get()

    @override_settings(WEBCLIENT_ENABLED=False)
    def test_get_disabled(self):
        self.authenticated_response = 404
        self.unauthenticated_response = 404

class ChannelListTest(EvenniaWebTest):
    url_name = "channels"

class ChannelDetailTest(EvenniaWebTest):
    url_name = "channel-detail"

        klass = class_from_module(
            self.channel_typeclass, fallback=settings.FALLBACK_CHANNEL_TYPECLASS
        )

        # Create a channel
        klass.create("demo")

        return {"slug": slugify("demo")}

class HelpListTest(EvenniaWebTest):
    url_name = "help"

HELP_ENTRY_DICTS = [
    {"key": "unit test file entry", "category": "General", "text": "cache test file entry text"}
]

class HelpDetailTest(EvenniaWebTest):
    url_name = "help-entry-detail"

        # create a db help entry
        create_help_entry("unit test db entry", "unit test db entry text", category="General")

        return {"category": slugify("general"), "topic": slugify("unit test db entry")}

    def test_view(self):
        self.assertEqual(response.context["entry_text"], "unit test db entry text")

    def test_object_cache(self):
        # clear file help entries, use local HELP_ENTRY_DICTS to recreate new entries
        global _FILE_HELP_ENTRIES
        if _FILE_HELP_ENTRIES is None:
            from evennia.help.filehelp import FILE_HELP_ENTRIES as _FILE_HELP_ENTRIES
        help_module = "evennia.web.website.tests"
        self.file_help_store = _FILE_HELP_ENTRIES.__init__(help_file_modules=[help_module])

        # request access to an entry
        # request a second entry, verifing the cached object is not provided on a new topic request
        entry_two_args = {"category": slugify("general"), "topic": slugify("unit test file entry")}
        response = self.client.get(reverse(self.url_name, kwargs=entry_two_args), follow=True)
        self.assertEqual(response.context["entry_text"], "cache test file entry text")

class HelpLockedDetailTest(EvenniaWebTest):

        # create a db entry with a lock
        self.db_help_entry = create_help_entry(
            "unit test locked topic",
            "unit test locked entrytext",
            category="General",
            locks="read:perm(Developer)",
        )

        return {"category": slugify("general"), "topic": slugify("unit test locked topic")}

    def test_locked_entry(self):
        # request access to an entry for permission the account does not have
        self.assertEqual(response.context["entry_text"], "Failed to find entry.")

    def test_lock_with_perm(self):
        # log TestAccount in, grant permission required, read the entry
        self.login()
        self.assertEqual(response.context["entry_text"], "unit test locked entrytext")

class CharacterCreateView(EvenniaWebTest):
    url_name = "character-create"

    @override_settings(MAX_NR_CHARACTERS=1)
    def test_valid_access_multisession_0(self):
        "Account1 with no characters should be able to create a new one"
        self.account.characters.remove(self.char1)

        # Login account

        # Post data for a new character
        data = {"db_key": "gannon", "desc": "Some dude."}

        response = self.client.post(reverse(self.url_name), data=data, follow=True)
        self.assertEqual(response.status_code, 200)

        # Make sure the character was actually created
            len(self.account.characters) == 1,
            "Account only has the following characters attributed to it: %s"
            % self.account.characters,
        )

    @override_settings(MAX_NR_CHARACTERS=5)
    def test_valid_access_multisession_2(self):
        "Account1 should be able to create multiple new characters"

            len(self.account.characters) > 1,
        )

class CharacterPuppetView(EvenniaWebTest):
    url_name = "character-puppet"

        return {"pk": self.char1.pk, "slug": slugify(self.char1.name)}

    def test_invalid_access(self):
        "Account1 should not be able to puppet Account2:Char2"

        # Try to access puppet page for char2
        kwargs = {"pk": self.char2.pk, "slug": slugify(self.char2.name)}
        response = self.client.get(reverse(self.url_name, kwargs=kwargs), follow=True)
            response.status_code >= 400,
            "Invalid access should return a 4xx code-- either obj not found or permission denied!"
            " (Returned %s)" % response.status_code,
        )

class CharacterListView(EvenniaWebTest):
    url_name = "characters"

class CharacterManageView(EvenniaWebTest):
    url_name = "character-manage"

class CharacterUpdateView(EvenniaWebTest):
    url_name = "character-update"

    def test_valid_access(self):
        "Account1 should be able to update Account1:Char1"

        # Try to access update page for char1

        # Try to update char1 desc
        data = {"db_key": self.char1.db_key, "desc": "Just a regular type of dude."}
        response = self.client.post(
            reverse(self.url_name, kwargs=self.get_kwargs()), data=data, follow=True
        )

        # Make sure the change was made successfully
        self.assertEqual(self.char1.db.desc, data["desc"])

        "Account1 should not be able to update Account2:Char2"

        # Try to access update page for char2
        self.assertEqual(response.status_code, 403)

class CharacterDeleteView(EvenniaWebTest):
    url_name = "character-delete"

        "Account1 should be able to delete Account1:Char1"

        # Try to access delete page for char1

        # Proceed with deleting it
        data = {"value": "yes"}
        )

        # Make sure it deleted
            self.char1 in self.account.characters,
            "Char1 is still in Account playable characters list.",
        )

        "Account1 should not be able to delete Account2:Char2"

        # Try to access delete page for char2

"""
Views for managing accounts.

"""

from django.contrib import messages
from django.urls import reverse_lazy

from evennia.web.website import forms

from .mixins import EvenniaCreateView, TypeclassMixin

class AccountMixin(TypeclassMixin):
    """
    This is used to grant abilities to classes it is added to.

    Any view class with this in its inheritance list will be modified to work
    with Account objects instead of generic Objects or otherwise.

    """

    # -- Django constructs --
    model = class_from_module(
        settings.BASE_ACCOUNT_TYPECLASS, fallback=settings.FALLBACK_ACCOUNT_TYPECLASS
    )
    form_class = forms.AccountForm

class AccountCreateView(AccountMixin, EvenniaCreateView):
    """
    Account creation view.

    """

    template_name = "website/registration/register.html"
    success_url = reverse_lazy("login")

    def form_valid(self, form):
        """
        Django hook, modified for Evennia.

        This hook is called after a valid form is submitted.

        When an account creation form is submitted and the data is deemed valid,
        proceeds with creating the Account object.

        """
        # Get values provided
        username = form.cleaned_data["username"]
        password = form.cleaned_data["password1"]
        email = form.cleaned_data.get("email", "")

        # Create account. This also runs all validations on the username/password.
        account, errs = self.typeclass.create(username=username, password=password, email=email)

            # password validation happens earlier, only username checks appear here.
            form.add_error("username", ", ".join(errs))
            return self.form_invalid(form)
        else:
            # Inform user of success
            messages.success(
                self.request, f"Your account '{account.name}' was successfully created!"
            )
            return HttpResponseRedirect(self.success_url)

from functools import total_ordering, wraps

    """
    Decorator that converts a method with a single self argument into a
    property cached on the instance.

    A cached property can be made out of an existing method:
    (e.g. ``url = cached_property(get_absolute_url)``).
    """

    name = None

    def func(instance):
            "Cannot use cached_property instance without calling "
            "__set_name__() on it."
        )

    def __init__(self, func, name=None):
        from django.utils.deprecation import RemovedInDjango50Warning

        if name is not None:
                "The name argument is deprecated as it's unnecessary as of "
                "Python 3.6.",
                RemovedInDjango50Warning,
            )
        self.real_func = func
        self.__doc__ = getattr(func, "__doc__")

        if self.name is None:
            self.name = name
            self.func = self.real_func
        elif name != self.name:
                "(%r and %r)." % (self.name, name)
            )

        """
        Call the function and put the return value in instance.__dict__ so that
        subsequent attribute access on the instance returns the cached value
        instead of calling cached_property.__get__().
        """
        res = instance.__dict__[self.name] = self.func(instance)
        return res

class classproperty:
    """
    Decorator that converts a method with a single cls argument into a property
    that can be accessed directly from the class.
    """

    def __init__(self, method=None):
        self.fget = method

        return self.fget(cls)

    def getter(self, method):

class Promise:
    """
    Base class for the proxy class created in the closure of the lazy function.
    It's used to recognize promises in code.
    """

    pass

def lazy(func, *resultclasses):
    """
    Turn any callable into a lazy evaluated callable. result classes or types
    is required -- at least one is needed so that the automatic forcing of
    the lazy evaluation code is triggered. Results are not memoized; the
    function is evaluated on every access.
    """

    @total_ordering
    class __proxy__(Promise):
        """
        Encapsulate a function call and act as a proxy for methods that are
        called on the result of that function. The function is not evaluated
        until one of the methods on the result is called.
        """

        __prepared = False

        def __init__(self, args, kw):
            self.__args = args
            self.__kw = kw
            if not self.__prepared:
                self.__prepare_class__()
            self.__class__.__prepared = True

        def __reduce__(self):
            return (
                _lazy_proxy_unpickle,
                (func, self.__args, self.__kw) + resultclasses,
            )

        def __repr__(self):
            return repr(self.__cast())

        def __prepare_class__(cls):
            for resultclass in resultclasses:
                for type_ in resultclass.mro():
                    for method_name in type_.__dict__:
                        # All __promise__ return the same wrapper method, they
                        # look up the correct implementation when called.
                        if hasattr(cls, method_name):
                            continue
                        meth = cls.__promise__(method_name)
                        setattr(cls, method_name, meth)
            cls._delegate_bytes = bytes in resultclasses
            cls._delegate_text = str in resultclasses
            if cls._delegate_bytes and cls._delegate_text:
                    "Cannot call lazy() with both bytes and text return types."
                )
            if cls._delegate_text:
                cls.__str__ = cls.__text_cast
            elif cls._delegate_bytes:
                cls.__bytes__ = cls.__bytes_cast

        def __promise__(cls, method_name):
            # Builds a wrapper around some magic method
            def __wrapper__(self, *args, **kw):
                # Automatically triggers the evaluation of a lazy value and
                # applies the given magic method of the result type.
                res = func(*self.__args, **self.__kw)
                return getattr(res, method_name)(*args, **kw)

            return __wrapper__

        def __text_cast(self):
            return func(*self.__args, **self.__kw)

        def __bytes_cast(self):
            return bytes(func(*self.__args, **self.__kw))

        def __bytes_cast_encoded(self):
            return func(*self.__args, **self.__kw).encode()

        def __cast(self):
            if self._delegate_bytes:
                return self.__bytes_cast()
            elif self._delegate_text:
                return self.__text_cast()
            else:
                return func(*self.__args, **self.__kw)

        def __str__(self):
            # object defines __str__(), so __prepare_class__() won't overload
            # a __str__() method from the proxied class.
            return str(self.__cast())

            if isinstance(other, Promise):
                other = other.__cast()
            return self.__cast() == other

            return self.__cast() < other

        def __hash__(self):
            return hash(self.__cast())

        def __mod__(self, rhs):
            if self._delegate_text:
                return str(self) % rhs
            return self.__cast() % rhs

        def __add__(self, other):
            return self.__cast() + other

        def __radd__(self, other):
            return other + self.__cast()

        def __deepcopy__(self, memo):
            # Instances of this class are effectively immutable. It's just a
            # collection of functions. So we don't need to do anything
            # complicated for copying.
            memo[id(self)] = self

    @wraps(func)
    def __wrapper__(*args, **kw):
        # Creates the proxy object, instead of the actual value.
        return __proxy__(args, kw)

    return __wrapper__

def _lazy_proxy_unpickle(func, args, kwargs, *resultclasses):
    return lazy(func, *resultclasses)(*args, **kwargs)

def lazystr(text):
    """
    Shortcut for the common case of a lazy callable that returns str.
    """
    return lazy(str, str)(text)

def keep_lazy(*resultclasses):
    """
    A decorator that allows a function to be called with one or more lazy
    arguments. If none of the args are lazy, the function is evaluated
    immediately, otherwise a __proxy__ is returned that will evaluate the
    function when needed.
    """
    if not resultclasses:
        raise TypeError("You must pass at least one argument to keep_lazy().")

        lazy_func = lazy(func, *resultclasses)

        @wraps(func)
        def wrapper(*args, **kwargs):
            if any(
                isinstance(arg, Promise)
                for arg in itertools.chain(args, kwargs.values())
            ):
                return lazy_func(*args, **kwargs)
            return func(*args, **kwargs)

def keep_lazy_text(func):
    """
    A decorator for functions that accept lazy arguments and return text.
    """
    return keep_lazy(str)(func)

empty = object()

def new_method_proxy(func):
    def inner(self, *args):
            self._setup()
        return func(_wrapped, *args)

    inner._mask_wrapped = False
    return inner

class LazyObject:
    """
    A wrapper for another class that can be used to delay instantiation of the
    wrapped class.

    By subclassing, you have the opportunity to intercept and alter the
    instantiation. If you don't need to do that, use SimpleLazyObject.
    """

    # Avoid infinite recursion when tracing __init__ (#19456).
    _wrapped = None

        # Note: if a subclass overrides __init__(), it will likely need to
        # override __copy__() and __deepcopy__() as well.
        self._wrapped = empty

    def __getattribute__(self, name):
            # Avoid recursion when getting wrapped object.
            return super().__getattribute__(name)
        value = super().__getattribute__(name)
        # If attribute is a proxy method, raise an AttributeError to call
        # __getattr__() and use the wrapped object method.
        if not getattr(value, "_mask_wrapped", True):

    __getattr__ = new_method_proxy(getattr)

            # Assign to __dict__ to avoid infinite __setattr__ loops.
            self.__dict__["_wrapped"] = value
        else:
            if self._wrapped is empty:
                self._setup()
            setattr(self._wrapped, name, value)

            raise TypeError("can't delete _wrapped.")
        delattr(self._wrapped, name)

    def _setup(self):
        """
        Must be implemented by subclasses to initialize the wrapped object.
        """
        raise NotImplementedError(
            "subclasses of LazyObject must provide a _setup() method"
        )

    # Because we have messed with __class__ below, we confuse pickle as to what
    # class we are pickling. We're going to have to initialize the wrapped
    # object to successfully pickle it, so we might as well just pickle the
    # wrapped object since they're supposed to act the same way.
    #
    # Unfortunately, if we try to simply act like the wrapped object, the ruse
    # will break down when pickle gets our id(). Thus we end up with pickle
    # thinking, in effect, that we are a distinct object from the wrapped
    # object, but with the same __dict__. This can cause problems (see #25389).
    #
    # So instead, we define our own __reduce__ method and custom unpickler. We
    # pickle the wrapped object as the unpickler's argument, so that pickle
    # will pickle it normally, and then the unpickler simply returns its
        return (unpickle_lazyobject, (self._wrapped,))

    def __copy__(self):
            # If uninitialized, copy the wrapper. Use type(self), not
            # self.__class__, because the latter is proxied.
            return type(self)()
        else:
            # If initialized, return a copy of the wrapped object.
            return copy.copy(self._wrapped)

            # We have to use type(self), not self.__class__, because the
            # latter is proxied.
            result = type(self)()
            memo[id(self)] = result
        return copy.deepcopy(self._wrapped, memo)

    __bytes__ = new_method_proxy(bytes)
    __str__ = new_method_proxy(str)
    __bool__ = new_method_proxy(bool)

    # Introspection support
    __dir__ = new_method_proxy(dir)

    # Need to pretend to be the wrapped class, for the sake of objects that
    # care about this (especially in equality tests)
    __class__ = property(new_method_proxy(operator.attrgetter("__class__")))
    __eq__ = new_method_proxy(operator.eq)
    __lt__ = new_method_proxy(operator.lt)
    __gt__ = new_method_proxy(operator.gt)
    __ne__ = new_method_proxy(operator.ne)
    __hash__ = new_method_proxy(hash)

    # List/Tuple/Dictionary methods support
    __getitem__ = new_method_proxy(operator.getitem)
    __setitem__ = new_method_proxy(operator.setitem)
    __delitem__ = new_method_proxy(operator.delitem)
    __iter__ = new_method_proxy(iter)
    __len__ = new_method_proxy(len)
    __contains__ = new_method_proxy(operator.contains)

def unpickle_lazyobject(wrapped):
    """
    Used to unpickle lazy objects. Just return its argument, which will be the
    wrapped object.
    """

class SimpleLazyObject(LazyObject):
    """
    A lazy object initialized from any function.

    Designed for compound objects of unknown type. For builtins or objects of
    known type, use django.utils.functional.lazy.
    """

        """
        Pass in a callable that returns the object to be wrapped.

        If copies are made of the resulting SimpleLazyObject, which can happen
        in various circumstances within Django, then you must ensure that the
        callable can be safely run more than once and will return the same
        value.
        """
        self.__dict__["_setupfunc"] = func

        self._wrapped = self._setupfunc()

    # Return a meaningful representation of the lazy object for debugging
    # without evaluating the wrapped object.
            repr_attr = self._setupfunc
        else:
            repr_attr = self._wrapped
        return "<%s: %r>" % (type(self).__name__, repr_attr)

            # If uninitialized, copy the wrapper. Use SimpleLazyObject, not
            return SimpleLazyObject(self._setupfunc)
        else:

            # We have to use SimpleLazyObject, not self.__class__, because the
            result = SimpleLazyObject(self._setupfunc)

    __add__ = new_method_proxy(operator.add)

    @new_method_proxy
        return other + self

def partition(predicate, values):
    """
    Split the values into two sets, based on the return value of the function
    (True/False). e.g.:

        >>> partition(lambda x: x > 3, range(5))
        [0, 1, 2, 3], [4]
    """
    results = ([], [])
    for item in values:
        results[predicate(item)].append(item)

"""
Views for managing channels.

"""

from django.db.models.functions import Lower
from django.http import HttpResponseBadRequest
from django.views.generic import ListView

from .mixins import TypeclassMixin
from .objects import ObjectDetailView

class ChannelMixin(TypeclassMixin):
    """
    This is a "mixin", a modifier of sorts.

    with HelpEntry objects instead of generic Objects or otherwise.

    """

        settings.BASE_CHANNEL_TYPECLASS, fallback=settings.FALLBACK_CHANNEL_TYPECLASS
    )

    # -- Evennia constructs --
    page_title = "Channels"

    # What lock type to check for the requesting user, authenticated or not.
    # https://github.com/evennia/evennia/wiki/Locks#valid-access_types
    access_type = "listen"

        """
        Django hook; here we want to return a list of only those Channels
        and other documentation that the current user is allowed to see.

        Returns:
            queryset (QuerySet): List of Channels available to the user.

        """
        account = self.request.user

        # Get list of all Channels
        channels = self.typeclass.objects.all().iterator()

        # Now figure out which ones the current user is allowed to see
        bucket = [channel.id for channel in channels if channel.access(account, "listen")]

        # Re-query and set a sorted list
        filtered = self.typeclass.objects.filter(id__in=bucket).order_by(Lower("db_key"))

        return filtered

class ChannelListView(ChannelMixin, ListView):
    """
    Returns a list of channels that can be viewed by a user, authenticated
    or not.

    """

    paginate_by = 100
    template_name = "website/channel_list.html"

    page_title = "Channel Index"

    max_popular = 10

    def get_context_data(self, **kwargs):
        """
        Django hook; we override it to calculate the most popular channels.

        Returns:
            context (dict): Django context object

        """
        context = super().get_context_data(**kwargs)

        # Calculate which channels are most popular
        context["most_popular"] = sorted(
            list(self.get_queryset()),
            key=lambda channel: len(channel.subscriptions.all()),
        )[: self.max_popular]

        return context

class ChannelDetailView(ChannelMixin, ObjectDetailView):
    """
    Returns the log entries for a given channel.

    """

    template_name = "website/channel_detail.html"

    # What attributes of the object you wish to display on the page. Model-level
    # attributes will take precedence over identically-named db.attributes!
    # The order you specify here will be followed.
    attributes = ["name"]

    # How many log entries to read and display.
    max_num_lines = 10000

        """
        Django hook; before we can display the channel logs, we need to recall
        the logfile and read its lines.

        Returns:

        """
        # Get the parent context object, necessary first step
        channel = self.object

        # Get the filename this Channel is recording to
        filename = channel.get_log_filename()

        # Split log entries so we can filter by time
        bucket = []
        for log in (x.strip() for x in tail_log_file(filename, 0, self.max_num_lines)):
            if not log:
                continue
            try:
                time, msg = log.split(" [-] ")
                time_key = time.split(":")[0]
                # malformed log line. Skip.
                continue

            bucket.append({"key": time_key, "timestamp": time, "message": msg})

        # Add the processed entries to the context
        context["object_list"] = bucket

        # Get a list of unique timestamps by hour and sort them
        context["object_filters"] = sorted(set([x["key"] for x in bucket]))

    def get_object(self, queryset=None):
        """
        Override of Django hook that retrieves an object by slugified channel
        name.

        Returns:
            channel (Channel): Channel requested in the URL.

        """
        # Get the queryset for the help entries the user can access
        if not queryset:

        # Find the object in the queryset
        channel = slugify(self.kwargs.get("slug", ""))
        obj = next((x for x in queryset if slugify(x.db_key) == channel), None)

        # Check if this object was requested in a valid manner
        if not obj:
            raise HttpResponseBadRequest(
                "No %(verbose_name)s found matching the query"
                % {"verbose_name": queryset.model._meta.verbose_name}
            )

        return obj

"""
Views for manipulating Characters (children of Objects often used for
puppeting).

"""

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.base import RedirectView

from .objects import (
    ObjectCreateView,
    ObjectDeleteView,
    ObjectDetailView,
    ObjectUpdateView,
)

class CharacterMixin(TypeclassMixin):
    """

    with Character objects instead of generic Objects or otherwise.

    """

        settings.BASE_CHARACTER_TYPECLASS, fallback=settings.FALLBACK_CHARACTER_TYPECLASS
    )
    form_class = forms.CharacterForm
    success_url = reverse_lazy("character-manage")

        """
        This method will override the Django get_queryset method to only
        return a list of characters associated with the current authenticated
        user.

        Returns:
            queryset (QuerySet): Django queryset for use in the given view.

        """
        # Get IDs of characters owned by account
        ids = [getattr(x, "id") for x in account.characters if x]

        # Return a queryset consisting of those characters
        return self.typeclass.objects.filter(id__in=ids).order_by(Lower("db_key"))

class CharacterListView(LoginRequiredMixin, CharacterMixin, ListView):
    """
    This view provides a mechanism by which a logged-in player can view a list
    of all other characters.

    This view requires authentication by default as a nominal effort to prevent
    human stalkers and automated bots/scrapers from harvesting data on your users.

    """

    template_name = "website/character_list.html"

    page_title = "Character List"
    access_type = "view"

        """
        This method will override the Django get_queryset method to return a
        list of all characters (filtered/sorted) instead of just those limited
        to the account.

        Returns:

        """

        # Return a queryset consisting of characters the user is allowed to
        # see.
        ids = [
            obj.id for obj in self.typeclass.objects.all() if obj.access(account, self.access_type)
        ]

class CharacterPuppetView(LoginRequiredMixin, CharacterMixin, RedirectView, ObjectDetailView):
    """
    This view provides a mechanism by which a logged-in player can "puppet" one
    of their characters within the context of the website.

    It also ensures that any user attempting to puppet something is logged in,
    and that their intended puppet is one that they own.

    """

    def get_redirect_url(self, *args, **kwargs):
        """
        Django hook.

        This view returns the URL to which the user should be redirected after
        a passed or failed puppet attempt.

        Returns:
            url (str): Path to post-puppet destination.

        """
        # Get the requested character, if it belongs to the authenticated user
        char = self.get_object()

        # Get the page the user came from
        next_page = self.request.GET.get("next", self.success_url)

        if char:
            # If the account owns the char, store the ID of the char in the
            # Django request's session (different from Evennia session!).
            # We do this because characters don't serialize well.
            self.request.session["puppet"] = int(char.pk)
            messages.success(self.request, "You become '%s'!" % char)
        else:
            # If the puppeting failed, clear out the cached puppet value
            self.request.session["puppet"] = None
            messages.error(self.request, "You cannot become '%s'." % char)

        return next_page

class CharacterManageView(LoginRequiredMixin, CharacterMixin, ListView):
    """
    This view provides a mechanism by which a logged-in player can browse,
    edit, or delete their own characters.

    """

    paginate_by = 10
    template_name = "website/character_manage_list.html"

    page_title = "Manage Characters"

class CharacterUpdateView(CharacterMixin, ObjectUpdateView):
    """
    This view provides a mechanism by which a logged-in player (enforced by
    ObjectUpdateView) can edit the attributes of a character they own.

    """

    form_class = forms.CharacterUpdateForm
    template_name = "website/character_form.html"

class CharacterDetailView(CharacterMixin, ObjectDetailView):
    """
    This view provides a mechanism by which a user can view the attributes of
    a character, owned by them or not.

    """

    template_name = "website/object_detail.html"

    # What attributes to display for this object
    attributes = ["name", "desc"]

        """
        list of all characters the user may access.

        Returns:

        """

        # see.
        ids = [
        ]

class CharacterDeleteView(CharacterMixin, ObjectDeleteView):
    """
    ObjectDeleteView) can delete a character they own.

    """

    # using the character form fails there
    form_class = forms.EvenniaForm

class CharacterCreateView(CharacterMixin, ObjectCreateView):
    """
    ObjectCreateView) can create a new character.

    """

        """

        When an character creation form is submitted and the data is deemed valid,
        proceeds with creating the Character object.

        """
        # Get account object creating the character
        character = None

        # Get attributes from the form
        self.attributes = {k: form.cleaned_data[k] for k in form.cleaned_data.keys()}
        charname = self.attributes.pop("db_key")
        description = self.attributes.pop("desc")
        # Create a character
        character, errors = self.typeclass.create(charname, account, description=description)

        if errors:
            # Echo error messages to the user
            [messages.error(self.request, x) for x in errors]

            # Assign attributes from form
            for key, value in self.attributes.items():
                setattr(character.db, key, value)

            # Return the user to the character management page, unless overridden
            messages.success(self.request, "Your character '%s' was created!" % character.name)

        else:
            # Call the Django "form failed" hook
            messages.error(self.request, "Your character could not be created.")

"""
Error views.

"""

def to_be_implemented(request):
    """
    A notice letting the user know that this particular feature hasn't been
    implemented yet.
    """

    pagevars = {"page_title": "To Be Implemented..."}

    return render(request, "tbi.html", pagevars)

"""
Views to manipulate help entries.

Multi entry object type supported added by DaveWithTheNiceHat 2021
    Pull Request #2429
"""
from django.views.generic import DetailView, ListView

def get_help_category(help_entry, slugify_cat=True):
    """Returns help category.

    Args:
        help_entry (HelpEntry, FileHelpEntry or Command): Help entry instance.
        slugify_cat (bool): If true the return string is slugified. Default is True.

    Notes:
        If an entry does not have a `help_category` attribute, DEFAULT_HELP_CATEGORY from the
        settings file is used.
        If the entry does not have attribute 'web_help_entries'. One is created with a slugified
        copy of the attribute help_category. This attribute is used for sorting the entries for the
        help index (ListView) page.

    Returns:
        help_category (str): The category for the help entry.
    """
    help_category = getattr(help_entry, "help_category", None)
    if not help_category:
        help_category = getattr(help_entry, "db_help_category", DEFAULT_HELP_CATEGORY)
    # if one does not exist, create a category for ease of use with web views html templates
    if not hasattr(help_entry, "web_help_category"):
        setattr(help_entry, "web_help_category", slugify(help_category))
    help_category = help_category.lower()
    return slugify(help_category) if slugify_cat else help_category

def get_help_topic(help_entry):
    """Get the topic of the help entry.

    Args:

    Returns:
        help_topic (str): The topic of the help entry. Default is 'unknown_topic'.
    """
    help_topic = getattr(help_entry, "key", None)
    # if object has no key, assume it is a db help entry.
    if not help_topic:
        help_topic = getattr(help_entry, "db_key", "unknown_topic")
    # if one does not exist, create a key for ease of use with web views html templates
    if not hasattr(help_entry, "web_help_key"):
        setattr(help_entry, "web_help_key", slugify(help_topic))
    return help_topic.lower()

def can_read_topic(cmd_or_topic, account):
    """Check if an account or puppet has read access to a help entry.

    Args:
        cmd_or_topic (Command, HelpEntry or FileHelpEntry): The topic/command to test.
        account: the account or puppet checking for access.

    Returns:
        bool: If command can be viewed or not.

    Notes:
        This uses the 'read' lock. If no 'read' lock is defined, the topic is assumed readable
        by all.
        Even if this returns False, the entry will still be visible in the help index unless
        `can_list_topic` is also returning False.
    """
    if inherits_from(cmd_or_topic, "evennia.commands.command.Command"):
        return cmd_or_topic.auto_help and cmd_or_topic.access(account, "read", default=True)
    else:
        return cmd_or_topic.access(account, "read", default=True)

def collect_topics(account):
    """Collect help topics from all sources (cmd/db/file).

    Args:
        account (Character or Account): Account or Character to collect help topics from.

    Returns:
        cmd_help_topics (dict): contains Command instances.
        db_help_topics (dict): contains HelpEntry instances.
        file_help_topics (dict): contains FileHelpEntry instances
    """

    # collect commands of account and all puppets
    # skip a command if an entry is recorded with the same topics, category and help entry
    cmd_help_topics = []
    if not str(account) == "AnonymousUser":
        # create list of account and account's puppets
        puppets = account.characters.all() + [account]
        # add the account's and puppets' commands to cmd_help_topics list
        for puppet in puppets:
            for cmdset in puppet.cmdset.get():
                # removing doublets in cmdset, caused by cmdhandler
                # having to allow doublet commands to manage exits etc.
                cmdset.make_unique(puppet)
                # retrieve all available commands and database / file-help topics.
                # also check the 'cmd:' lock here
                for cmd in cmdset:
                    # skip the command if the puppet does not have access
                    if not cmd.access(puppet, "cmd"):
                        continue
                    # skip the command if the puppet does not have read access
                    if not can_read_topic(cmd, puppet):
                        continue
                    # skip the command if it's help entry already exists in the topic list
                    entry_exists = False
                    for verify_cmd in cmd_help_topics:
                        if (
                            verify_cmd.key
                            and cmd.key
                            and verify_cmd.help_category == cmd.help_category
                            and verify_cmd.__doc__ == cmd.__doc__
                        ):
                            entry_exists = True
                            break
                    if entry_exists:
                        continue
                    # add this command to the list
                    cmd_help_topics.append(cmd)

    # Verify account has read access to filehelp entries and gather them into a dictionary
    file_help_topics = {
        topic.key.lower().strip(): topic
        for topic in FILE_HELP_ENTRIES.all()
        if can_read_topic(topic, account)
    }

    # Verify account has read access to database entries and gather them into a dictionary
    db_help_topics = {
        for topic in HelpEntry.objects.all()
    }

    # Collect commands into a dictionary, read access verified at puppet level
    cmd_help_topics = {
        cmd.auto_help_display_key if hasattr(cmd, "auto_help_display_key") else cmd.key: cmd
        for cmd in cmd_help_topics
    }

    return cmd_help_topics, db_help_topics, file_help_topics

class HelpMixin:
    """

    """

    page_title = "Help"

        """
        Django hook; here we want to return a list of only those HelpEntries

        Returns:
            queryset (list): List of Help entries available to the user.

        """

        # collect all help entries
        cmd_help_topics, db_help_topics, file_help_topics = collect_topics(account)

        # merge the entries
        all_entries = list(all_topics.values())

        # sort the entries
        all_entries.sort(key=get_help_topic)  # sort alphabetically
        all_entries.sort(key=get_help_category)  # group by categories

        return all_entries

class HelpListView(HelpMixin, ListView):
    """
    Returns a list of help entries that can be viewed by a user, authenticated
    or not.

    """

    paginate_by = 500
    template_name = "website/help_list.html"

    page_title = "Help Index"

class HelpDetailView(HelpMixin, DetailView):
    """
    Returns the detail page for a given help entry.

    """

    # the html template to use when contructing the detail page for a help topic
    template_name = "website/help_detail.html"

    @property
    def page_title(self):
        # Makes sure the page has a sensible title.
        topic = get_help_topic(obj)
        return f"{topic} detail"

        """
        Adds navigational data to the template to let browsers go to the next
        or previous entry in the help list.

        Returns:

        """

        # get a full query set
        full_set = self.get_queryset()

        # Get the object in question
        obj = self.get_object(full_set)

        # filter non related categories from the query set
        obj_category = get_help_category(obj)
        category_set = []
        for entry in full_set:
            entry_category = get_help_category(entry)
            if entry_category.lower() == obj_category.lower():
                category_set.append(entry)
        context["topic_list"] = category_set

        # Find the index position of the given obj in the category set
        objs = list(category_set)
        for i, x in enumerate(objs):
            if obj is x:
                break

        # Find the previous and next topics, if either exist
        try:
            assert i + 1 <= len(objs) and objs[i + 1] is not obj
            context["topic_next"] = objs[i + 1]
        except:
            context["topic_next"] = None

        try:
            assert i - 1 >= 0 and objs[i - 1] is not obj
            context["topic_previous"] = objs[i - 1]
        except:
            context["topic_previous"] = None

        # Get the help entry text
        text = "Failed to find entry."
        if inherits_from(obj, "evennia.commands.command.Command"):
            text = obj.__doc__
        elif inherits_from(obj, "evennia.help.models.HelpEntry"):
            text = obj.db_entrytext
        elif inherits_from(obj, "evennia.help.filehelp.FileHelpEntry"):
            text = obj.entrytext
        text = strip_ansi(text)  # remove ansii markups
        context["entry_text"] = text.strip()

        """
        Override of Django hook that retrieves an object by category and topic
        instead of pk and slug.

        Args:
            queryset (list): A list of help entry objects.

        Returns:
            entry (HelpEntry, FileHelpEntry or Command): HelpEntry requested in the URL.

        """

        if hasattr(self, "obj"):
            return getattr(self, "obj", None)

        # get the category and topic requested
        category = slugify(self.kwargs.get("category", ""))
        topic = slugify(self.kwargs.get("topic", ""))

        obj = None
        for entry in queryset:
            # continue to next entry if the topics do not match
            entry_topic = get_help_topic(entry)
            if not slugify(entry_topic) == topic:
                continue
            # if the category also matches, object requested is found
            if slugify(entry_category) == category:
                obj = entry
                break

        if not obj:
            return HttpResponseBadRequest(f"No ({category}/{topic})s found matching the query.")
        else:
            # cache the object if one was found

        return obj

"""
The main index page, including the game stats

"""

from django.views.generic import TemplateView

def _gamestats():
    """
    Generate a the gamestat context for the main index page
    """
    # Some misc. configurable stuff.
    # TODO: Move this to either SQL or settings.py based configuration.
    fpage_account_limit = 4

    # A QuerySet of the most recently connected accounts.
    recent_users = AccountDB.objects.get_recently_connected_accounts()
    nplyrs_conn_recent = len(recent_users) or "none"
    nplyrs = AccountDB.objects.num_total_accounts() or "none"
    nplyrs_reg_recent = len(AccountDB.objects.get_recently_created_accounts()) or "none"
    nsess = evennia.SESSION_HANDLER.account_count()
    # nsess = len(AccountDB.objects.get_connected_accounts()) or "no one"

    nobjs = ObjectDB.objects.count()
    nobjs = nobjs or 1  # fix zero-div error with empty database
    Character = class_from_module(
    )
    nchars = Character.objects.all_family().count()
    Room = class_from_module(
        settings.BASE_ROOM_TYPECLASS, fallback=settings.FALLBACK_ROOM_TYPECLASS
    )
    nrooms = Room.objects.all_family().count()
    Exit = class_from_module(
        settings.BASE_EXIT_TYPECLASS, fallback=settings.FALLBACK_EXIT_TYPECLASS
    )
    nexits = Exit.objects.all_family().count()
    nothers = nobjs - nchars - nrooms - nexits

    pagevars = {
        "page_title": "Front Page",
        "accounts_connected_recent": recent_users[:fpage_account_limit],
        "num_accounts_connected": nsess or "no one",
        "num_accounts_registered": nplyrs or "no",
        "num_accounts_connected_recent": nplyrs_conn_recent or "no",
        "num_accounts_registered_recent": nplyrs_reg_recent or "no one",
        "num_rooms": nrooms or "none",
        "num_exits": nexits or "no",
        "num_objects": nobjs or "none",
        "num_characters": nchars or "no",
        "num_others": nothers or "no",
    }
    return pagevars

class EvenniaIndexView(TemplateView):
    """
    This is a basic example of a Django class-based view, which are functionally
    very similar to Evennia Commands but differ in structure. Commands are used
    to interface with users using a terminal client. Views are used to interface
    with users using a web browser.

    To use a class-based view, you need to have written a template in HTML, and
    then you write a view like this to tell Django what values to display on it.

    While there are simpler ways of writing views using plain functions (and
    Evennia currently contains a few examples of them), just like Commands,
    writing views as classes provides you with more flexibility-- you can extend
    classes and change things to suit your needs rather than having to copy and
    paste entire code blocks over and over. Django also comes with many default
    views for displaying things, all of them implemented as classes.

    This particular example displays the index page.

    """

    # Tell the view what HTML template to use for the page
    template_name = "website/index.html"

    # This method tells the view what data should be displayed on the template.
        """
        This is a common Django method. Think of this as the website
        equivalent of the Evennia Command.func() method.

        If you just want to display a static page with no customization, you
        don't need to define this method-- just create a view, define
        template_name and you're done.

        The only catch here is that if you extend or overwrite this method,
        you'll always want to make sure you call the parent method to get a
        context object. It's just a dict, but it comes prepopulated with all
        sorts of background data intended for display on the page.

        You can do whatever you want to it, but it must be returned at the end
        of this method.

            any (any): Passed through.

        Returns:
            context (dict): Dictionary of data you want to display on the page.

        """
        # Always call the base implementation first to get a context object

        # Add game statistics and other pagevars
        context.update(_gamestats())

"""
These are mixins for class-based views, granting functionality.

"""
from django.views.generic import DetailView
from django.views.generic.edit import CreateView, DeleteView, UpdateView

class TypeclassMixin:
    """

    Django views typically work with classes called "models." Evennia objects
    are an enhancement upon these Django models and are called "typeclasses."
    But Django itself has no idea what a "typeclass" is.

    For the sake of mitigating confusion, any view class with this in its
    inheritance list will be modified to work with Evennia Typeclass objects or
    Django models interchangeably.

    """

    @property
    def typeclass(self):
        return self.model

    @typeclass.setter
    def typeclass(self, value):
        self.model = value

class EvenniaCreateView(CreateView, TypeclassMixin):
    """
    This view extends Django's default CreateView.

    CreateView is used for creating new objects, be they Accounts, Characters or
    otherwise.

    """

    @property
        return "Create %s" % self.typeclass._meta.verbose_name.title()

class EvenniaDetailView(DetailView, TypeclassMixin):
    """
    This view extends Django's default DetailView.

    DetailView is used for displaying objects, be they Accounts, Characters or

    """

    @property
        return "%s Detail" % self.typeclass._meta.verbose_name.title()

class EvenniaUpdateView(UpdateView, TypeclassMixin):
    """
    This view extends Django's default UpdateView.

    UpdateView is used for updating objects, be they Accounts, Characters or

    """

    @property
        return "Update %s" % self.typeclass._meta.verbose_name.title()

class EvenniaDeleteView(DeleteView, TypeclassMixin):
    """
    This view extends Django's default DeleteView.

    DeleteView is used for deleting objects, be they Accounts, Characters or

    """

    @property
        return "Delete %s" % self.typeclass._meta.verbose_name.title()

"""
Views for managing a specific object)

"""

from django.http import HttpResponseBadRequest, HttpResponseRedirect

from .mixins import (
    EvenniaCreateView,
    EvenniaDeleteView,
    EvenniaDetailView,
    EvenniaUpdateView,
)

class ObjectDetailView(EvenniaDetailView):
    """
    This is an important view.

    Any view you write that deals with displaying, updating or deleting a
    specific object will want to inherit from this. It provides the mechanisms
    by which to retrieve the object and make sure the user requesting it has
    permissions to actually *do* things to it.

    """

    #
    # Choose what class of object this view will display. Note that this should
    # be an actual Python class (i.e. do `from typeclasses.characters import
    # Character`, then put `Character`), not an Evennia typeclass path
    # (i.e. `typeclasses.characters.Character`).
    #
    # So when you extend it, this line should look simple, like:
    # model = Object
        settings.BASE_OBJECT_TYPECLASS, fallback=settings.FALLBACK_OBJECT_TYPECLASS
    )

    # What HTML template you wish to use to display this page.

    #

        """
        Adds an 'attributes' list to the request context consisting of the
        attributes specified at the class level, and in the order provided.

        Django views do not provide a way to reference dynamic attributes, so
        we have to grab them all before we render the template.

        Returns:

        """
        # Get the base Django context object

        # Create an ordered dictionary to contain the attribute map
        attribute_list = OrderedDict()

        for attribute in self.attributes:
            # Check if the attribute is a core fieldname (name, desc)
            if attribute in self.typeclass._meta._property_names:
                attribute_list[attribute.title()] = getattr(obj, attribute, "")

            # Check if the attribute is a db attribute (char1.db.favorite_color)
            else:
                attribute_list[attribute.title()] = getattr(obj.db, attribute, "")

        # Add our attribute map to the Django request context, so it gets
        # displayed on the template
        context["attribute_list"] = attribute_list

        # Return the comprehensive context object

        """
        Override of Django hook that provides some important Evennia-specific
        functionality.

        Evennia does not natively store slugs, so where a slug is provided,
        calculate the same for the object and make sure it matches.

        This also checks to make sure the user has access to view/edit/delete
        this object!

        """
        # A queryset can be provided to pre-emptively limit what objects can
        # possibly be returned. For example, you can supply a queryset that
        # only returns objects whose name begins with "a".

        # Get the object, ignoring all checks and filters for now
        obj = self.typeclass.objects.get(pk=self.kwargs.get("pk"))

        if slugify(obj.name) != self.kwargs.get(self.slug_url_kwarg):
            )

        # Check if the requestor account has permissions to access object
        if not obj.access(account, self.access_type):
            raise PermissionDenied("You are not authorized to %s this object." % self.access_type)

        # Get the object, if it is in the specified queryset
        obj = super().get_object(queryset)

        return obj

class ObjectCreateView(LoginRequiredMixin, EvenniaCreateView):
    """

    Any view you write that deals with creating a specific object will want to
    inherit from this. It provides the mechanisms by which to make sure the user
    requesting creation of an object is authenticated, and provides a sane
    default title for the page.

    """

    )

class ObjectDeleteView(LoginRequiredMixin, ObjectDetailView, EvenniaDeleteView):
    """
    This is an important view for obvious reasons!

    Any view you write that deals with deleting a specific object will want to
    requesting deletion of an object is authenticated, and that they have
    permissions to delete the requested object.

    """

    )
    template_name = "website/object_confirm_delete.html"

    access_type = "delete"

class ObjectUpdateView(LoginRequiredMixin, ObjectDetailView, EvenniaUpdateView):
    """

    Any view you write that deals with updating a specific object will want to
    requesting editing of an object is authenticated, and that they have
    permissions to edit the requested object.

    This functions slightly different from default Django UpdateViews in that
    it does not update core model fields, *only* object attributes!

    """

    )

    access_type = "edit"

    def get_success_url(self):
        """

        Can be overridden to return any URL you want to redirect the user to
        after the object is successfully updated, but by default it goes to the
        object detail page so the user can see their changes reflected.

        """
        if self.success_url:
            return self.success_url
        return self.object.web_get_detail_url()

    def get_initial(self):
        """

        Prepopulates the update form field values based on object db attributes.

        Returns:
            data (dict): Dictionary of key:value pairs containing initial form
                data.

        """
        # Get the object we want to update

        # Get attributes
        data = {k: getattr(obj.db, k, "") for k in self.form_class.base_fields}

        # Get model fields
        data.update({k: getattr(obj, k, "") for k in self.form_class.Meta.fields})

        """
        Override of Django hook.

        Updates object attributes based on values submitted.

        This is run when the form is submitted and the data on it is deemed
        valid-- all values are within expected ranges, all strings contain
        valid characters and lengths, etc.

        This method is only called if all values for the fields submitted
        passed form validation, so at this point we can assume the data is
        validated and sanitized.

        """
        # Get the attributes after they've been cleaned and validated
        data = {k: v for k, v in form.cleaned_data.items() if k not in self.form_class.Meta.fields}

        # Update the object attributes
        for key, value in data.items():
            self.object.attributes.add(key, value)
            messages.success(self.request, "Successfully updated '%s' for %s." % (key, self.object))

        # Do not return super().form_valid; we don't want to update the model
        # instance, just its attributes.
        return HttpResponseRedirect(self.get_success_url())