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)
try:
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
pass
return version

View file

@ -3091,7 +3091,8 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
return
# we have a prototype, check access
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.")
return

View file

@ -744,7 +744,7 @@ hole
the remains of the castle. There is also a standing archway
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
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
broken by falling stone and rubble. At one end (the |wnorth|nern) of
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
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
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.
#
@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 random import randint as base_randint, random as base_random, choice as base_choice
import re
from evennia.utils import search
from evennia.utils.utils import justify as base_justify, is_iter, to_str
_PROTLIB = None
_RE_DBREF = re.compile(r"\#[0-9]+")
# default protfuncs
@ -325,3 +328,14 @@ def objlist(*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 time
from ast import literal_eval
@ -33,8 +32,6 @@ _PROTOTYPE_TAG_CATEGORY = "from_prototype"
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
PROT_FUNCS = {}
_RE_DBREF = re.compile(r"(?<!\$obj\()(#[0-9]+)")
class PermissionError(RuntimeError):
pass
@ -258,7 +255,7 @@ def delete_prototype(prototype_key, caller=None):
stored_prototype = stored_prototype[0]
if caller:
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))
stored_prototype.delete()
return True
@ -374,14 +371,14 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed
display_tuples = []
for prototype in sorted(prototypes, key=lambda d: d.get('prototype_key', '')):
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:
continue
if prototype.get('prototype_key', '') in _MODULE_PROTOTYPES:
lock_edit = False
else:
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:
continue
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
# insert $obj(#dbref) for #dbref
value = _RE_DBREF.sub("$obj(\\1)", value)
result = inlinefuncs.parse_inlinefunc(
value, available_funcs=available_functions,
stacktrace=stacktrace, testing=testing, **kwargs)
@ -713,7 +707,8 @@ def check_permission(prototype_key, action, default=True):
lockstring = prototype.get("prototype_locks")
if lockstring:
return check_lockstring(None, lockstring, default=default, access_type=action)
return check_lockstring(None, lockstring,
default=default, access_type=action)
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.prototypes import spawner, prototypes as protlib
from evennia.prototypes import menus as olc_menus
from evennia.prototypes import protfuncs as protofuncs
from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY
@ -312,11 +313,83 @@ class TestProtFuncs(EvenniaTest):
self.assertEqual(protlib.protfunc_parser(
"$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')
self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), '#6')
self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), '#6')
self.assertEqual(protlib.protfunc_parser("$objlist(#1)", session=self.session), ['#1'])
# no object search
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), '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(
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_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.
"""
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: 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
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.
"""
@ -1201,7 +1217,7 @@ def evennia_version():
"git rev-parse --short HEAD",
shell=True, cwd=EVENNIA_ROOT, stderr=STDOUT).strip()
version = "%s (rev %s)" % (version, rev)
except (IOError, CalledProcessError):
except (IOError, CalledProcessError, WindowsError):
# move on if git is not answering
pass
return version
@ -1331,7 +1347,10 @@ def create_settings_file(init=True, secret_settings=False):
else:
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)
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
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:
path (str): Path to new game directory, including its name.
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_gamedir(path)
if need_gamedir:
set_gamedir(path)
# Add gamedir to python path
sys.path.insert(0, GAMEDIR)
if TEST_MODE:
if TEST_MODE or not need_gamedir:
if ENFORCED_SETTING:
print(NOTE_TEST_CUSTOM.format(settings_dotpath=SETTINGS_DOTPATH))
os.environ['DJANGO_SETTINGS_MODULE'] = SETTINGS_DOTPATH
@ -1669,6 +1690,10 @@ def init_game_directory(path, check_db=True):
if check_db:
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
global AMP_PORT, AMP_HOST, AMP_INTERFACE
global SERVER_PY_FILE, PORTAL_PY_FILE
@ -1914,6 +1939,10 @@ def main():
'--initsettings', action='store_true', dest="initsettings",
default=False,
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(
'--profiler', action='store_true', dest='profiler', default=False,
help="start given server component under the Python profiler")
@ -1987,6 +2016,21 @@ def main():
print(ERROR_INITSETTINGS)
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:
# set up for tailing the log files
global NO_REACTOR_STOP
@ -2053,6 +2097,10 @@ def main():
elif option != "noop":
# pass-through to django manager
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
if option in ('runserver', 'testserver'):
@ -2065,7 +2113,7 @@ def main():
global TEST_MODE
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
args = [option]
@ -2081,6 +2129,11 @@ def main():
kwargs[arg.lstrip("--")] = value
else:
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:
django.core.management.call_command(*args, **kwargs)
except django.core.management.base.CommandError as exc:

View file

@ -421,7 +421,7 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
def clientConnectionLost(self, connector, reason):
"""
Called when Client looses connection.
Called when Client loses connection.
Args:
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__))
# Path to the game directory (containing the server/conf/settings.py file)
# 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
GAME_DIR = os.getcwd()
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')
# 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
# loose log info.
# lose log info.
CYCLE_LOGFILES = True
# Number of lines to append to rotating channel logs when they rotate
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
# can be used.
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

View file

@ -397,6 +397,11 @@ class SharedMemoryModel(with_metaclass(SharedMemoryModelBase, Model)):
super(SharedMemoryModel, cls).save(*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
new = False
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
# if hasattr(self, fieldtracker):
# _GA(self, fieldtracker)(fieldname)
pass
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>
<!-- 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>
<!-- Load gui library -->