mirror of
https://github.com/evennia/evennia.git
synced 2026-03-29 20:17:16 +02:00
521 lines
16 KiB
Python
521 lines
16 KiB
Python
"""
|
|
Link Evennia to external resources (wizard plugin for evennia_launcher)
|
|
|
|
"""
|
|
import sys
|
|
from os import path
|
|
import pprint
|
|
from django.conf import settings
|
|
from evennia.utils.utils import list_to_string, mod_import
|
|
|
|
|
|
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 + "\n")
|
|
|
|
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.
|
|
|
|
Keyword Args:
|
|
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.
|
|
|
|
Keyword Args:
|
|
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 + "\n")
|
|
|
|
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, validator=None):
|
|
"""
|
|
Get arbitrary input inline.
|
|
|
|
Keyword Args:
|
|
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.
|
|
|
|
"""
|
|
while True:
|
|
resp = input(prompt).strip()
|
|
|
|
if not resp and default:
|
|
resp = str(default)
|
|
|
|
if resp.lower() in ("q", "quit"):
|
|
sys.exit()
|
|
|
|
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"):
|
|
sys.exit()
|
|
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!).
|
|
"""
|
|
|
|
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 (return to keep):
|
|
{status_default}
|
|
"""
|
|
|
|
options = ["pre-alpha", "alpha", "beta", "launched"]
|
|
|
|
wizard.display(text)
|
|
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()
|
|
return False
|
|
return True
|
|
|
|
wizard.display(text)
|
|
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.
|
|
|
|
Current value:
|
|
{sdesc_default}
|
|
"""
|
|
|
|
def sdesc_validator(inp):
|
|
tmax = 255
|
|
tlen = len(inp)
|
|
if tlen > tmax:
|
|
print(f"The short desc must be shorter than {tmax} characters (was {tlen}).")
|
|
wizard.ask_continue()
|
|
return False
|
|
return True
|
|
|
|
wizard.display(text)
|
|
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.
|
|
|
|
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}
|
|
"""
|
|
|
|
def contact_validator(inp):
|
|
if not inp or "@" not in inp:
|
|
print("This should be an email and cannot be blank.")
|
|
wizard.ask_continue()
|
|
return False
|
|
return True
|
|
|
|
wizard.display(text)
|
|
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.
|
|
|
|
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.
|
|
|
|
Write '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)
|
|
|
|
Write '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):
|
|
|
|
mssp_module = mod_import(settings.MSSP_META_MODULE or "server.conf.mssp")
|
|
try:
|
|
filename = mssp_module.__file__
|
|
except AttributeError:
|
|
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.display(text)
|
|
wizard.ask_continue()
|
|
node_start(wizard)
|
|
|
|
|
|
# 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.")
|
|
wizard.ask_continue()
|
|
node_start(wizard)
|