From ab052b8301ad5c67f6fb5b1938138a3409791280 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 8 Jan 2015 19:56:54 +0100 Subject: [PATCH] Fixed bugs, made server start. Still cannot reload. --- bin/evennia | 377 +++++++++++++------------- bin/evennia_runner.py | 12 +- evennia/__init__.py | 7 +- evennia/comms/comms.py | 253 ++++++++--------- evennia/server/portal/portal.py | 4 +- evennia/server/server.py | 4 +- evennia/settings_default.py | 2 +- game_template/server/conf/oobfuncs.py | 2 +- game_template/typeclasses/channels.py | 60 ++++ 9 files changed, 394 insertions(+), 327 deletions(-) create mode 100644 game_template/typeclasses/channels.py diff --git a/bin/evennia b/bin/evennia index 43e8069ef..e0c220ecd 100755 --- a/bin/evennia +++ b/bin/evennia @@ -291,6 +291,22 @@ ERROR_NODJANGO = \ # #------------------------------------------------------------ +def evennia_version(): + """ + Get the Evennia version info from the main package. + """ + version = "Unknown" + with open(os.path.join(EVENNIA_ROOT, "VERSION.txt"), 'r') as f: + version = f.read().strip() + try: + version = "%s (rev %s)" % (version, check_output("git rev-parse --short HEAD", shell=True, cwd=EVENNIA_ROOT).strip()) + except IOError: + pass + return version + +EVENNIA_VERSION = evennia_version() + + def check_main_evennia_dependencies(): """ Checks and imports the Evennia dependencies. This must be done @@ -354,22 +370,6 @@ def set_gamedir(path): sys.exit() -def evennia_version(): - """ - Get the Evennia version info from the main package. - """ - version = "Unknown" - with open(os.path.join(EVENNIA_ROOT, "VERSION.txt"), 'r') as f: - version = f.read().strip() - try: - version = "%s (rev %s)" % (version, check_output("git rev-parse --short HEAD", shell=True, cwd=EVENNIA_ROOT).strip()) - except IOError: - pass - return version - -EVENNIA_VERSION = evennia_version() - - def create_secret_key(): """ Randomly create the secret key for the settings file @@ -422,6 +422,166 @@ def create_game_directory(dirname): create_settings_file() +def create_superuser(): + print "\nCreate a superuser below. The superuser is Player #1, the 'owner' account of the server.\n" + django.core.management.call_command("createsuperuser", interactive=True) + + +def check_database(exit_on_error=False): + """ + Check database exists and has a superuser + """ + # Check so a database exists and is accessible + from django.db import connection + tables = connection.introspection.get_table_list(connection.cursor()) + if tables and u'players_playerdb' in tables: + # database exists and seems set up. Initialize evennia. + import evennia + evennia.init() + else: + if exit_on_error: + print ERROR_DATABASE.format(traceback=e) + sys.exit() + return False + + # Try to get Player#1 + from evennia.players.models import PlayerDB + try: + PlayerDB.objects.get(id=1) + except PlayerDB.DoesNotExist: + # no superuser yet. We need to create it. + create_superuser() + return True + + +def getenv(): + """ + Get current environment and add PYTHONPATH + """ + sep = ";" if os.name == 'nt' else ":" + env = os.environ.copy() + env['PYTHONPATH'] = sep.join(sys.path) + return env + + +def get_pid(pidfile): + """ + Get the PID (Process ID) by trying to access + an PID file. + """ + pid = None + if os.path.exists(pidfile): + f = open(pidfile, 'r') + 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. + """ + if os.path.exists(pidfile): + os.remove(pidfile) + + +def kill(pidfile, signal=SIG, succmsg="", errmsg="", restart_file=SERVER_RESTART, restart="reload"): + """ + 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. + """ + pid = get_pid(pidfile) + if pid: + if os.name == 'nt': + if sys.version < "2.7": + print "Windows requires Python 2.7 or higher for this operation." + return + os.remove(pidfile) + # set restart/norestart flag + if restart == 'reload': + django.core.management.call_command('collectstatic', interactive=False, verbosity=0) + with open(restart_file, 'w') as f: + f.write(str(restart)) + try: + os.kill(int(pid), signal) + except OSError: + print "Process %(pid)s could not be signalled. The PID file '%(pidfile)s' seems stale. Try removing it." % {'pid': pid, 'pidfile': pidfile} + return + print "Evennia:", succmsg + return + print "Evennia:", errmsg + + +def show_version_info(about=False): + """ + Display version info + """ + import os, sys + import twisted + import django + + 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(): + """ + 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. + """ + from django.conf import settings + def imp(path, split=True): + mod, fromlist = path, "None" + if split: + mod, fromlist = path.rsplit('.', 1) + __import__(mod, fromlist=[fromlist]) + + # core modules + imp(settings.COMMAND_PARSER) + imp(settings.SEARCH_AT_RESULT) + imp(settings.SEARCH_AT_MULTIMATCH_INPUT) + 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) + # cmdsets + + 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_PLAYER")) + 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"): + raise DeprecationWarning("settings.CHARACTER_DEFAULT_HOME should be renamed to DEFAULT_HOME. " \ + "See also settings.START_LOCATION (see evennia/settings_default.py).") + + 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_PLAYER, None): print "Warning: CMDSET_PLAYER failed to load" + # typeclasses + imp(settings.BASE_PLAYER_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) + + def init_game_directory(path): """ Try to analyze the given path to find settings.py - this defines @@ -450,12 +610,8 @@ def init_game_directory(path): print ERROR_SETTINGS sys.exit() - # testing the main library import. If there are errors in importing - # the main library, it should show here. - evennia = importlib.import_module("evennia") - - if check_database(): - evennia.init() + # this will both check the database and initialize the evennia dir. + check_database() # set up the Evennia executables and log file locations global SERVER_PY_FILE, PORTAL_PY_FILE @@ -517,107 +673,6 @@ def init_game_directory(path): print INFO_WINDOWS_BATFILE.format(twistd_path=twistd_path) -def getenv(): - """ - Get current environment and add PYTHONPATH - """ - sep = ";" if os.name == 'nt' else ":" - env = os.environ.copy() - env['PYTHONPATH'] = sep.join(sys.path) - return env - - -def create_database(): - print "\nCreating a database ...\n" - django.core.management.call_command("migrate", interactive=False) - print "\n ... database initialized.\n" - - -def create_superuser(): - print "\nCreate a superuser below. The superuser is Player #1, the 'owner' account of the server.\n" - django.core.management.call_command("createsuperuser", interactive=True) - - -def check_database(exit_on_error=False): - # Check so a database exists and is accessible - from django.db import DatabaseError - from evennia.players.models import PlayerDB - try: - PlayerDB.objects.get(id=1) - except DatabaseError, e: - if exit_on_error: - print ERROR_DATABASE.format(traceback=e) - sys.exit() - return False - except PlayerDB.DoesNotExist: - # no superuser yet. We need to create it. - create_superuser() - return True - - -def get_pid(pidfile): - """ - Get the PID (Process ID) by trying to access - an PID file. - """ - pid = None - if os.path.exists(pidfile): - f = open(pidfile, 'r') - 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. - """ - if os.path.exists(pidfile): - os.remove(pidfile) - - -def kill(pidfile, signal=SIG, succmsg="", errmsg="", restart_file=SERVER_RESTART, restart="reload"): - """ - 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. - """ - pid = get_pid(pidfile) - if pid: - if os.name == 'nt': - if sys.version < "2.7": - print "Windows requires Python 2.7 or higher for this operation." - return - os.remove(pidfile) - # set restart/norestart flag - if restart == 'reload': - django.core.management.call_command('collectstatic', interactive=False, verbosity=0) - f = open(restart_file, 'w') - f.write(str(restart)) - f.close() - try: - os.kill(int(pid), signal) - except OSError: - print "Process %(pid)s could not be signalled. The PID file '%(pidfile)s' seems stale. Try removing it." % {'pid': pid, 'pidfile': pidfile} - return - print "Evennia:", succmsg - return - print "Evennia:", errmsg - -def show_version_info(about=False): - """ - Display version info - """ - import os, sys - import twisted - import django - - 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 run_menu(): """ This launches an interactive menu. @@ -668,19 +723,19 @@ def run_menu(): if os.name == 'nt': print "This operation is not supported under Windows. Log into the game to restart/reload the server." return - kill(SERVER_PIDFILE, SIG, "Server reloaded.", errmsg % "Server", restart="reload") + kill(SERVER_PIDFILE, SIG, "Server reloaded.", errmsg % "Server", SERVER_RESTART, restart="reload") elif inp == 6: if os.name == 'nt': print "This operation is not supported under Windows." return - kill(PORTAL_PIDFILE, SIG, "Portal reloaded (or stopped if in daemon mode).", errmsg % "Portal", restart=True) + kill(PORTAL_PIDFILE, SIG, "Portal reloaded (or stopped if in daemon mode).", errmsg % "Portal", PORTAL_RESTART, restart=True) elif inp == 7: - kill(SERVER_PIDFILE, SIG, "Stopped Portal.", errmsg % "Portal", PORTAL_RESTART, restart=False) - kill(PORTAL_PIDFILE, SIG, "Stopped Server.", errmsg % "Server", restart="shutdown") + kill(PORTAL_PIDFILE, SIG, "Stopped Portal.", errmsg % "Portal", PORTAL_RESTART, restart=False) + kill(SERVER_PIDFILE, SIG, "Stopped Server.", errmsg % "Server", SERVER_RESTART, restart="shutdown") elif inp == 8: - kill(PORTAL_PIDFILE, SIG, "Stopped Server.", errmsg % "Server", restart="shutdown") + kill(SERVER_PIDFILE, SIG, "Stopped Server.", errmsg % "Server", SERVER_RESTART, restart="shutdown") elif inp == 9: - kill(SERVER_PIDFILE, SIG, "Stopped Portal.", errmsg % "Portal", PORTAL_RESTART, restart=False) + kill(PORTAL_PIDFILE, SIG, "Stopped Portal.", errmsg % "Portal", PORTAL_RESTART, restart=False) return else: print "Not a valid option." @@ -736,81 +791,27 @@ def server_operation(mode, service, interactive, profiler): print "Restarting from command line is not supported under Windows. Log into the game to restart." return if service == 'server': - kill(SERVER_PIDFILE, SIG, "Server reloaded.", errmsg % 'Server', restart="reload") + kill(SERVER_PIDFILE, SIG, "Server reloaded.", errmsg % 'Server', SERVER_RESTART, restart="reload") elif service == 'portal': print """ Note: Portal usually don't need to be reloaded unless you are debugging in interactive mode. If Portal was running in default Daemon mode, it cannot be restarted. In that case you have to restart it manually with 'evennia.py start portal' """ - kill(PORTAL_PIDFILE, SIG, "Portal reloaded (or stopped, if it was in daemon mode).", errmsg % 'Portal', PORTAL_RESTART) + kill(PORTAL_PIDFILE, SIG, "Portal reloaded (or stopped, if it was in daemon mode).", errmsg % 'Portal', PORTAL_RESTART, restart="reload") else: # all # default mode, only restart server - kill(SERVER_PIDFILE, SIG, "Server reload.", errmsg % 'Server', restart="reload") + kill(SERVER_PIDFILE, SIG, "Server reload.", errmsg % 'Server', SERVER_RESTART, restart="reload") elif mode == 'stop': # stop processes, avoiding reload if service == 'server': - kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', restart="shutdown") + kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', SERVER_RESTART, restart="shutdown") elif service == 'portal': kill(PORTAL_PIDFILE, SIG, "Portal stopped.", errmsg % 'Portal', PORTAL_RESTART, restart=False) else: kill(PORTAL_PIDFILE, SIG, "Portal stopped.", errmsg % 'Portal', PORTAL_RESTART, restart=False) - kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', restart="shutdown") - - -def error_check_python_modules(): - """ - 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. - """ - from django.conf import settings - def imp(path, split=True): - mod, fromlist = path, "None" - if split: - mod, fromlist = path.rsplit('.', 1) - __import__(mod, fromlist=[fromlist]) - - # core modules - imp(settings.COMMAND_PARSER) - imp(settings.SEARCH_AT_RESULT) - imp(settings.SEARCH_AT_MULTIMATCH_INPUT) - 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) - # cmdsets - - 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_PLAYER")) - 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"): - raise DeprecationWarning("settings.CHARACTER_DEFAULT_HOME should be renamed to DEFAULT_HOME. " \ - "See also settings.START_LOCATION (see evennia/settings_default.py).") - - 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_PLAYER, None): print "Warning: CMDSET_PLAYER failed to load" - # typeclasses - imp(settings.BASE_PLAYER_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) + kill(SERVER_PIDFILE, SIG, "Server stopped.", errmsg % 'Server', SERVER_RESTART, restart="shutdown") diff --git a/bin/evennia_runner.py b/bin/evennia_runner.py index 693eb7ece..686bfe090 100644 --- a/bin/evennia_runner.py +++ b/bin/evennia_runner.py @@ -1,10 +1,10 @@ #!/usr/bin/env python """ -This runner is controlled by evennia.py and should normally not be - launched directly. It manages the two main Evennia processes (Server - and Portal) and most importanly runs a passive, threaded loop that - makes sure to restart Server whenever it shuts down. +This runner is controlled by the evennia launcher and should normally +not be launched directly. It manages the two main Evennia processes +(Server and Portal) and most importanly runs a passive, threaded loop +that makes sure to restart Server whenever it shuts down. Since twistd does not allow for returning an optional exit code we need to handle the current reload state for server and portal with @@ -183,14 +183,14 @@ def start_services(server_argv, portal_argv): message, rc = processes.get() # restart only if process stopped cleanly - if (message == "server_stopped" and int(rc) == 0 and + if (message == "server_stopped" and not rc.returncode and get_restart_mode(SERVER_RESTART) in ("True", "reload", "reset")): print PROCESS_RESTART.format(component="Server") SERVER = thread.start_new_thread(server_waiter, (processes, )) continue # normally the portal is not reloaded since it's run as a daemon. - if (message == "portal_stopped" and int(rc) == 0 and + if (message == "portal_stopped" and not rc.returncode and get_restart_mode(PORTAL_RESTART) == "True"): print PROCESS_RESTART.format(component="Portal") PORTAL = thread.start_new_thread(portal_waiter, (processes, )) diff --git a/evennia/__init__.py b/evennia/__init__.py index a39757793..377dba5a3 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -21,7 +21,7 @@ DefaultObject = None DefaultCharacter = None DefaultRoom = None DefaultExit = None -Channel = None +DefaultChannel = None Script = None # Database models @@ -80,7 +80,8 @@ def init(): mod, fromlist = path.rsplit('.', 1) return __import__(mod, fromlist=[fromlist]) - global DefaultPlayer, DefaultObject, DefaultGuest, DefaultCharacter, DefaultRoom, DefaultExit, Channel, Script + global DefaultPlayer, DefaultObject, DefaultGuest, DefaultCharacter, \ + DefaultRoom, DefaultExit, DefaultChannel, Script global ObjectDB, PlayerDB, ScriptDB, ChannelDB, Msg global Command, default_cmds, syscmdkeys global search_object, search_script, search_player, search_channel, search_help @@ -93,7 +94,7 @@ def init(): from objects.objects import DefaultCharacter from objects.objects import DefaultRoom from objects.objects import DefaultExit - from comms.comms import Channel + from comms.comms import DefaultChannel from scripts.scripts import Script # Database models diff --git a/evennia/comms/comms.py b/evennia/comms/comms.py index df4b8f787..f2bcd2b86 100644 --- a/evennia/comms/comms.py +++ b/evennia/comms/comms.py @@ -10,7 +10,7 @@ from evennia.utils import logger from evennia.utils.utils import make_iter -class Channel(ChannelDB): +class DefaultChannel(ChannelDB): """ This is the base class for all Comms. Inherit from this to create different types of communication channels. @@ -113,78 +113,6 @@ class Channel(ChannelDB): from evennia.comms.channelhandler import CHANNELHANDLER CHANNELHANDLER.update() - def channel_prefix(self, msg=None, emit=False): - """ - How the channel should prefix itself for users. Return a string. - """ - return '[%s] ' % self.key - - def format_senders(self, senders=None): - """ - Function used to format a list of sender names. - - This function exists separately so that external sources can use - it to format source names in the same manner as normal object/player - names. - """ - if not senders: - return '' - return ', '.join(senders) - - def pose_transform(self, msg, sender_string): - """ - Detects if the sender is posing, and modifies the message accordingly. - """ - pose = False - message = msg.message - message_start = message.lstrip() - if message_start.startswith((':', ';')): - pose = True - message = message[1:] - if not message.startswith((':', "'", ',')): - if not message.startswith(' '): - message = ' ' + message - if pose: - return '%s%s' % (sender_string, message) - else: - return '%s: %s' % (sender_string, message) - - def format_external(self, msg, senders, emit=False): - """ - Used for formatting external messages. This is needed as a separate - operation because the senders of external messages may not be in-game - objects/players, and so cannot have things like custom user - preferences. - - senders should be a list of strings, each containing a sender. - msg should contain the body of the message to be sent. - """ - if not senders: - emit = True - if emit: - return msg.message - senders = ', '.join(senders) - return self.pose_transform(msg, senders) - - def format_message(self, msg, emit=False): - """ - Formats a message body for display. - - If emit is True, it means the message is intended to be posted detached - from an identity. - """ - # We don't want to count things like external sources as senders for - # the purpose of constructing the message string. - senders = [sender for sender in msg.senders if hasattr(sender, 'key')] - if not senders: - emit = True - if emit: - return msg.message - else: - senders = [sender.key for sender in msg.senders] - senders = ', '.join(senders) - return self.pose_transform(msg, senders) - def message_transform(self, msg, emit=False, prefix=True, sender_strings=None, external=False): """ @@ -199,57 +127,6 @@ class Channel(ChannelDB): msg.message = body return msg - def pre_join_channel(self, joiner): - """ - Run right before a channel is joined. If this returns a false value, - channel joining is aborted. - """ - return True - - def post_join_channel(self, joiner): - """ - Run right after an object or player joins a channel. - """ - return True - - def pre_leave_channel(self, leaver): - """ - Run right before a user leaves a channel. If this returns a false - value, leaving the channel will be aborted. - """ - return True - - def post_leave_channel(self, leaver): - """ - Run right after an object or player leaves a channel. - """ - pass - - def pre_send_message(self, msg): - """ - Run before a message is sent to the channel. - - This should return the message object, after any transformations. - If the message is to be discarded, return a false value. - """ - return msg - - def post_send_message(self, msg): - """ - Run after a message is sent to the channel. - """ - pass - - def at_init(self): - """ - 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 - def distribute_message(self, msg, online=False): """ Method for grabbing all listeners that a message should be sent to on @@ -328,3 +205,131 @@ class Channel(ChannelDB): """ self.msg(message, senders=senders, header=header, persistent=False) + + # hooks + + def channel_prefix(self, msg=None, emit=False): + + """ + How the channel should prefix itself for users. Return a string. + """ + return '[%s] ' % self.key + + def format_senders(self, senders=None): + """ + Function used to format a list of sender names. + + This function exists separately so that external sources can use + it to format source names in the same manner as normal object/player + names. + """ + if not senders: + return '' + return ', '.join(senders) + + def pose_transform(self, msg, sender_string): + """ + Detects if the sender is posing, and modifies the message accordingly. + """ + pose = False + message = msg.message + message_start = message.lstrip() + if message_start.startswith((':', ';')): + pose = True + message = message[1:] + if not message.startswith((':', "'", ',')): + if not message.startswith(' '): + message = ' ' + message + if pose: + return '%s%s' % (sender_string, message) + else: + return '%s: %s' % (sender_string, message) + + def format_external(self, msg, senders, emit=False): + """ + Used for formatting external messages. This is needed as a separate + operation because the senders of external messages may not be in-game + objects/players, and so cannot have things like custom user + preferences. + + senders should be a list of strings, each containing a sender. + msg should contain the body of the message to be sent. + """ + if not senders: + emit = True + if emit: + return msg.message + senders = ', '.join(senders) + return self.pose_transform(msg, senders) + + def format_message(self, msg, emit=False): + """ + Formats a message body for display. + + If emit is True, it means the message is intended to be posted detached + from an identity. + """ + # We don't want to count things like external sources as senders for + # the purpose of constructing the message string. + senders = [sender for sender in msg.senders if hasattr(sender, 'key')] + if not senders: + emit = True + if emit: + return msg.message + else: + senders = [sender.key for sender in msg.senders] + senders = ', '.join(senders) + return self.pose_transform(msg, senders) + + def pre_join_channel(self, joiner): + """ + Run right before a channel is joined. If this returns a false value, + channel joining is aborted. + """ + return True + + def post_join_channel(self, joiner): + """ + Run right after an object or player joins a channel. + """ + return True + + def pre_leave_channel(self, leaver): + """ + Run right before a user leaves a channel. If this returns a false + value, leaving the channel will be aborted. + """ + return True + + def post_leave_channel(self, leaver): + """ + Run right after an object or player leaves a channel. + """ + pass + + def pre_send_message(self, msg): + """ + Run before a message is sent to the channel. + + This should return the message object, after any transformations. + If the message is to be discarded, return a false value. + """ + return msg + + def post_send_message(self, msg): + """ + Run after a message is sent to the channel. + """ + pass + + def at_init(self): + """ + 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 + + diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index 395c89e93..38d165cf3 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -34,7 +34,7 @@ PORTAL_SERVICES_PLUGIN_MODULES = [mod_import(module) for module in make_iter(set if os.name == 'nt': # For Windows we need to handle pid files manually. - PORTAL_PIDFILE = os.path.join(settings.GAME_DIR, 'portal.pid') + PORTAL_PIDFILE = os.path.join(settings.GAME_DIR, "server", 'portal.pid') #------------------------------------------------------------ # Evennia Portal settings @@ -44,7 +44,7 @@ VERSION = get_evennia_version() SERVERNAME = settings.SERVERNAME -PORTAL_RESTART = os.path.join(settings.GAME_DIR, 'portal.restart') +PORTAL_RESTART = os.path.join(settings.GAME_DIR, "server", 'portal.restart') TELNET_PORTS = settings.TELNET_PORTS SSL_PORTS = settings.SSL_PORTS diff --git a/evennia/server/server.py b/evennia/server/server.py index 01ac9e1d6..6309c558d 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -53,10 +53,10 @@ _SA = object.__setattr__ if os.name == 'nt': # For Windows we need to handle pid files manually. - SERVER_PIDFILE = os.path.join(settings.GAME_DIR, 'server.pid') + SERVER_PIDFILE = os.path.join(settings.GAME_DIR, "server", 'server.pid') # a file with a flag telling the server to restart after shutdown or not. -SERVER_RESTART = os.path.join(settings.GAME_DIR, 'server.restart') +SERVER_RESTART = os.path.join(settings.GAME_DIR, "server", 'server.restart') # module containing hook methods called during start_stop SERVER_STARTSTOP_MODULE = mod_import(settings.AT_SERVER_STARTSTOP_MODULE) diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 08442e117..7993524f0 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -242,7 +242,7 @@ LOCK_FUNC_MODULES = ("evennia.locks.lockfuncs", "server.conf.lockfuncs",) # Module holding OOB (Out of Band) hook objects. This allows for customization # and expansion of which hooks OOB protocols are allowed to call on the server # protocols for attaching tracker hooks for when various object field change -OOB_PLUGIN_MODULES = ["evennia.server.oob_cmds", "server.conf.oob_cmds"] +OOB_PLUGIN_MODULES = ["evennia.server.oob_cmds", "server.conf.oobfuncs"] ###################################################################### # Default command sets diff --git a/game_template/server/conf/oobfuncs.py b/game_template/server/conf/oobfuncs.py index 8547ed325..3125c7ce8 100644 --- a/game_template/server/conf/oobfuncs.py +++ b/game_template/server/conf/oobfuncs.py @@ -32,5 +32,5 @@ a global function oob_error will be used as optional error management. """ # import the contents of the default msdp module -from src.server.oob_cmds import * +from evennia.server.oob_cmds import * diff --git a/game_template/typeclasses/channels.py b/game_template/typeclasses/channels.py new file mode 100644 index 000000000..3be9a6dde --- /dev/null +++ b/game_template/typeclasses/channels.py @@ -0,0 +1,60 @@ +""" +Channel + +The channel class represents the out-of-character chat-room usable by +Players in-game. It is mostly overloaded to change its appearance, but +channels can be used to implement many different forms of message +distribution systems. + +Note that sending data to channels are handled via the CMD_CHANNEL +syscommand (see evennia.syscmds). The sending should normally not need +to be modified. + +""" + +from evennia import DefaultChannel + +class Channel(DefaultChannel): + """ + Working methods: + at_channel_creation() - called once, when the channel is created + has_connection(player) - check if the given player listens to this channel + connect(player) - connect player to this channel + disconnect(player) - disconnect player from channel + access(access_obj, access_type='listen', default=False) - check the + access on this channel (default access_type is listen) + delete() - delete this channel + message_transform(msg, emit=False, prefix=True, + sender_strings=None, external=False) - called by + the comm system and triggers the hooks below + msg(msgobj, header=None, senders=None, sender_strings=None, + persistent=None, online=False, emit=False, external=False) - main + send method, builds and sends a new message to channel. + tempmsg(msg, header=None, senders=None) - wrapper for sending non-persistent + messages. + distribute_message(msg, online=False) - send a message to all + connected players on channel, optionally sending only + to players that are currently online (optimized for very large sends) + + Useful hooks: + channel_prefix(msg, emit=False) - how the channel should be + prefixed when returning to user. Returns a string + format_senders(senders) - should return how to display multiple + senders to a channel + pose_transform(msg, sender_string) - should detect if the + sender is posing, and if so, modify the string + format_external(msg, senders, emit=False) - format messages sent + from outside the game, like from IRC + format_message(msg, emit=False) - format the message body before + displaying it to the user. 'emit' generally means that the + message should not be displayed with the sender's name. + + pre_join_channel(joiner) - if returning False, abort join + post_join_channel(joiner) - called right after successful join + pre_leave_channel(leaver) - if returning False, abort leave + post_leave_channel(leaver) - called right after successful leave + pre_send_message(msg) - runs just before a message is sent to channel + post_send_message(msg) - called just after message was sent to channel + + """ + pass