Merge branch 'master' of https://github.com/evennia/evennia into puzzles

This commit is contained in:
Henddher Pedroza 2018-12-02 10:34:59 -06:00
commit 271d5aa0a5
16 changed files with 281 additions and 32 deletions

7
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,7 @@
# Contributing to Evennia
Evennia utilizes GitHub for issue tracking and contributions:
- Reporting Issues issues/bugs and making feature requests can be done [in the issue tracker](https://github.com/evennia/evennia/issues).
- Evennia's documentation is a [wiki](https://github.com/evennia/evennia/wiki) that everyone can contribute to. Further
instructions and details about contributing is found [here](https://github.com/evennia/evennia/wiki/Contributing).

View file

@ -105,7 +105,7 @@ def _create_version():
print(err) print(err)
try: try:
version = "%s (rev %s)" % (version, check_output("git rev-parse --short HEAD", shell=True, cwd=root, stderr=STDOUT).strip()) version = "%s (rev %s)" % (version, check_output("git rev-parse --short HEAD", shell=True, cwd=root, stderr=STDOUT).strip())
except (IOError, CalledProcessError): except (IOError, CalledProcessError, WindowsError):
# ignore if we cannot get to git # ignore if we cannot get to git
pass pass
return version return version

View file

@ -3091,7 +3091,8 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
return return
# we have a prototype, check access # we have a prototype, check access
prototype = prototypes[0] prototype = prototypes[0]
if not caller.locks.check_lockstring(caller, prototype.get('prototype_locks', ''), access_type='spawn'): if not caller.locks.check_lockstring(
caller, prototype.get('prototype_locks', ''), access_type='spawn', default=True):
caller.msg("You don't have access to use this prototype.") caller.msg("You don't have access to use this prototype.")
return return

View file

@ -744,7 +744,7 @@ hole
the remains of the castle. There is also a standing archway the remains of the castle. There is also a standing archway
offering passage to a path along the old |wsouth|nern inner wall. offering passage to a path along the old |wsouth|nern inner wall.
# #
@detail portoculis;fall;fallen;grating = @detail portcullis;fall;fallen;grating =
This heavy iron grating used to block off the inner part of the gate house, now it has fallen This heavy iron grating used to block off the inner part of the gate house, now it has fallen
to the ground together with the stone archway that once help it up. to the ground together with the stone archway that once help it up.
# #
@ -786,7 +786,7 @@ archway
The buildings make a half-circle along the main wall, here and there The buildings make a half-circle along the main wall, here and there
broken by falling stone and rubble. At one end (the |wnorth|nern) of broken by falling stone and rubble. At one end (the |wnorth|nern) of
this half-circle is the entrance to the castle, the ruined this half-circle is the entrance to the castle, the ruined
gatehoue. |wEast|nwards from here is some sort of open courtyard. gatehouse. |wEast|nwards from here is some sort of open courtyard.
#------------------------------------------------------------ #------------------------------------------------------------
# #
@ -808,7 +808,7 @@ archway
Previously one could probably continue past the obelisk and eastward Previously one could probably continue past the obelisk and eastward
into the castle keep itself, but that way is now completely blocked into the castle keep itself, but that way is now completely blocked
by fallen rubble. To the |wwest|n is the gatehouse and entrance to by fallen rubble. To the |wwest|n is the gatehouse and entrance to
the castle, whereas |wsouth|nwards the collumns make way for a wide the castle, whereas |wsouth|nwards the columns make way for a wide
open courtyard. open courtyard.
# #
@set here/tutorial_info = @set here/tutorial_info =

View file

@ -0,0 +1,15 @@
This directory contains Evennia's log files. The existence of this README.md file is also necessary
to correctly include the log directory in git (since log files are ignored by git and you can't
commit an empty directory).
- `server.log` - log file from the game Server.
- `portal.log` - log file from Portal proxy (internet facing)
Usually these logs are viewed together with `evennia -l`. They are also rotated every week so as not
to be too big. Older log names will have a name appended by `_month_date`.
- `lockwarnings.log` - warnings from the lock system.
- `http_requests.log` - this will generally be empty unless turning on debugging inside the server.
- `channel_<channelname>.log` - these are channel logs for the in-game channels They are also used
by the `/history` flag in-game to get the latest message history.

View file

@ -37,12 +37,15 @@ prototype key (this value must be possible to serialize in an Attribute).
from ast import literal_eval from ast import literal_eval
from random import randint as base_randint, random as base_random, choice as base_choice from random import randint as base_randint, random as base_random, choice as base_choice
import re
from evennia.utils import search from evennia.utils import search
from evennia.utils.utils import justify as base_justify, is_iter, to_str from evennia.utils.utils import justify as base_justify, is_iter, to_str
_PROTLIB = None _PROTLIB = None
_RE_DBREF = re.compile(r"\#[0-9]+")
# default protfuncs # default protfuncs
@ -325,3 +328,14 @@ def objlist(*args, **kwargs):
""" """
return ["#{}".format(obj.id) for obj in _obj_search(return_list=True, *args, **kwargs)] return ["#{}".format(obj.id) for obj in _obj_search(return_list=True, *args, **kwargs)]
def dbref(*args, **kwargs):
"""
Usage $dbref(<#dbref>)
Returns one Object searched globally by #dbref. Error if #dbref is invalid.
"""
if not args or len(args) < 1 or _RE_DBREF.match(args[0]) is None:
raise ValueError('$dbref requires a valid #dbref argument.')
return obj(args[0])

View file

@ -5,7 +5,6 @@ Handling storage of prototypes, both database-based ones (DBPrototypes) and thos
""" """
import re
import hashlib import hashlib
import time import time
from ast import literal_eval from ast import literal_eval
@ -33,8 +32,6 @@ _PROTOTYPE_TAG_CATEGORY = "from_prototype"
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype" _PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
PROT_FUNCS = {} PROT_FUNCS = {}
_RE_DBREF = re.compile(r"(?<!\$obj\()(#[0-9]+)")
class PermissionError(RuntimeError): class PermissionError(RuntimeError):
pass pass
@ -258,7 +255,7 @@ def delete_prototype(prototype_key, caller=None):
stored_prototype = stored_prototype[0] stored_prototype = stored_prototype[0]
if caller: if caller:
if not stored_prototype.access(caller, 'edit'): if not stored_prototype.access(caller, 'edit'):
raise PermissionError("{} does not have permission to " raise PermissionError("{} needs explicit 'edit' permissions to "
"delete prototype {}.".format(caller, prototype_key)) "delete prototype {}.".format(caller, prototype_key))
stored_prototype.delete() stored_prototype.delete()
return True return True
@ -374,14 +371,14 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed
display_tuples = [] display_tuples = []
for prototype in sorted(prototypes, key=lambda d: d.get('prototype_key', '')): for prototype in sorted(prototypes, key=lambda d: d.get('prototype_key', '')):
lock_use = caller.locks.check_lockstring( lock_use = caller.locks.check_lockstring(
caller, prototype.get('prototype_locks', ''), access_type='spawn') caller, prototype.get('prototype_locks', ''), access_type='spawn', default=True)
if not show_non_use and not lock_use: if not show_non_use and not lock_use:
continue continue
if prototype.get('prototype_key', '') in _MODULE_PROTOTYPES: if prototype.get('prototype_key', '') in _MODULE_PROTOTYPES:
lock_edit = False lock_edit = False
else: else:
lock_edit = caller.locks.check_lockstring( lock_edit = caller.locks.check_lockstring(
caller, prototype.get('prototype_locks', ''), access_type='edit') caller, prototype.get('prototype_locks', ''), access_type='edit', default=True)
if not show_non_edit and not lock_edit: if not show_non_edit and not lock_edit:
continue continue
ptags = [] ptags = []
@ -576,9 +573,6 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F
available_functions = PROT_FUNCS if available_functions is None else available_functions available_functions = PROT_FUNCS if available_functions is None else available_functions
# insert $obj(#dbref) for #dbref
value = _RE_DBREF.sub("$obj(\\1)", value)
result = inlinefuncs.parse_inlinefunc( result = inlinefuncs.parse_inlinefunc(
value, available_funcs=available_functions, value, available_funcs=available_functions,
stacktrace=stacktrace, testing=testing, **kwargs) stacktrace=stacktrace, testing=testing, **kwargs)
@ -713,7 +707,8 @@ def check_permission(prototype_key, action, default=True):
lockstring = prototype.get("prototype_locks") lockstring = prototype.get("prototype_locks")
if lockstring: if lockstring:
return check_lockstring(None, lockstring, default=default, access_type=action) return check_lockstring(None, lockstring,
default=default, access_type=action)
return default return default

View file

@ -11,6 +11,7 @@ from evennia.utils.test_resources import EvenniaTest
from evennia.utils.tests.test_evmenu import TestEvMenu from evennia.utils.tests.test_evmenu import TestEvMenu
from evennia.prototypes import spawner, prototypes as protlib from evennia.prototypes import spawner, prototypes as protlib
from evennia.prototypes import menus as olc_menus from evennia.prototypes import menus as olc_menus
from evennia.prototypes import protfuncs as protofuncs
from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY
@ -312,11 +313,83 @@ class TestProtFuncs(EvenniaTest):
self.assertEqual(protlib.protfunc_parser( self.assertEqual(protlib.protfunc_parser(
"$eval({'test': '1', 2:3, 3: $toint(3.5)})"), {'test': '1', 2: 3, 3: 3}) "$eval({'test': '1', 2:3, 3: $toint(3.5)})"), {'test': '1', 2: 3, 3: 3})
self.assertEqual(protlib.protfunc_parser("$obj(#1)", session=self.session), '#1')
self.assertEqual(protlib.protfunc_parser("#1", session=self.session), '#1') # no object search
self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), '#6')
self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), '#6') with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("$objlist(#1)", session=self.session), ['#1']) self.assertEqual(protlib.protfunc_parser("obj(#1)", session=self.session), 'obj(#1)')
mocked__obj_search.assert_not_called()
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("dbref(#1)", session=self.session), 'dbref(#1)')
mocked__obj_search.assert_not_called()
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("stone(#12345)", session=self.session), 'stone(#12345)')
mocked__obj_search.assert_not_called()
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("#1", session=self.session), '#1')
mocked__obj_search.assert_not_called()
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("#12345", session=self.session), '#12345')
mocked__obj_search.assert_not_called()
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("nothing(#1)", session=self.session), 'nothing(#1)')
mocked__obj_search.assert_not_called()
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("(#12345)", session=self.session), '(#12345)')
mocked__obj_search.assert_not_called()
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("obj(Char)", session=self.session), 'obj(Char)')
mocked__obj_search.assert_not_called()
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("objlist(#1)", session=self.session), 'objlist(#1)')
mocked__obj_search.assert_not_called()
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("dbref(Char)", session=self.session), 'dbref(Char)')
mocked__obj_search.assert_not_called()
# obj search happens
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("$objlist(#1)", session=self.session), ['#1'])
mocked__obj_search.assert_called_once()
assert ('#1',) == mocked__obj_search.call_args[0]
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("$obj(#1)", session=self.session), '#1')
mocked__obj_search.assert_called_once()
assert ('#1',) == mocked__obj_search.call_args[0]
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("$dbref(#1)", session=self.session), '#1')
mocked__obj_search.assert_called_once()
assert ('#1',) == mocked__obj_search.call_args[0]
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), '#6')
mocked__obj_search.assert_called_once()
assert ('Char',) == mocked__obj_search.call_args[0]
# bad invocation
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertEqual(protlib.protfunc_parser("$badfunc(#1)", session=self.session), '<UNKNOWN>')
mocked__obj_search.assert_not_called()
with mock.patch("evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search) as mocked__obj_search:
self.assertRaises(ValueError, protlib.protfunc_parser, "$dbref(Char)")
mocked__obj_search.assert_not_called()
self.assertEqual(protlib.value_to_obj( self.assertEqual(protlib.value_to_obj(
protlib.protfunc_parser("#6", session=self.session)), self.char1) protlib.protfunc_parser("#6", session=self.session)), self.char1)

View file

@ -98,7 +98,10 @@ TWISTED_MIN = '18.0.0'
DJANGO_MIN = '1.11' DJANGO_MIN = '1.11'
DJANGO_REC = '1.11' DJANGO_REC = '1.11'
sys.path[1] = EVENNIA_ROOT try:
sys.path[1] = EVENNIA_ROOT
except IndexError:
sys.path.append(EVENNIA_ROOT)
# ------------------------------------------------------------ # ------------------------------------------------------------
# #
@ -222,6 +225,19 @@ RECREATED_SETTINGS = \
their accounts with their old passwords. their accounts with their old passwords.
""" """
ERROR_INITMISSING = \
"""
ERROR: 'evennia --initmissing' must be called from the root of
your game directory, since it tries to create any missing files
in the server/ subfolder.
"""
RECREATED_MISSING = \
"""
(Re)created any missing directories or files. Evennia should
be ready to run now!
"""
ERROR_DATABASE = \ ERROR_DATABASE = \
""" """
ERROR: Your database does not seem to be set up correctly. ERROR: Your database does not seem to be set up correctly.
@ -261,7 +277,7 @@ INFO_WINDOWS_BATFILE = \
twistd.bat to point to the actual location of the Twisted twistd.bat to point to the actual location of the Twisted
executable (usually called twistd.py) on your machine. executable (usually called twistd.py) on your machine.
This procedure is only done once. Run evennia.py again when you This procedure is only done once. Run `evennia` again when you
are ready to start the server. are ready to start the server.
""" """
@ -1201,7 +1217,7 @@ def evennia_version():
"git rev-parse --short HEAD", "git rev-parse --short HEAD",
shell=True, cwd=EVENNIA_ROOT, stderr=STDOUT).strip() shell=True, cwd=EVENNIA_ROOT, stderr=STDOUT).strip()
version = "%s (rev %s)" % (version, rev) version = "%s (rev %s)" % (version, rev)
except (IOError, CalledProcessError): except (IOError, CalledProcessError, WindowsError):
# move on if git is not answering # move on if git is not answering
pass pass
return version return version
@ -1331,7 +1347,10 @@ def create_settings_file(init=True, secret_settings=False):
else: else:
print("Reset the settings file.") print("Reset the settings file.")
default_settings_path = os.path.join(EVENNIA_TEMPLATE, "server", "conf", "settings.py") if secret_settings:
default_settings_path = os.path.join(EVENNIA_TEMPLATE, "server", "conf", "secret_settings.py")
else:
default_settings_path = os.path.join(EVENNIA_TEMPLATE, "server", "conf", "settings.py")
shutil.copy(default_settings_path, settings_path) shutil.copy(default_settings_path, settings_path)
with open(settings_path, 'r') as f: with open(settings_path, 'r') as f:
@ -1625,7 +1644,7 @@ def error_check_python_modules():
# #
# ------------------------------------------------------------ # ------------------------------------------------------------
def init_game_directory(path, check_db=True): def init_game_directory(path, check_db=True, need_gamedir=True):
""" """
Try to analyze the given path to find settings.py - this defines Try to analyze the given path to find settings.py - this defines
the game directory and also sets PYTHONPATH as well as the django the game directory and also sets PYTHONPATH as well as the django
@ -1634,15 +1653,17 @@ def init_game_directory(path, check_db=True):
Args: Args:
path (str): Path to new game directory, including its name. path (str): Path to new game directory, including its name.
check_db (bool, optional): Check if the databae exists. check_db (bool, optional): Check if the databae exists.
need_gamedir (bool, optional): set to False if Evennia doesn't require to be run in a valid game directory.
""" """
# set the GAMEDIR path # set the GAMEDIR path
set_gamedir(path) if need_gamedir:
set_gamedir(path)
# Add gamedir to python path # Add gamedir to python path
sys.path.insert(0, GAMEDIR) sys.path.insert(0, GAMEDIR)
if TEST_MODE: if TEST_MODE or not need_gamedir:
if ENFORCED_SETTING: if ENFORCED_SETTING:
print(NOTE_TEST_CUSTOM.format(settings_dotpath=SETTINGS_DOTPATH)) print(NOTE_TEST_CUSTOM.format(settings_dotpath=SETTINGS_DOTPATH))
os.environ['DJANGO_SETTINGS_MODULE'] = SETTINGS_DOTPATH os.environ['DJANGO_SETTINGS_MODULE'] = SETTINGS_DOTPATH
@ -1669,6 +1690,10 @@ def init_game_directory(path, check_db=True):
if check_db: if check_db:
check_database() check_database()
# if we don't have to check the game directory, return right away
if not need_gamedir:
return
# set up the Evennia executables and log file locations # set up the Evennia executables and log file locations
global AMP_PORT, AMP_HOST, AMP_INTERFACE global AMP_PORT, AMP_HOST, AMP_INTERFACE
global SERVER_PY_FILE, PORTAL_PY_FILE global SERVER_PY_FILE, PORTAL_PY_FILE
@ -1914,6 +1939,10 @@ def main():
'--initsettings', action='store_true', dest="initsettings", '--initsettings', action='store_true', dest="initsettings",
default=False, default=False,
help="create a new, empty settings file as\n gamedir/server/conf/settings.py") help="create a new, empty settings file as\n gamedir/server/conf/settings.py")
parser.add_argument(
'--initmissing', action='store_true', dest="initmissing",
default=False,
help="checks for missing secret_settings or server logs\n directory, and adds them if needed")
parser.add_argument( parser.add_argument(
'--profiler', action='store_true', dest='profiler', default=False, '--profiler', action='store_true', dest='profiler', default=False,
help="start given server component under the Python profiler") help="start given server component under the Python profiler")
@ -1987,6 +2016,21 @@ def main():
print(ERROR_INITSETTINGS) print(ERROR_INITSETTINGS)
sys.exit() sys.exit()
if args.initmissing:
try:
log_path = os.path.join(SERVERDIR, "logs")
if not os.path.exists(log_path):
os.makedirs(log_path)
settings_path = os.path.join(CONFDIR, "secret_settings.py")
if not os.path.exists(settings_path):
create_settings_file(init=False, secret_settings=True)
print(RECREATED_MISSING)
except IOError:
print(ERROR_INITMISSING)
sys.exit()
if args.tail_log: if args.tail_log:
# set up for tailing the log files # set up for tailing the log files
global NO_REACTOR_STOP global NO_REACTOR_STOP
@ -2053,6 +2097,10 @@ def main():
elif option != "noop": elif option != "noop":
# pass-through to django manager # pass-through to django manager
check_db = False check_db = False
need_gamedir = True
# some commands don't require the presence of a game directory to work
if option in ('makemessages', 'compilemessages'):
need_gamedir = False
# handle special django commands # handle special django commands
if option in ('runserver', 'testserver'): if option in ('runserver', 'testserver'):
@ -2065,7 +2113,7 @@ def main():
global TEST_MODE global TEST_MODE
TEST_MODE = True TEST_MODE = True
init_game_directory(CURRENT_DIR, check_db=check_db) init_game_directory(CURRENT_DIR, check_db=check_db, need_gamedir=need_gamedir)
# pass on to the manager # pass on to the manager
args = [option] args = [option]
@ -2081,6 +2129,11 @@ def main():
kwargs[arg.lstrip("--")] = value kwargs[arg.lstrip("--")] = value
else: else:
args.append(arg) args.append(arg)
# makemessages needs a special syntax to not conflict with the -l option
if len(args) > 1 and args[0] == "makemessages":
args.insert(1, "-l")
try: try:
django.core.management.call_command(*args, **kwargs) django.core.management.call_command(*args, **kwargs)
except django.core.management.base.CommandError as exc: except django.core.management.base.CommandError as exc:

View file

@ -421,7 +421,7 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
def clientConnectionLost(self, connector, reason): def clientConnectionLost(self, connector, reason):
""" """
Called when Client looses connection. Called when Client loses connection.
Args: Args:
connector (Connection): Represents the connection. connector (Connection): Represents the connection.

View file

@ -115,7 +115,7 @@ AMP_INTERFACE = '127.0.0.1'
EVENNIA_DIR = os.path.dirname(os.path.abspath(__file__)) EVENNIA_DIR = os.path.dirname(os.path.abspath(__file__))
# Path to the game directory (containing the server/conf/settings.py file) # Path to the game directory (containing the server/conf/settings.py file)
# This is dynamically created- there is generally no need to change this! # This is dynamically created- there is generally no need to change this!
if sys.argv[1] == 'test' if len(sys.argv) > 1 else False: if EVENNIA_DIR.lower() == os.getcwd().lower() or (sys.argv[1] == 'test' if len(sys.argv) > 1 else False):
# unittesting mode # unittesting mode
GAME_DIR = os.getcwd() GAME_DIR = os.getcwd()
else: else:
@ -138,7 +138,7 @@ HTTP_LOG_FILE = os.path.join(LOG_DIR, 'http_requests.log')
LOCKWARNING_LOG_FILE = os.path.join(LOG_DIR, 'lockwarnings.log') LOCKWARNING_LOG_FILE = os.path.join(LOG_DIR, 'lockwarnings.log')
# Rotate log files when server and/or portal stops. This will keep log # Rotate log files when server and/or portal stops. This will keep log
# file sizes down. Turn off to get ever growing log files and never # file sizes down. Turn off to get ever growing log files and never
# loose log info. # lose log info.
CYCLE_LOGFILES = True CYCLE_LOGFILES = True
# Number of lines to append to rotating channel logs when they rotate # Number of lines to append to rotating channel logs when they rotate
CHANNEL_LOG_NUM_TAIL_LINES = 20 CHANNEL_LOG_NUM_TAIL_LINES = 20

View file

@ -229,6 +229,12 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
# at_first_save hook on the typeclass, where the _createdict # at_first_save hook on the typeclass, where the _createdict
# can be used. # can be used.
new_script.save() new_script.save()
if not new_script.id:
# this happens in the case of having a repeating script with `repeats=1` and
# `start_delay=False` - the script will run once and immediately stop before save is over.
return None
return new_script return new_script

View file

@ -397,6 +397,11 @@ class SharedMemoryModel(with_metaclass(SharedMemoryModelBase, Model)):
super(SharedMemoryModel, cls).save(*args, **kwargs) super(SharedMemoryModel, cls).save(*args, **kwargs)
callFromThread(_save_callback, self, *args, **kwargs) callFromThread(_save_callback, self, *args, **kwargs)
if not self.pk:
# this can happen if some of the startup methods immediately
# delete the object (an example are Scripts that start and die immediately)
return
# update field-update hooks and eventual OOB watchers # update field-update hooks and eventual OOB watchers
new = False new = False
if "update_fields" in kwargs and kwargs["update_fields"]: if "update_fields" in kwargs and kwargs["update_fields"]:
@ -421,6 +426,7 @@ class SharedMemoryModel(with_metaclass(SharedMemoryModelBase, Model)):
# fieldtracker = "_oob_at_%s_postsave" % fieldname # fieldtracker = "_oob_at_%s_postsave" % fieldname
# if hasattr(self, fieldtracker): # if hasattr(self, fieldtracker):
# _GA(self, fieldtracker)(fieldname) # _GA(self, fieldtracker)(fieldname)
pass
class WeakSharedMemoryModelBase(SharedMemoryModelBase): class WeakSharedMemoryModelBase(SharedMemoryModelBase):

View file

@ -0,0 +1,79 @@
"""
Tests of create functions
"""
from evennia.utils.test_resources import EvenniaTest
from evennia.scripts.scripts import DefaultScript
from evennia.utils import create
class TestCreateScript(EvenniaTest):
def test_create_script(self):
class TestScriptA(DefaultScript):
def at_script_creation(self):
self.key = 'test_script'
self.interval = 10
self.persistent = False
script = create.create_script(TestScriptA, key='test_script')
assert script is not None
assert script.interval == 10
assert script.key == 'test_script'
script.stop()
def test_create_script_w_repeats_equal_1(self):
class TestScriptB(DefaultScript):
def at_script_creation(self):
self.key = 'test_script'
self.interval = 10
self.repeats = 1
self.persistent = False
# script is already stopped (interval=1, start_delay=False)
script = create.create_script(TestScriptB, key='test_script')
assert script is None
def test_create_script_w_repeats_equal_1_persisted(self):
class TestScriptB1(DefaultScript):
def at_script_creation(self):
self.key = 'test_script'
self.interval = 10
self.repeats = 1
self.persistent = True
# script is already stopped (interval=1, start_delay=False)
script = create.create_script(TestScriptB1, key='test_script')
assert script is None
def test_create_script_w_repeats_equal_2(self):
class TestScriptC(DefaultScript):
def at_script_creation(self):
self.key = 'test_script'
self.interval = 10
self.repeats = 2
self.persistent = False
script = create.create_script(TestScriptC, key='test_script')
assert script is not None
assert script.interval == 10
assert script.repeats == 2
assert script.key == 'test_script'
script.stop()
def test_create_script_w_repeats_equal_1_and_delayed(self):
class TestScriptD(DefaultScript):
def at_script_creation(self):
self.key = 'test_script'
self.interval = 10
self.start_delay = True
self.repeats = 1
self.persistent = False
script = create.create_script(TestScriptD, key='test_script')
assert script is not None
assert script.interval == 10
assert script.repeats == 1
assert script.key == 'test_script'
script.stop()

View file

@ -64,7 +64,7 @@ JQuery available.
<script src={% static "webclient/js/evennia.js" %} language="javascript" type="text/javascript" charset="utf-8"/></script> <script src={% static "webclient/js/evennia.js" %} language="javascript" type="text/javascript" charset="utf-8"/></script>
<!-- set up splits before loading the GUI --> <!-- set up splits before loading the GUI -->
<script src="https://unpkg.com/split.js/split.min.js"></script> <script src="https://unpkg.com/split.js@1.5.9/dist/split.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script>
<!-- Load gui library --> <!-- Load gui library -->