From fb7875835679c532ee1247dbe0826c2b2ab17d99 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 20 Nov 2011 11:52:01 +0100 Subject: [PATCH] Implemented working MCCP (data compression) and MSSP (mud-listing crawler support). Moved all user-level customization modules from gamesrc/world to gamesrc/conf to reduce clutter. --- game/gamesrc/conf/__init__.py | 0 game/gamesrc/conf/at_initial_setup.py | 17 ++ .../{world => conf}/connection_screens.py | 4 +- game/gamesrc/conf/lockfuncs.py | 27 +++ game/gamesrc/conf/mssp.py | 121 ++++++++++++ game/gamesrc/conf/oobfuncs.py | 16 ++ src/commands/default/unloggedin.py | 4 +- src/server/amp.py | 4 +- src/server/mccp.py | 11 +- src/server/mssp.py | 187 ++++++++++++++++++ src/server/session.py | 4 +- src/server/sessionhandler.py | 19 ++ src/server/telnet.py | 47 +++-- src/settings_default.py | 12 +- src/utils/utils.py | 40 +++- 15 files changed, 465 insertions(+), 48 deletions(-) create mode 100644 game/gamesrc/conf/__init__.py create mode 100644 game/gamesrc/conf/at_initial_setup.py rename game/gamesrc/{world => conf}/connection_screens.py (96%) create mode 100644 game/gamesrc/conf/lockfuncs.py create mode 100644 game/gamesrc/conf/mssp.py create mode 100644 game/gamesrc/conf/oobfuncs.py create mode 100644 src/server/mssp.py diff --git a/game/gamesrc/conf/__init__.py b/game/gamesrc/conf/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/game/gamesrc/conf/at_initial_setup.py b/game/gamesrc/conf/at_initial_setup.py new file mode 100644 index 0000000000..cf15ea02ae --- /dev/null +++ b/game/gamesrc/conf/at_initial_setup.py @@ -0,0 +1,17 @@ +""" +Custom at_initial_setup method. This allows you to hook special +modifications to the initial server startup process. Note that this +will only be run once - when the server starts up for the very first +time! It is called last in the startup process and can thus be used to +overload things that happened before it. + +The module must contain a global function at_initial_setup(). This +will be called without arguments. Note that tracebacks in this module +will be QUIETLY ignored, so make sure to check it well to make sure it +does what you expect it to. + +This module is selected by settings.AT_INITIAL_SETUP_HOOK_MODULE. +""" + +def at_initial_setup(): + pass diff --git a/game/gamesrc/world/connection_screens.py b/game/gamesrc/conf/connection_screens.py similarity index 96% rename from game/gamesrc/world/connection_screens.py rename to game/gamesrc/conf/connection_screens.py index 79539b552c..96294d449a 100644 --- a/game/gamesrc/world/connection_screens.py +++ b/game/gamesrc/conf/connection_screens.py @@ -16,9 +16,9 @@ # reboot or reload the server to make them available. # +from src.utils import utils from src.commands.connection_screen import DEFAULT_SCREEN -#from src.utils import utils # # CUSTOM_SCREEN = \ # """{b=============================================================={n @@ -32,10 +32,8 @@ from src.commands.connection_screen import DEFAULT_SCREEN # Enter {whelp{n for more info. {wlook{n will re-load this screen. #{b=============================================================={n""" % utils.get_evennia_version() - # # A suggested alternative screen for the Menu login system -# from src.utils import utils # MENU_SCREEN = \ # """{b=============================================================={n # Welcome to {gEvennnia{n, version %s! diff --git a/game/gamesrc/conf/lockfuncs.py b/game/gamesrc/conf/lockfuncs.py new file mode 100644 index 0000000000..05a84663d2 --- /dev/null +++ b/game/gamesrc/conf/lockfuncs.py @@ -0,0 +1,27 @@ +""" + +This is an example module for holding custom lock funcs, used in +in-game locks. The modules available to use as lockfuncs are defined +in the tuple settings.LOCK_FUNC_MODULES. + +All functions defined globally in this module are assumed to be +available for use in lockstrings to determine access. See +http://code.google.com/p/evennia/wiki/Locks + +A lock function is always called with two arguments, accessing_obj and +accessed_obj, followed by any number of arguments. All possible +arguments should be handled (excess ones calling magic (*args, +**kwargs) to avoid errors). The lock function should handle all +eventual tracebacks by logging the error and returning False. + +See many more examples of lock functions in src.locks.lockfuncs. +""" + +def myfalse(accessing_obj, accessed_obj, *args, **kwargs): + """ + called in lockstring with myfalse(). + A simple logger that always returns false. Prints to stdout + for simplicity, should use utils.logger for real operation. + """ + print "%s tried to access %s. Access denied." % (accessing_obj, accessed_obj) + return False diff --git a/game/gamesrc/conf/mssp.py b/game/gamesrc/conf/mssp.py new file mode 100644 index 0000000000..8cdf0805ba --- /dev/null +++ b/game/gamesrc/conf/mssp.py @@ -0,0 +1,121 @@ +# +# MSSP (Mud Server Status Protocol) meta information +# +# MUD website listings (that you have registered with) can use this +# information to keep up-to-date with your game stats as you change +# them. Also number of currently active players and uptime will +# automatically be reported. You don't have to fill in everything +# (and most are not used by all crawlers); leave the default +# if so needed. You need to @reload the game before updated +# information is made available to crawlers (reloading does not +# affect uptime). +# + +MSSPTable = { + + # Required fieldss + + "NAME": "Evennia", + + # Generic + + "CRAWL DELAY": "-1", # limit how often crawler updates the listing. -1 for no limit + + "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": "", # 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 + "INTERMUD": "IMC2", # evennia supports IMC2. + "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) + + "ANSI": "1", + "GMCP": "0", + "MCCP": "0", + "MCP": "0", + "MSDP": "0", + "MSP": "0", + "MXP": "0", + "PUEBLO": "0", + "UTF-8": "1", + "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 or 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" + + # Protocols (only change if you added/removed something manually) + + "ATCP": "0", + "MSDP": "0", + "MCCP": "1", + "SSL": "1", + "UTF-8": "1", + "ZMP": "0", + "XTERM 256 COLORS": "0"} diff --git a/game/gamesrc/conf/oobfuncs.py b/game/gamesrc/conf/oobfuncs.py new file mode 100644 index 0000000000..2dd2863721 --- /dev/null +++ b/game/gamesrc/conf/oobfuncs.py @@ -0,0 +1,16 @@ +# +# Example module holding functions for out-of-band protocols to +# import and map to given commands from the client. This module +# is selected by settings.OOB_FUNC_MODULE. +# +# All functions defined global in this module will be available +# for the oob system to call. They will be called with a session/character +# as first argument (depending on if the session is logged in or not), +# following by any number of extra arguments. The return value will +# be packed and returned to the oob protocol and can be on any form. +# + +def testoob(character, *args, **kwargs): + "Simple test function" + print "Called testoob: %s" % val + return "testoob did stuff to the input string '%s'!" % val diff --git a/src/commands/default/unloggedin.py b/src/commands/default/unloggedin.py index e71f637c17..c89bc63d22 100644 --- a/src/commands/default/unloggedin.py +++ b/src/commands/default/unloggedin.py @@ -237,13 +237,13 @@ class CmdUnconnectedLook(MuxCommand): string = ansi.parse_ansi(screen) self.caller.msg(string) except Exception, e: - self.caller.msg(e) + self.caller.msg("Error in CONNECTION_SCREEN MODULE: " + str(e)) self.caller.msg("Connect screen not found. Enter 'help' for aid.") class CmdUnconnectedHelp(MuxCommand): """ This is an unconnected version of the help command, - for simplicity. It shows a pane or info. + for simplicity. It shows a pane of info. """ key = "help" aliases = ["h", "?"] diff --git a/src/server/amp.py b/src/server/amp.py index 6d2c21ff46..4b0dd8e2c8 100644 --- a/src/server/amp.py +++ b/src/server/amp.py @@ -210,14 +210,14 @@ class AMPProtocol(amp.AMP): """ if hasattr(self.factory, "portal"): sessdata = self.factory.portal.sessions.get_all_sync_data() - print sessdata + #print sessdata self.call_remote_ServerAdmin(0, "PSYNC", data=sessdata) if get_restart_mode(SERVER_RESTART): msg = _(" ... Server restarted.") self.factory.portal.sessions.announce_all(msg) - + self.factory.portal.sessions.at_server_connection() # Error handling diff --git a/src/server/mccp.py b/src/server/mccp.py index 04580a2a4d..7e583945b8 100644 --- a/src/server/mccp.py +++ b/src/server/mccp.py @@ -7,10 +7,12 @@ 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 -instead. Note: On modern hardware you are not likely to notice the +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 @@ -21,8 +23,7 @@ FLUSH = zlib.Z_SYNC_FLUSH def mccp_compress(protocol, data): "Handles zlib compression, if applicable" if hasattr(protocol, 'zlib'): - data = protocol.zlib.compress(data) - data += protocol.zlib.flush(FLUSH) + return protocol.zlib.compress(data) + protocol.zlib.flush(FLUSH) return data class Mccp(object): @@ -47,8 +48,7 @@ class Mccp(object): def no_mccp(self, option): """ If client doesn't support mccp, don't do anything. - """ - print "deactivating mccp ..." + """ if hasattr(self.protocol, 'zlib'): del self.protocol.zlib self.protocol.protocol_flags['MCCP'] = False @@ -58,7 +58,6 @@ class Mccp(object): The client supports MCCP. Set things up by creating a zlib compression stream. """ - print "activating mccp ..." self.protocol.protocol_flags['MCCP'] = True self.protocol.requestNegotiation(MCCP, '') self.protocol.zlib = zlib.compressobj(9) diff --git a/src/server/mssp.py b/src/server/mssp.py new file mode 100644 index 0000000000..82d3b766a9 --- /dev/null +++ b/src/server/mssp.py @@ -0,0 +1,187 @@ +""" + +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. + +Most of these settings are de + +""" + +from src.utils import utils + +MSSP = chr(70) +MSSP_VAR = chr(1) +MSSP_VAL = chr(2) + + +# try to get the customized mssp info, if it exists. +MSSPTable_CUSTOM = utils.variable_from_module("game.gamesrc.conf.mssp", "MSSPTable", default={}) + +class Mssp(object): + """ + Implements the MSSP protocol. Add this to a + variable on the telnet protocol to set it up. + """ + def __init__(self, protocol): + """ + initialize MSSP by storing protocol on ourselves + and calling the client to see if it supports + MSSP. + """ + self.protocol = protocol + self.protocol.will(MSSP).addCallbacks(self.do_mssp, self.no_mssp) + + def get_player_count(self): + "Get number of logged-in players" + return str(self.protocol.sessionhandler.count_loggedin()) + + def get_uptime(self): + "Get how long the portal has been online (reloads are not counted)" + return str(self.protocol.sessionhandler.uptime) + + def no_mssp(self, option): + """ + This is the normal operation. + """ + print "no mssp" + pass + + def do_mssp(self, option): + """ + Negotiate all the information. + """ + + self.mssp_table = { + + # Required fields + + "NAME": "Evennia", + "PLAYERS": self.get_player_count, + "UPTIME" : self.get_uptime, + + # Generic + + "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 + "INTERMUD": "IMC2", # evennia supports IMC2. + "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) + + "ANSI": "1", + "GMCP": "0", + "MCCP": "0", + "MCP": "0", + "MSDP": "0", + "MSP": "0", + "MXP": "0", + "PUEBLO": "0", + "UTF-8": "1", + "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" + + # Protocols (only change if you added/removed something manually) + + "ATCP": "0", + "MSDP": "0", + "MCCP": "1", + "SSL": "1", + "UTF-8": "1", + "ZMP": "0", + "XTERM 256 COLORS": "0"} + + # update the static table with the custom one + self.mssp_table.update(MSSPTable_CUSTOM) + + varlist = '' + 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 + str(variable) + MSSP_VAL + str(partval) + else: + varlist += MSSP_VAR + str(variable) + MSSP_VAL + str(value) + + # send to crawler by subnegotiation + self.protocol.requestNegotiation(MSSP, varlist) + + + diff --git a/src/server/session.py b/src/server/session.py index 8db2a7789b..77dd69c3e9 100644 --- a/src/server/session.py +++ b/src/server/session.py @@ -35,7 +35,8 @@ class Session(object): # names of attributes that should be affected by syncing. _attrs_to_sync = ['protocol_key', 'address', 'suid', 'sessid', 'uid', 'uname', 'logged_in', 'cid', 'encoding', - 'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total', 'protocol_flags'] + 'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total', + 'protocol_flags', 'server_data'] def init_session(self, protocol_key, address, sessionhandler): """ @@ -72,6 +73,7 @@ class Session(object): self.cmd_total = 0 self.protocol_flags = {} + self.server_data = {} # a back-reference to the relevant sessionhandler this # session is stored in. diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index 9eb16bb014..5dd952e210 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -89,6 +89,7 @@ class ServerSessionHandler(SessionHandler): """ self.sessions = {} self.server = None + self.server_data = {"servername":settings.SERVERNAME} def portal_connect(self, sessid, session): """ @@ -333,6 +334,16 @@ class PortalSessionHandler(SessionHandler): self.portal = None self.sessions = {} self.latest_sessid = 0 + self.uptime = time.time() + self.connection_time = 0 + + 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 connect(self, session): """ @@ -373,6 +384,14 @@ class PortalSessionHandler(SessionHandler): session.disconnect(reason) del session + + def count_loggedin(self, include_unloggedin=False): + """ + Count loggedin connections, alternatively count all connections. + """ + return len(self.get_sessions(include_unloggedin=include_unloggedin)) + + def session_from_suid(self, suid): """ Given a session id, retrieve the session (this is primarily diff --git a/src/server/telnet.py b/src/server/telnet.py index e55bf0a47c..c83953e3a1 100644 --- a/src/server/telnet.py +++ b/src/server/telnet.py @@ -9,7 +9,8 @@ sessions etc. from twisted.conch.telnet import Telnet, StatefulTelnetProtocol, IAC, LINEMODE, DO, DONT from src.server.session import Session -from src.server import ttype, mccp +from src.server import ttype, mssp +from src.server.mccp import Mccp, mccp_compress, MCCP from src.utils import utils, ansi class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): @@ -27,11 +28,14 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): client_address = self.transport.client self.init_session("telnet", client_address, self.factory.sessionhandler) - # setup ttype (client info) - #self.ttype = ttype.Ttype(self) + # negotiate mccp (data compression) + self.mccp = Mccp(self) + + # negotiate ttype (client info) + self.ttype = ttype.Ttype(self) - # setup mccp (data compression) - # self.mccp = mccp.Mccp(self) #TODO: mccp doesn't work quite right yet. + # negotiate mssp (crawler communication) + self.mssp = mssp.Mssp(self) # add us to sessionhandler self.sessionhandler.connect(self) @@ -42,18 +46,17 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): """ return (option == LINEMODE or option == ttype.TTYPE or - option == mccp.MCCP) + option == MCCP or + option == mssp.MSSP) def enableLocal(self, option): """ Allow certain options on this protocol """ - if option == mccp.MCCP: - #self.mccp.do_mccp(option) - return True + return option == MCCP def disableLocal(self, option): - if option == mccp.MCCP: + if option == MCCP: self.mccp.no_mccp(option) return True else: @@ -84,19 +87,26 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): # print str(e) + ":", str(data) if data and data[0] == IAC: - super(TelnetProtocol, self).dataReceived(data) - else: - StatefulTelnetProtocol.dataReceived(self, data) + try: + super(TelnetProtocol, self).dataReceived(data) + return + except Exception: + pass + StatefulTelnetProtocol.dataReceived(self, data) - def _write(self, byt): + def _write(self, data): "hook overloading the one used in plain telnet" - #print "_write (%s): %s" % (self.state, " ".join(str(ord(c)) for c in byt)) - super(TelnetProtocol, self)._write(mccp.mccp_compress(self, byt)) + #print "_write (%s): %s" % (self.state, " ".join(str(ord(c)) for c in data)) + data = data.replace('\n', '\r\n') + super(TelnetProtocol, self)._write(mccp_compress(self, data)) def sendLine(self, line): - "hook overloading the one used linereceiver" + "hook overloading the one used by linereceiver" #print "sendLine (%s):\n%s" % (self.state, line) - super(TelnetProtocol, self).sendLine(mccp.mccp_compress(self, line)) + #escape IAC in line mode, and correctly add \r\n + line += self.delimiter + line = line.replace(IAC, IAC + IAC).replace('\n', '\r\n') + return self.transport.write(mccp_compress(self, line)) def lineReceived(self, string): """ @@ -105,6 +115,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): """ self.sessionhandler.data_in(self, string) + # Session hooks def disconnect(self, reason=None): diff --git a/src/settings_default.py b/src/settings_default.py index 2afef767c9..33a754c738 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -157,12 +157,14 @@ SEARCH_AT_MULTIMATCH_INPUT = "src.commands.cmdparser.at_multimatch_input" # The module holding text strings for the connection screen. # This module should contain one or more variables # with strings defining the look of the screen. -CONNECTION_SCREEN_MODULE = "game.gamesrc.world.connection_screens" +CONNECTION_SCREEN_MODULE = "game.gamesrc.conf.connection_screens" # An option al module that, if existing, must hold a function # named at_initial_setup(). This hook method can be used to customize # the server's initial setup sequence (the very first startup of the system). # The check will fail quietly if module doesn't exist or fails to load. -AT_INITIAL_SETUP_HOOK_MODULE = "game.gamesrc.world.at_initial_setup" +AT_INITIAL_SETUP_HOOK_MODULE = "game.gamesrc.conf.at_initial_setup" +# Module holding server-side functions for out-of-band protocols to call. +OOB_FUNC_MODULE = "game.gamesrc.conf.oobfuncs" ################################################### # Default command sets @@ -211,7 +213,7 @@ BASE_SCRIPT_TYPECLASS = "src.scripts.scripts.DoNothing" CHARACTER_DEFAULT_HOME = "2" ################################################### -# Batch processors +# Batch processors ################################################### # Python path to a directory to be searched for batch scripts @@ -251,9 +253,7 @@ PERMISSION_HIERARCHY = ("Players","PlayerHelpers","Builders", "Wizards", "Immort PERMISSION_PLAYER_DEFAULT = "Players" # Tuple of modules implementing lock functions. All callable functions # inside these modules will be available as lock functions. -LOCK_FUNC_MODULES = ("src.locks.lockfuncs",) -# Module holding server-side functions for out-of-band protocols to call. -OOB_FUNC_MODULE = "" +LOCK_FUNC_MODULES = ("src.locks.lockfuncs","game.gamesrc.conf.lockfuncs") ################################################### diff --git a/src/utils/utils.py b/src/utils/utils.py index 686963a968..e1f65da48c 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -604,25 +604,45 @@ def mod_import(mod_path, propname=None): return mod_prop return mod -def string_from_module(modpath, variable=None): +def variable_from_module(modpath, variable, default=None): """ - This obtains a string from a given module python path. - The variable must be global within that module - that is, defined in - the outermost scope of the module. The value of the - variable will be returned. If not found (or if it's not a string), - None is returned. + Retrieve a given variable from a module. The variable must be + defined globally in the module. This can be used to implement + arbitrary plugin imports in the server. - This is useful primarily for storing various game strings - in a module and extract them by name or randomly. + If module cannot be imported or variable not found, default + is returned. + """ + try: + mod = __import__(modpath, fromlist=["None"]) + return mod.__dict__.get(variable, default) + except ImportError: + return default + +def string_from_module(modpath, variable=None, default=None): + """ + This is a variation used primarily to get login screens randomly + from a module. + + This obtains a string from a given module python path. Using a + specific variable name will also retrieve non-strings. + + The variable must be global within that module - that is, defined + in the outermost scope of the module. The value of the variable + will be returned. If not found, default is returned. If no variable is + given, a random string variable is returned. + + This is useful primarily for storing various game strings in a + module and extract them by name or randomly. """ mod = __import__(modpath, fromlist=[None]) if variable: - return mod.__dict__.get(variable, None) + return mod.__dict__.get(variable, default) else: mvars = [val for key, val in mod.__dict__.items() if not key.startswith('_') and isinstance(val, basestring)] if not mvars: - return None + return default return mvars[random.randint(0, len(mvars)-1)] def init_new_player(player):