From beffaa4753a8077b1ccb48d22744ab7a0194ddc9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2019 12:16:25 +0200 Subject: [PATCH] Start adding MSSP wizard info --- CHANGELOG.md | 2 + evennia/server/connection_wizard.py | 410 ++++++++++++++++++++++++++++ evennia/server/evennia_launcher.py | 15 +- evennia/server/portal/mssp.py | 108 +------- evennia/settings_default.py | 50 +++- 5 files changed, 484 insertions(+), 101 deletions(-) create mode 100644 evennia/server/connection_wizard.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cab847f74d..4d62cbe6e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -169,6 +169,8 @@ - Evscaperoom - a full puzzle engine for making multiplayer escape rooms in Evennia. Used to make the entry for the MUD-Coder's Guild's 2019 Game Jam with the theme "One Room", where it ranked #1. +- Evennia game-index client no longer a contrib - moved into server core and configured with new + setting `GAME_INDEX_ENABLED`. - The `extended_room` contrib saw some backwards-incompatible refactoring: + All commands now begin with `CmdExtendedRoom`. So before it was `CmdExtendedLook`, now it's `CmdExtendedRoomLook` etc. diff --git a/evennia/server/connection_wizard.py b/evennia/server/connection_wizard.py new file mode 100644 index 0000000000..28432c3fc1 --- /dev/null +++ b/evennia/server/connection_wizard.py @@ -0,0 +1,410 @@ +""" +Link Evennia to external resources (wizard plugin for evennia_launcher) + +""" +import sys +import pprint +from django.conf import settings +from evennia.utils.utils import list_to_string + +class ConnectionWizard(object): + + def __init__(self): + 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) + + while True: + 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. + + Kwargs: + 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]? ") + + while True: + resp = input(prompt).lstrip().lower() + if not resp: + 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" + elif resp.lower() in ("quit", "q"): + sys.exit() + + def ask_choice(self, prompt="> ", options=None, default=None): + """ + Ask multiple-choice question, get response inline. + + Kwargs: + 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)) + self.display(opt_txt) + + while True: + resp = input(prompt).strip() + + if not resp: + if default: + return options[int(default)] + if resp.lower() in ("quit", "q"): + sys.exit() + 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, verify=True, max_len=None): + """ + Get arbitrary input inline. + + Kwargs: + prompt (str): The display prompt. + default (str, optional): If empty input, use this. + + Returns: + inp (str): The input given, or default. + + """ + while True: + resp = input(prompt).strip() + if not resp and default: + resp = str(default) + + if resp.lower() == 'none': + resp = '' + 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 verify: + self.display(resp) + if max_len: + nlen = len(resp) + if nlen > max_len: + self.display(f" This text is {nlen} characters long. Max is {max_len}.") + continue + ok = input("\n Is the above looking correct? [Y]/N: ") + if ok.lower() in ("n", "no"): + continue + elif ok.lower() in ('q', 'quit'): + sys.exit() + return resp + + +def node_start(wizard): + text = """ + This wizard helps activate external networks with Evennia. It will create + a config that will be attached to the bottom of the game settings file. + + Use `quit` at any time to abort and throw away any changes. + """ + options = { + "1": ("Add game to Evennia game index (also for closed dev games)", + node_game_index_start, {}), + "2": ("Add MSSP information (for mud-list crawlers)", + node_mssp_start, {}), + "3": ("View and Save created settings", + node_view_and_apply_settings, {}), + } + + wizard.display(text) + wizard.ask_node(options) + + +# Evennia game index + + +def node_game_index_start(wizard, **kwargs): + text = f""" + 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. Just tell us you + are out there and make 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!). + """ + + wizard.display(text) + 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: + {status_default} + """ + + options = ["pre-alpha", "alpha", "beta", "launched"] + + wizard.display(text) + wizard.game_index_listing['game_status'] = \ + wizard.ask_choice("Select one: ", options) + + # 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. + + Current value: + {sdesc_default} + """ + + wizard.display(text) + wizard.game_index_listing['short_description'] = \ + wizard.ask_input(default=sdesc_default, max_len=255) + + # 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. + + Current value: + {long_default} + """ + + wizard.display(text) + 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). + + Current value: + {listing_default} + """ + + wizard.display(text) + wizard.game_index_listing['listing_contact'] = \ + wizard.ask_input(default=listing_default) + + # 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. + + Current value: + {hostname_default} + """ + + wizard.display(text) + 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. + + Write 'None' if you are not offering public telnet connections at this time. + + Current value: + {port_default} + """ + + wizard.display(text) + 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. + + Wtite 'None' if you are not offering a publicly visible website at this time. + + Current value: + {website_default} + """ + + wizard.display(text) + 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) + + Wtite 'None' if you don't want to list a publicly accessible webclient. + + Current value: + {webclient_default} + """ + + wizard.display(text) + 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): + + text = f""" + MSSP (Mud Server Status Protocol) allows 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 many fields is info about your game. + + To use MSSP you should generally have a publicly open game that external + players can connect to. + """ + + wizard.mssp_table + + +# Admin + +def _save_changes(wizard): + """ + Perform the save + """ + print("saving!") + +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_txt = "No changes to save for Game Index." + if hasattr(wizard, "game_index_listing"): + if wizard.game_index_listing != settings.GAME_INDEX_LISTING: + game_index_txt = "No changes to save for Game Index." + else: + game_index_txt = pp.pformat(wizard.game_index_listing) + saves = True + + text = game_index_txt + + print("- Game index:\n" + text) + + if saves: + if wizard.ask_yesno("Do you want to save these settings?") == 'yes': + _save_changes(wizard) + else: + print("Cancelled. Returning ...") + wizard.ask_continue() + node_start(wizard) + diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index b7e02eded2..3fd53c4451 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -1823,6 +1823,16 @@ def run_dummyrunner(number_of_dummies): pass +def run_connect_wizard(): + """ + Run the linking wizard, for adding new external connections. + + """ + from .connection_wizard import ConnectionWizard, node_start + wizard = ConnectionWizard() + node_start(wizard) + + def list_settings(keys): """ Display the server settings. We only display the Evennia specific @@ -2082,7 +2092,7 @@ def main(): init_game_directory(CURRENT_DIR, check_db=True) run_menu() elif option in ('status', 'info', 'start', 'istart', 'ipstart', 'reload', 'restart', 'reboot', - 'reset', 'stop', 'sstop', 'kill', 'skill', 'sstart'): + 'reset', 'stop', 'sstop', 'kill', 'skill', 'sstart', 'connections'): # operate the server directly if not SERVER_LOGFILE: init_game_directory(CURRENT_DIR, check_db=True) @@ -2120,6 +2130,9 @@ def main(): print("This option is not supported on Windows.") else: kill(SERVER_PIDFILE, 'Server') + elif option == 'connections': + run_connect_wizard() + elif option != "noop": # pass-through to django manager, but set things up first check_db = False diff --git a/evennia/server/portal/mssp.py b/evennia/server/portal/mssp.py index cf80298c77..dff20a37a5 100644 --- a/evennia/server/portal/mssp.py +++ b/evennia/server/portal/mssp.py @@ -19,10 +19,6 @@ MSSP_VAR = b'\x01' MSSP_VAL = 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(object): """ Implements the MSSP protocol. Add this to a variable on the telnet @@ -86,109 +82,34 @@ class Mssp(object): # Required fields - "NAME": "Evennia", + "NAME": settings.SERVERNAME, "PLAYERS": self.get_player_count, "UPTIME": self.get_uptime, - # Generic + "PORT": list(reversed(settings.TELNET_PORTS)), # most important port should be last in list + # Evennia auto-filled "CRAWL DELAY": "-1", - - "HOSTNAME": "", # current or new hostname - "PORT": ["4000"], # most important port should be last in list - "CODEBASE": "Evennia", - "CONTACT": "", # email for contacting the mud - "CREATED": "", # year MUD was created - "ICON": "", # url to icon 32x32 or larger; <32kb. - "IP": "", # current or new IP address - "LANGUAGE": "", # name of language used, e.g. English - "LOCATION": "", # full English name of server country - "MINIMUM AGE": "0", # set to 0 if not applicable - "WEBSITE": "www.evennia.com", - - # Categorisation - - "FAMILY": "Custom", # evennia goes under 'Custom' - "GENRE": "None", # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction - "GAMEPLAY": "None", # Adventure, Educational, Hack and Slash, None, - # Player versus Player, Player versus Environment, - # Roleplaying, Simulation, Social or Strategy - "STATUS": "Open Beta", # Alpha, Closed Beta, Open Beta, Live - "GAMESYSTEM": "Custom", # D&D, d20 System, World of Darkness, etc. Use Custom if homebrew - "SUBGENRE": "None", # LASG, Medieval Fantasy, World War II, Frankenstein, - # Cyberpunk, Dragonlance, etc. Or None if not available. - - # World - - "AREAS": "0", - "HELPFILES": "0", - "MOBILES": "0", - "OBJECTS": "0", - "ROOMS": "0", # use 0 if room-less - "CLASSES": "0", # use 0 if class-less - "LEVELS": "0", # use 0 if level-less - "RACES": "0", # use 0 if race-less - "SKILLS": "0", # use 0 if skill-less - - # Protocols set to 1 or 0) - + "CODEBASE": utils.get_evennia_version(mode='pretty'), + "FAMILY": "Custom", "ANSI": "1", - "GMCP": "0", + "GMCP": "1" if settings.TELNET_OOB_ENABLED else "0", "ATCP": "0", - "MCCP": "0", + "MCCP": "1", "MCP": "0", - "MSDP": "0", + "MSDP": "1" if settings.TELNET_OOB_ENABLED else "0", "MSP": "0", - "MXP": "0", + "MXP": "1", "PUEBLO": "0", - "SSL": "1", + "SSL": "1" if settings.SSL_ENABLED else "0", "UTF-8": "1", "ZMP": "0", - "VT100": "0", - "XTERM 256 COLORS": "0", - - # Commercial set to 1 or 0) - - "PAY TO PLAY": "0", - "PAY FOR PERKS": "0", - - # Hiring set to 1 or 0) - - "HIRING BUILDERS": "0", - "HIRING CODERS": "0", - - # Extended variables - - # World - - "DBSIZE": "0", - "EXITS": "0", - "EXTRA DESCRIPTIONS": "0", - "MUDPROGS": "0", - "MUDTRIGS": "0", - "RESETS": "0", - - # Game (set to 1, 0 or one of the given alternatives) - - "ADULT MATERIAL": "0", - "MULTICLASSING": "0", - "NEWBIE FRIENDLY": "0", - "PLAYER CITIES": "0", - "PLAYER CLANS": "0", - "PLAYER CRAFTING": "0", - "PLAYER GUILDS": "0", - "EQUIPMENT SYSTEM": "None", # "None", "Level", "Skill", "Both" - "MULTIPLAYING": "None", # "None", "Restricted", "Full" - "PLAYERKILLING": "None", # "None", "Restricted", "Full" - "QUEST SYSTEM": "None", # "None", "Immortal Run", "Automated", "Integrated" - "ROLEPLAYING": "None", # "None", "Accepted", "Encouraged", "Enforced" - "TRAINING SYSTEM": "None", # "None", "Level", "Skill", "Both" - "WORLD ORIGINALITY": "None", # "All Stock", "Mostly Stock", "Mostly Original", "All Original" + "VT100": "1", + "XTERM 256 COLORS": "1", } # update the static table with the custom one - if MSSPTable_CUSTOM: - self.mssp_table.update(MSSPTable_CUSTOM) + self.mssp_table.update(settings.MSSP_TABLE) varlist = '' for variable, value in self.mssp_table.items(): @@ -196,7 +117,8 @@ class Mssp(object): value = value() if utils.is_iter(value): for partval in value: - varlist += MSSP_VAR + bytes(variable, 'utf-8') + MSSP_VAL + bytes(partval, 'utf-8') + varlist += (MSSP_VAR + bytes(variable, 'utf-8') + + MSSP_VAL + bytes(partval, 'utf-8')) else: varlist += MSSP_VAR + bytes(variable, 'utf-8') + MSSP_VAL + bytes(value, 'utf-8') diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 47a8cba271..7dbc578726 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -343,9 +343,6 @@ SERVER_SERVICES_PLUGIN_MODULES = ["server.conf.server_services_plugins"] # main Evennia Portal application when the Portal is initiated. # It will be called last in the startup sequence. PORTAL_SERVICES_PLUGIN_MODULES = ["server.conf.portal_services_plugins"] -# Module holding MSSP meta data. This is used by MUD-crawlers to determine -# what type of game you are running, how many accounts you have etc. -MSSP_META_MODULE = "server.conf.mssp" # Module for web plugins. WEB_PLUGINS_MODULE = "server.conf.web_plugins" # Tuple of modules implementing lock functions. All callable functions @@ -682,6 +679,10 @@ DEFAULT_CHANNELS = [ # While the MudInfo channel will also receieve this, this channel is meant for non-staffers. CHANNEL_CONNECTINFO = None +###################################################################### +# External Connections +###################################################################### + # The Evennia Game Index is a dynamic listing of Evennia games. You can add your game # to this list also if it is in closed pre-alpha development. GAME_INDEX_ENABLED = False @@ -692,15 +693,50 @@ GAME_INDEX_LISTING = { 'long_description': '', 'listing_contact': '', # email 'telnet_hostname': '', # mygame.com - 'telnet_port': 1234, + 'telnet_port': '', # 1234 'game_website': '', # http://mygame.com 'web_client_url': '' # http://mygame.com/webclient } +# MSSP (Mud Server Status Protocol) is used by MUD-crawlers to determine +# what type of game you are running, how many players you have etc. Some of +# this (like server name and current number of players) is handled by Evennia +# automatically, other fields are set by you. +MSSP_TABLE = { + "HOSTNAME": "", "PORT": "", # telnet host/port + "CONTACT": "", "CREATED": "", # email, year of game creation + "IP": "", "ICON": "", # ip address; url to icon 32x32or larger; <32kb. + "LANGUAGE": "English", "LOCATION": "", # server country location, like "Sweden" + "MINIMUM AGE": "0", # set to 0 if not applicable + "WEBSITE": "www.evennia.com", + "GENRE": "None", # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction + "GAMEPLAY": "None", # Adventure, Educational, Hack and Slash, None, + # Player versus Player, Player versus Environment, + # Roleplaying, Simulation, Social or Strategy + "STATUS": "Alpha", # Alpha, Closed Beta, Open Beta, Live + "GAMESYSTEM": "Custom", # D&D, d20 System, World of Darkness, etc. Use Custom if homebrew + "SUBGENRE": "None", # Freeform, like LASG, Medieval Fantasy, World War II, Frankenstein, + # Cyberpunk, Dragonlance, etc. Or None if not available. + # use 0 if not applicable or off + "AREAS": "0", "HELPFILES": "0", "MOBILES": "0", "OBJECTS": "0", + "ROOMS": "0", "CLASSES": "0", "LEVELS": "0", "RACES": "0", "SKILLS": "0", -###################################################################### -# External Channel connections -###################################################################### + "PAY TO PLAY": "0", "PAY FOR PERKS": "0", + "HIRING BUILDERS": "0", "HIRING CODERS": "0", + + "DBSIZE": "0", "EXITS": "0", "EXTRA DESCRIPTIONS": "0", + "MUDPROGS": "0", "MUDTRIGS": "0", "RESETS": "0", + + "ADULT MATERIAL": "0", "MULTICLASSING": "0", "NEWBIE FRIENDLY": "0", "PLAYER CITIES": "0", + "PLAYER CLANS": "0", "PLAYER CRAFTING": "0", "PLAYER GUILDS": "0", + "EQUIPMENT SYSTEM": "None", # "None", "Level", "Skill", "Both" + "MULTIPLAYING": "None", # "None", "Restricted", "Full" + "PLAYERKILLING": "None", # "None", "Restricted", "Full" + "QUEST SYSTEM": "None", # "None", "Immortal Run", "Automated", "Integrated" + "ROLEPLAYING": "None", # "None", "Accepted", "Encouraged", "Enforced" + "TRAINING SYSTEM": "None", # "None", "Level", "Skill", "Both" + "WORLD ORIGINALITY": "None", # "All Stock", "Mostly Stock", "Mostly Original", "All Original" +} # Note: You do *not* have to make your MUD open to # the public to use the external connections, they