Resolve merge conflicts
This commit is contained in:
commit
9cdc37355c
25 changed files with 726 additions and 444 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -56,9 +56,7 @@ without arguments starts a full interactive Python console.
|
||||||
of texts (such as tables). New `justify` bool. Old `justify_kwargs` remains
|
of texts (such as tables). New `justify` bool. Old `justify_kwargs` remains
|
||||||
but is now only used to pass extra kwargs into the justify function.
|
but is now only used to pass extra kwargs into the justify function.
|
||||||
- EvMore `text` argument can now also be a list or a queryset. Querysets will be
|
- EvMore `text` argument can now also be a list or a queryset. Querysets will be
|
||||||
sliced to only return the required data per page. EvMore takes a new kwarg
|
sliced to only return the required data per page.
|
||||||
`page_formatter` which will be called for each page. This allows to customize
|
|
||||||
the display of queryset data, build a new EvTable per page etc.
|
|
||||||
- Improve performance of `find` and `objects` commands on large data sets (strikaco)
|
- Improve performance of `find` and `objects` commands on large data sets (strikaco)
|
||||||
- New `CHANNEL_HANDLER_CLASS` setting allows for replacing the ChannelHandler entirely.
|
- New `CHANNEL_HANDLER_CLASS` setting allows for replacing the ChannelHandler entirely.
|
||||||
- Made `py` interactive mode support regular quit() and more verbose.
|
- Made `py` interactive mode support regular quit() and more verbose.
|
||||||
|
|
@ -88,6 +86,12 @@ without arguments starts a full interactive Python console.
|
||||||
- Make `INLINEFUNC_STACK_MAXSIZE` default visible in `settings_default.py`.
|
- Make `INLINEFUNC_STACK_MAXSIZE` default visible in `settings_default.py`.
|
||||||
- Change how `ic` finds puppets; non-priveleged users will use `_playable_characters` list as
|
- Change how `ic` finds puppets; non-priveleged users will use `_playable_characters` list as
|
||||||
candidates, Builders+ will use list, local search and only global search if no match found.
|
candidates, Builders+ will use list, local search and only global search if no match found.
|
||||||
|
- Make `cmd.at_post_cmd()` always run after `cmd.func()`, even when the latter uses delays
|
||||||
|
with yield.
|
||||||
|
- `EvMore` support for db queries and django paginators as well as easier to override for custom
|
||||||
|
pagination (e.g. to create EvTables for every page instead of splittine one table)
|
||||||
|
- Using `EvMore pagination`, dramatically improves performance of `spawn/list` and `scripts` listings
|
||||||
|
(100x speed increase for displaying 1000+ prototypes/scripts).
|
||||||
|
|
||||||
|
|
||||||
## Evennia 0.9 (2018-2019)
|
## Evennia 0.9 (2018-2019)
|
||||||
|
|
|
||||||
|
|
@ -1262,7 +1262,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
||||||
]
|
]
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.log_trace()
|
logger.log_trace()
|
||||||
now = timezone.now()
|
now = timezone.localtime()
|
||||||
now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, now.day, now.hour, now.minute)
|
now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, now.day, now.hour, now.minute)
|
||||||
if _MUDINFO_CHANNEL:
|
if _MUDINFO_CHANNEL:
|
||||||
_MUDINFO_CHANNEL.tempmsg(f"[{_MUDINFO_CHANNEL.key}, {now}]: {message}")
|
_MUDINFO_CHANNEL.tempmsg(f"[{_MUDINFO_CHANNEL.key}, {now}]: {message}")
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,27 @@ def _msg_err(receiver, stringtuple):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _process_input(caller, prompt, result, cmd, generator):
|
||||||
|
"""
|
||||||
|
Specifically handle the get_input value to send to _progressive_cmd_run as
|
||||||
|
part of yielding from a Command's `func`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
caller (Character, Account or Session): the caller.
|
||||||
|
prompt (str): The sent prompt.
|
||||||
|
result (str): The unprocessed answer.
|
||||||
|
cmd (Command): The command itself.
|
||||||
|
generator (GeneratorType): The generator.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
result (bool): Always `False` (stop processing).
|
||||||
|
|
||||||
|
"""
|
||||||
|
# We call it using a Twisted deferLater to make sure the input is properly closed.
|
||||||
|
deferLater(reactor, 0, _progressive_cmd_run, cmd, generator, response=result)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _progressive_cmd_run(cmd, generator, response=None):
|
def _progressive_cmd_run(cmd, generator, response=None):
|
||||||
"""
|
"""
|
||||||
Progressively call the command that was given in argument. Used
|
Progressively call the command that was given in argument. Used
|
||||||
|
|
@ -206,7 +227,15 @@ def _progressive_cmd_run(cmd, generator, response=None):
|
||||||
else:
|
else:
|
||||||
value = generator.send(response)
|
value = generator.send(response)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
pass
|
# duplicated from cmdhandler._run_command, to have these
|
||||||
|
# run in the right order while staying inside the deferred
|
||||||
|
cmd.at_post_cmd()
|
||||||
|
if cmd.save_for_next:
|
||||||
|
# store a reference to this command, possibly
|
||||||
|
# accessible by the next command.
|
||||||
|
cmd.caller.ndb.last_cmd = copy(cmd)
|
||||||
|
else:
|
||||||
|
cmd.caller.ndb.last_cmd = None
|
||||||
else:
|
else:
|
||||||
if isinstance(value, (int, float)):
|
if isinstance(value, (int, float)):
|
||||||
utils.delay(value, _progressive_cmd_run, cmd, generator)
|
utils.delay(value, _progressive_cmd_run, cmd, generator)
|
||||||
|
|
@ -216,27 +245,6 @@ def _progressive_cmd_run(cmd, generator, response=None):
|
||||||
raise ValueError("unknown type for a yielded value in command: {}".format(type(value)))
|
raise ValueError("unknown type for a yielded value in command: {}".format(type(value)))
|
||||||
|
|
||||||
|
|
||||||
def _process_input(caller, prompt, result, cmd, generator):
|
|
||||||
"""
|
|
||||||
Specifically handle the get_input value to send to _progressive_cmd_run as
|
|
||||||
part of yielding from a Command's `func`.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
caller (Character, Account or Session): the caller.
|
|
||||||
prompt (str): The sent prompt.
|
|
||||||
result (str): The unprocessed answer.
|
|
||||||
cmd (Command): The command itself.
|
|
||||||
generator (GeneratorType): The generator.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
result (bool): Always `False` (stop processing).
|
|
||||||
|
|
||||||
"""
|
|
||||||
# We call it using a Twisted deferLater to make sure the input is properly closed.
|
|
||||||
deferLater(reactor, 0, _progressive_cmd_run, cmd, generator, response=result)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# custom Exceptions
|
# custom Exceptions
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -632,10 +640,14 @@ def cmdhandler(
|
||||||
if isinstance(ret, types.GeneratorType):
|
if isinstance(ret, types.GeneratorType):
|
||||||
# cmd.func() is a generator, execute progressively
|
# cmd.func() is a generator, execute progressively
|
||||||
_progressive_cmd_run(cmd, ret)
|
_progressive_cmd_run(cmd, ret)
|
||||||
yield None
|
ret = yield ret
|
||||||
|
# note that the _progressive_cmd_run will itself run
|
||||||
|
# the at_post_cmd etc as it finishes; this is a bit of
|
||||||
|
# code duplication but there seems to be no way to
|
||||||
|
# catch the StopIteration here (it's not in the same
|
||||||
|
# frame since this is in a deferred chain)
|
||||||
else:
|
else:
|
||||||
ret = yield ret
|
ret = yield ret
|
||||||
|
|
||||||
# post-command hook
|
# post-command hook
|
||||||
yield cmd.at_post_cmd()
|
yield cmd.at_post_cmd()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3076,9 +3076,9 @@ class CmdScript(COMMAND_DEFAULT_CLASS):
|
||||||
result.append("No scripts defined on %s." % obj.get_display_name(caller))
|
result.append("No scripts defined on %s." % obj.get_display_name(caller))
|
||||||
elif not self.switches:
|
elif not self.switches:
|
||||||
# view all scripts
|
# view all scripts
|
||||||
from evennia.commands.default.system import format_script_list
|
from evennia.commands.default.system import ScriptEvMore
|
||||||
|
ScriptEvMore(self.caller, scripts.order_by("id"), session=self.session)
|
||||||
result.append(format_script_list(scripts))
|
return
|
||||||
elif "start" in self.switches:
|
elif "start" in self.switches:
|
||||||
num = sum([obj.scripts.start(script.key) for script in scripts])
|
num = sum([obj.scripts.start(script.key) for script in scripts])
|
||||||
result.append("%s scripts started on %s." % (num, obj.get_display_name(caller)))
|
result.append("%s scripts started on %s." % (num, obj.get_display_name(caller)))
|
||||||
|
|
@ -3285,6 +3285,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
spawn/search [prototype_keykey][;tag[,tag]]
|
spawn/search [prototype_keykey][;tag[,tag]]
|
||||||
spawn/list [tag, tag, ...]
|
spawn/list [tag, tag, ...]
|
||||||
|
spawn/list modules - list only module-based prototypes
|
||||||
spawn/show [<prototype_key>]
|
spawn/show [<prototype_key>]
|
||||||
spawn/update <prototype_key>
|
spawn/update <prototype_key>
|
||||||
|
|
||||||
|
|
@ -3476,16 +3477,11 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
elif query:
|
elif query:
|
||||||
self.caller.msg(f"No prototype named '{query}' was found.")
|
self.caller.msg(f"No prototype named '{query}' was found.")
|
||||||
else:
|
else:
|
||||||
self.caller.msg(f"No prototypes found.")
|
self.caller.msg("No prototypes found.")
|
||||||
|
|
||||||
def _list_prototypes(self, key=None, tags=None):
|
def _list_prototypes(self, key=None, tags=None):
|
||||||
"""Display prototypes as a list, optionally limited by key/tags. """
|
"""Display prototypes as a list, optionally limited by key/tags. """
|
||||||
table = protlib.list_prototypes(self.caller, key=key, tags=tags)
|
protlib.list_prototypes(self.caller, key=key, tags=tags, session=self.session)
|
||||||
if not table:
|
|
||||||
return True
|
|
||||||
EvMore(
|
|
||||||
self.caller, str(table), exit_on_lastpage=True, justify_kwargs=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
@interactive
|
@interactive
|
||||||
def _update_existing_objects(self, caller, prototype_key, quiet=False):
|
def _update_existing_objects(self, caller, prototype_key, quiet=False):
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,10 @@ __all__ = (
|
||||||
"CmdCdesc",
|
"CmdCdesc",
|
||||||
"CmdPage",
|
"CmdPage",
|
||||||
"CmdIRC2Chan",
|
"CmdIRC2Chan",
|
||||||
|
"CmdIRCStatus",
|
||||||
"CmdRSS2Chan",
|
"CmdRSS2Chan",
|
||||||
|
"CmdGrapevine2Chan",
|
||||||
|
|
||||||
)
|
)
|
||||||
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import twisted
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.paginator import Paginator
|
||||||
from evennia.server.sessionhandler import SESSIONS
|
from evennia.server.sessionhandler import SESSIONS
|
||||||
from evennia.scripts.models import ScriptDB
|
from evennia.scripts.models import ScriptDB
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
|
|
@ -408,12 +409,23 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# helper function. Kept outside so it can be imported and run
|
class ScriptEvMore(EvMore):
|
||||||
# by other commands.
|
"""
|
||||||
|
Listing 1000+ Scripts can be very slow and memory-consuming. So
|
||||||
|
we use this custom EvMore child to build en EvTable only for
|
||||||
|
each page of the list.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def init_pages(self, scripts):
|
||||||
|
"""Prepare the script list pagination"""
|
||||||
|
script_pages = Paginator(scripts, max(1, int(self.height / 2)))
|
||||||
|
super().init_pages(script_pages)
|
||||||
|
|
||||||
|
def page_formatter(self, scripts):
|
||||||
|
"""Takes a page of scripts and formats the output
|
||||||
|
into an EvTable."""
|
||||||
|
|
||||||
def format_script_list(scripts):
|
|
||||||
"""Takes a list of scripts and formats the output."""
|
|
||||||
if not scripts:
|
if not scripts:
|
||||||
return "<No scripts>"
|
return "<No scripts>"
|
||||||
|
|
||||||
|
|
@ -429,6 +441,7 @@ def format_script_list(scripts):
|
||||||
"|wdesc|n",
|
"|wdesc|n",
|
||||||
align="r",
|
align="r",
|
||||||
border="tablecols",
|
border="tablecols",
|
||||||
|
width=self.width
|
||||||
)
|
)
|
||||||
|
|
||||||
for script in scripts:
|
for script in scripts:
|
||||||
|
|
@ -460,7 +473,7 @@ def format_script_list(scripts):
|
||||||
crop(script.desc, width=20),
|
crop(script.desc, width=20),
|
||||||
)
|
)
|
||||||
|
|
||||||
return "%s" % table
|
return str(table)
|
||||||
|
|
||||||
|
|
||||||
class CmdScripts(COMMAND_DEFAULT_CLASS):
|
class CmdScripts(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -549,7 +562,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
||||||
caller.msg(string)
|
caller.msg(string)
|
||||||
else:
|
else:
|
||||||
# multiple matches.
|
# multiple matches.
|
||||||
EvMore(caller, scripts, page_formatter=format_script_list)
|
ScriptEvMore(caller, scripts, session=self.session)
|
||||||
caller.msg("Multiple script matches. Please refine your search")
|
caller.msg("Multiple script matches. Please refine your search")
|
||||||
elif self.switches and self.switches[0] in ("validate", "valid", "val"):
|
elif self.switches and self.switches[0] in ("validate", "valid", "val"):
|
||||||
# run validation on all found scripts
|
# run validation on all found scripts
|
||||||
|
|
@ -559,7 +572,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
||||||
caller.msg(string)
|
caller.msg(string)
|
||||||
else:
|
else:
|
||||||
# No stopping or validation. We just want to view things.
|
# No stopping or validation. We just want to view things.
|
||||||
EvMore(caller, scripts, page_formatter=format_script_list)
|
ScriptEvMore(caller, scripts.order_by('id'), session=self.session)
|
||||||
|
|
||||||
|
|
||||||
class CmdObjects(COMMAND_DEFAULT_CLASS):
|
class CmdObjects(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
|
||||||
|
|
@ -1159,7 +1159,7 @@ class TestBuilding(CommandTest):
|
||||||
"= Obj",
|
"= Obj",
|
||||||
"To create a global script you need scripts/add <typeclass>.",
|
"To create a global script you need scripts/add <typeclass>.",
|
||||||
)
|
)
|
||||||
self.call(building.CmdScript(), "Obj = ", "dbref obj")
|
self.call(building.CmdScript(), "Obj ", "dbref ")
|
||||||
|
|
||||||
self.call(
|
self.call(
|
||||||
building.CmdScript(), "/start Obj", "0 scripts started on Obj"
|
building.CmdScript(), "/start Obj", "0 scripts started on Obj"
|
||||||
|
|
@ -1252,10 +1252,11 @@ class TestBuilding(CommandTest):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_spawn(self):
|
def test_spawn(self):
|
||||||
def getObject(commandTest, objKeyStr):
|
|
||||||
|
def get_object(commandTest, obj_key):
|
||||||
# A helper function to get a spawned object and
|
# A helper function to get a spawned object and
|
||||||
# check that it exists in the process.
|
# check that it exists in the process.
|
||||||
query = search_object(objKeyStr)
|
query = search_object(obj_key)
|
||||||
commandTest.assertIsNotNone(query)
|
commandTest.assertIsNotNone(query)
|
||||||
commandTest.assertTrue(bool(query))
|
commandTest.assertTrue(bool(query))
|
||||||
obj = query[0]
|
obj = query[0]
|
||||||
|
|
@ -1284,7 +1285,7 @@ class TestBuilding(CommandTest):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.call(building.CmdSpawn(), "/search ", "Key ")
|
self.call(building.CmdSpawn(), "/search ", "Key ")
|
||||||
self.call(building.CmdSpawn(), "/search test;test2", "")
|
self.call(building.CmdSpawn(), "/search test;test2", "No prototypes found.")
|
||||||
|
|
||||||
self.call(
|
self.call(
|
||||||
building.CmdSpawn(),
|
building.CmdSpawn(),
|
||||||
|
|
@ -1294,11 +1295,11 @@ class TestBuilding(CommandTest):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||||
|
|
||||||
self.call(building.CmdSpawn(), "testprot", "Spawned Test Char")
|
self.call(building.CmdSpawn(), "testprot", "Spawned Test Char")
|
||||||
# Tests that the spawned object's location is the same as the caharacter's location, since
|
|
||||||
|
# Tests that the spawned object's location is the same as the character's location, since
|
||||||
# we did not specify it.
|
# we did not specify it.
|
||||||
testchar = getObject(self, "Test Char")
|
testchar = get_object(self, "Test Char")
|
||||||
self.assertEqual(testchar.location, self.char1.location)
|
self.assertEqual(testchar.location, self.char1.location)
|
||||||
testchar.delete()
|
testchar.delete()
|
||||||
|
|
||||||
|
|
@ -1315,7 +1316,7 @@ class TestBuilding(CommandTest):
|
||||||
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref,
|
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref,
|
||||||
"Spawned goblin",
|
"Spawned goblin",
|
||||||
)
|
)
|
||||||
goblin = getObject(self, "goblin")
|
goblin = get_object(self, "goblin")
|
||||||
# Tests that the spawned object's type is a DefaultCharacter.
|
# Tests that the spawned object's type is a DefaultCharacter.
|
||||||
self.assertIsInstance(goblin, DefaultCharacter)
|
self.assertIsInstance(goblin, DefaultCharacter)
|
||||||
self.assertEqual(goblin.location, spawnLoc)
|
self.assertEqual(goblin.location, spawnLoc)
|
||||||
|
|
@ -1334,7 +1335,7 @@ class TestBuilding(CommandTest):
|
||||||
# Tests "spawn <prototype_name>"
|
# Tests "spawn <prototype_name>"
|
||||||
self.call(building.CmdSpawn(), "testball", "Spawned Ball")
|
self.call(building.CmdSpawn(), "testball", "Spawned Ball")
|
||||||
|
|
||||||
ball = getObject(self, "Ball")
|
ball = get_object(self, "Ball")
|
||||||
self.assertEqual(ball.location, self.char1.location)
|
self.assertEqual(ball.location, self.char1.location)
|
||||||
self.assertIsInstance(ball, DefaultObject)
|
self.assertIsInstance(ball, DefaultObject)
|
||||||
ball.delete()
|
ball.delete()
|
||||||
|
|
@ -1344,7 +1345,7 @@ class TestBuilding(CommandTest):
|
||||||
self.call(
|
self.call(
|
||||||
building.CmdSpawn(), "/n 'BALL'", "Spawned Ball"
|
building.CmdSpawn(), "/n 'BALL'", "Spawned Ball"
|
||||||
) # /n switch is abbreviated form of /noloc
|
) # /n switch is abbreviated form of /noloc
|
||||||
ball = getObject(self, "Ball")
|
ball = get_object(self, "Ball")
|
||||||
self.assertIsNone(ball.location)
|
self.assertIsNone(ball.location)
|
||||||
ball.delete()
|
ball.delete()
|
||||||
|
|
||||||
|
|
@ -1363,7 +1364,7 @@ class TestBuilding(CommandTest):
|
||||||
% spawnLoc.dbref,
|
% spawnLoc.dbref,
|
||||||
"Spawned Ball",
|
"Spawned Ball",
|
||||||
)
|
)
|
||||||
ball = getObject(self, "Ball")
|
ball = get_object(self, "Ball")
|
||||||
self.assertEqual(ball.location, spawnLoc)
|
self.assertEqual(ball.location, spawnLoc)
|
||||||
ball.delete()
|
ball.delete()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ class GenderCharacter(DefaultCharacter):
|
||||||
pronoun = _GENDER_PRONOUN_MAP[gender][typ.lower()]
|
pronoun = _GENDER_PRONOUN_MAP[gender][typ.lower()]
|
||||||
return pronoun.capitalize() if typ.isupper() else pronoun
|
return pronoun.capitalize() if typ.isupper() else pronoun
|
||||||
|
|
||||||
def msg(self, text, from_obj=None, session=None, **kwargs):
|
def msg(self, text=None, from_obj=None, session=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Emits something to a session attached to the object.
|
Emits something to a session attached to the object.
|
||||||
Overloads the default msg() implementation to include
|
Overloads the default msg() implementation to include
|
||||||
|
|
@ -141,6 +141,10 @@ class GenderCharacter(DefaultCharacter):
|
||||||
All extra kwargs will be passed on to the protocol.
|
All extra kwargs will be passed on to the protocol.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if text is None:
|
||||||
|
super().msg(from_obj=from_obj, session=session, **kwargs)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if text and isinstance(text, tuple):
|
if text and isinstance(text, tuple):
|
||||||
text = (_RE_GENDER_PRONOUN.sub(self._get_pronoun, text[0]), *text[1:])
|
text = (_RE_GENDER_PRONOUN.sub(self._get_pronoun, text[0]), *text[1:])
|
||||||
|
|
|
||||||
|
|
@ -872,20 +872,17 @@ class TestCustomGameTime(EvenniaTest):
|
||||||
|
|
||||||
# Test dice module
|
# Test dice module
|
||||||
|
|
||||||
|
from evennia.contrib import dice # noqa
|
||||||
|
|
||||||
@patch("random.randint", return_value=5)
|
|
||||||
|
@patch("evennia.contrib.dice.randint", return_value=5)
|
||||||
class TestDice(CommandTest):
|
class TestDice(CommandTest):
|
||||||
def test_roll_dice(self, mocked_randint):
|
def test_roll_dice(self, mocked_randint):
|
||||||
# we must import dice here for the mocked randint to apply correctly.
|
|
||||||
from evennia.contrib import dice
|
|
||||||
|
|
||||||
self.assertEqual(dice.roll_dice(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4)
|
self.assertEqual(dice.roll_dice(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4)
|
||||||
self.assertEqual(dice.roll_dice(6, 6, conditional=("<", 35)), True)
|
self.assertEqual(dice.roll_dice(6, 6, conditional=("<", 35)), True)
|
||||||
self.assertEqual(dice.roll_dice(6, 6, conditional=(">", 33)), False)
|
self.assertEqual(dice.roll_dice(6, 6, conditional=(">", 33)), False)
|
||||||
|
|
||||||
def test_cmddice(self, mocked_randint):
|
def test_cmddice(self, mocked_randint):
|
||||||
from evennia.contrib import dice
|
|
||||||
|
|
||||||
self.call(
|
self.call(
|
||||||
dice.CmdDice(), "3d6 + 4", "You roll 3d6 + 4.| Roll(s): 5, 5 and 5. Total result is 19."
|
dice.CmdDice(), "3d6 + 4", "You roll 3d6 + 4.| Roll(s): 5, 5 and 5. Total result is 19."
|
||||||
)
|
)
|
||||||
|
|
@ -896,7 +893,7 @@ class TestDice(CommandTest):
|
||||||
# Test email-login
|
# Test email-login
|
||||||
|
|
||||||
|
|
||||||
from evennia.contrib import email_login
|
from evennia.contrib import email_login # noqa
|
||||||
|
|
||||||
|
|
||||||
class TestEmailLogin(CommandTest):
|
class TestEmailLogin(CommandTest):
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
# HEADER
|
#HEADER
|
||||||
|
|
||||||
# everything in this block will be appended to the beginning of
|
# everything in this block will be appended to the beginning of
|
||||||
# all other #CODE blocks when they are executed.
|
# all other #CODE blocks when they are executed.
|
||||||
|
|
@ -51,7 +51,7 @@ from evennia import DefaultObject
|
||||||
limbo = search_object("Limbo")[0]
|
limbo = search_object("Limbo")[0]
|
||||||
|
|
||||||
|
|
||||||
# CODE
|
#CODE
|
||||||
|
|
||||||
# This is the first code block. Within each block, Python
|
# This is the first code block. Within each block, Python
|
||||||
# code works as normal. Note how we make use if imports and
|
# code works as normal. Note how we make use if imports and
|
||||||
|
|
@ -67,7 +67,7 @@ red_button = create_object(
|
||||||
# we take a look at what we created
|
# we take a look at what we created
|
||||||
caller.msg("A %s was created." % red_button.key)
|
caller.msg("A %s was created." % red_button.key)
|
||||||
|
|
||||||
# CODE
|
#CODE
|
||||||
|
|
||||||
# this code block has 'table' and 'chair' set as deletable
|
# this code block has 'table' and 'chair' set as deletable
|
||||||
# objects. This means that when the batchcode processor runs in
|
# objects. This means that when the batchcode processor runs in
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,11 @@ needed on the Evennia side.
|
||||||
|
|
||||||
MSSPTable = {
|
MSSPTable = {
|
||||||
# Required fields
|
# Required fields
|
||||||
"NAME": "Evennia",
|
"NAME": "Mygame", # usually the same as SERVERNAME
|
||||||
# Generic
|
# Generic
|
||||||
"CRAWL DELAY": "-1", # limit how often crawler updates the listing. -1 for no limit
|
"CRAWL DELAY": "-1", # limit how often crawler may update the listing. -1 for no limit
|
||||||
"HOSTNAME": "", # current or new hostname
|
"HOSTNAME": "", # telnet hostname
|
||||||
"PORT": ["4000"], # most important port should be *last* in list!
|
"PORT": ["4000"], # telnet port - most important port should be *last* in list!
|
||||||
"CODEBASE": "Evennia",
|
"CODEBASE": "Evennia",
|
||||||
"CONTACT": "", # email for contacting the mud
|
"CONTACT": "", # email for contacting the mud
|
||||||
"CREATED": "", # year MUD was created
|
"CREATED": "", # year MUD was created
|
||||||
|
|
@ -33,7 +33,7 @@ MSSPTable = {
|
||||||
"LANGUAGE": "", # name of language used, e.g. English
|
"LANGUAGE": "", # name of language used, e.g. English
|
||||||
"LOCATION": "", # full English name of server country
|
"LOCATION": "", # full English name of server country
|
||||||
"MINIMUM AGE": "0", # set to 0 if not applicable
|
"MINIMUM AGE": "0", # set to 0 if not applicable
|
||||||
"WEBSITE": "www.evennia.com",
|
"WEBSITE": "", # http:// address to your game website
|
||||||
# Categorisation
|
# Categorisation
|
||||||
"FAMILY": "Custom", # evennia goes under 'Custom'
|
"FAMILY": "Custom", # evennia goes under 'Custom'
|
||||||
"GENRE": "None", # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction
|
"GENRE": "None", # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction
|
||||||
|
|
@ -41,10 +41,10 @@ MSSPTable = {
|
||||||
# Player versus Player, Player versus Environment,
|
# Player versus Player, Player versus Environment,
|
||||||
# Roleplaying, Simulation, Social or Strategy
|
# Roleplaying, Simulation, Social or Strategy
|
||||||
"GAMEPLAY": "",
|
"GAMEPLAY": "",
|
||||||
"STATUS": "Open Beta", # Alpha, Closed Beta, Open Beta, Live
|
"STATUS": "Open Beta", # Allowed: Alpha, Closed Beta, Open Beta, Live
|
||||||
"GAMESYSTEM": "Custom", # D&D, d20 System, World of Darkness, etc. Use Custom if homebrew
|
"GAMESYSTEM": "Custom", # D&D, d20 System, World of Darkness, etc. Use Custom if homebrew
|
||||||
# Subgenre: LASG, Medieval Fantasy, World War II, Frankenstein,
|
# Subgenre: LASG, Medieval Fantasy, World War II, Frankenstein,
|
||||||
# Cyberpunk, Dragonlance, etc. Or None if not available.
|
# Cyberpunk, Dragonlance, etc. Or None if not applicable.
|
||||||
"SUBGENRE": "None",
|
"SUBGENRE": "None",
|
||||||
# World
|
# World
|
||||||
"AREAS": "0",
|
"AREAS": "0",
|
||||||
|
|
@ -56,7 +56,7 @@ MSSPTable = {
|
||||||
"LEVELS": "0", # use 0 if level-less
|
"LEVELS": "0", # use 0 if level-less
|
||||||
"RACES": "0", # use 0 if race-less
|
"RACES": "0", # use 0 if race-less
|
||||||
"SKILLS": "0", # use 0 if skill-less
|
"SKILLS": "0", # use 0 if skill-less
|
||||||
# Protocols set to 1 or 0)
|
# Protocols set to 1 or 0; should usually not be changed)
|
||||||
"ANSI": "1",
|
"ANSI": "1",
|
||||||
"GMCP": "1",
|
"GMCP": "1",
|
||||||
"MSDP": "1",
|
"MSDP": "1",
|
||||||
|
|
|
||||||
|
|
@ -2,40 +2,56 @@
|
||||||
Prototypes
|
Prototypes
|
||||||
|
|
||||||
A prototype is a simple way to create individualized instances of a
|
A prototype is a simple way to create individualized instances of a
|
||||||
given `Typeclass`. For example, you might have a Sword typeclass that
|
given typeclass. It is dictionary with specific key names.
|
||||||
implements everything a Sword would need to do. The only difference
|
|
||||||
between different individual Swords would be their key, description
|
|
||||||
and some Attributes. The Prototype system allows to create a range of
|
|
||||||
such Swords with only minor variations. Prototypes can also inherit
|
|
||||||
and combine together to form entire hierarchies (such as giving all
|
|
||||||
Sabres and all Broadswords some common properties). Note that bigger
|
|
||||||
variations, such as custom commands or functionality belong in a
|
|
||||||
hierarchy of typeclasses instead.
|
|
||||||
|
|
||||||
Example prototypes are read by the `@spawn` command but is also easily
|
For example, you might have a Sword typeclass that implements everything a
|
||||||
available to use from code via `evennia.spawn` or `evennia.utils.spawner`.
|
Sword would need to do. The only difference between different individual Swords
|
||||||
Each prototype should be a dictionary. Use the same name as the
|
would be their key, description and some Attributes. The Prototype system
|
||||||
variable to refer to other prototypes.
|
allows to create a range of such Swords with only minor variations. Prototypes
|
||||||
|
can also inherit and combine together to form entire hierarchies (such as
|
||||||
|
giving all Sabres and all Broadswords some common properties). Note that bigger
|
||||||
|
variations, such as custom commands or functionality belong in a hierarchy of
|
||||||
|
typeclasses instead.
|
||||||
|
|
||||||
|
A prototype can either be a dictionary placed into a global variable in a
|
||||||
|
python module (a 'module-prototype') or stored in the database as a dict on a
|
||||||
|
special Script (a db-prototype). The former can be created just by adding dicts
|
||||||
|
to modules Evennia looks at for prototypes, the latter is easiest created
|
||||||
|
in-game via the `olc` command/menu.
|
||||||
|
|
||||||
|
Prototypes are read and used to create new objects with the `spawn` command
|
||||||
|
or directly via `evennia.spawn` or the full path `evennia.prototypes.spawner.spawn`.
|
||||||
|
|
||||||
|
A prototype dictionary have the following keywords:
|
||||||
|
|
||||||
Possible keywords are:
|
Possible keywords are:
|
||||||
prototype_parent - string pointing to parent prototype of this structure.
|
- `prototype_key` - the name of the prototype. This is required for db-prototypes,
|
||||||
key - string, the main object identifier.
|
for module-prototypes, the global variable name of the dict is used instead
|
||||||
typeclass - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`.
|
- `prototype_parent` - string pointing to parent prototype if any. Prototype inherits
|
||||||
location - this should be a valid object or #dbref.
|
in a similar way as classes, with children overriding values in their partents.
|
||||||
home - valid object or #dbref.
|
- `key` - string, the main object identifier.
|
||||||
destination - only valid for exits (object or dbref).
|
- `typeclass` - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`.
|
||||||
|
- `location` - this should be a valid object or #dbref.
|
||||||
|
- `home` - valid object or #dbref.
|
||||||
|
- `destination` - only valid for exits (object or #dbref).
|
||||||
|
- `permissions` - string or list of permission strings.
|
||||||
|
- `locks` - a lock-string to use for the spawned object.
|
||||||
|
- `aliases` - string or list of strings.
|
||||||
|
- `attrs` - Attributes, expressed as a list of tuples on the form `(attrname, value)`,
|
||||||
|
`(attrname, value, category)`, or `(attrname, value, category, locks)`. If using one
|
||||||
|
of the shorter forms, defaults are used for the rest.
|
||||||
|
- `tags` - Tags, as a list of tuples `(tag,)`, `(tag, category)` or `(tag, category, data)`.
|
||||||
|
- Any other keywords are interpreted as Attributes with no category or lock.
|
||||||
|
These will internally be added to `attrs` (eqivalent to `(attrname, value)`.
|
||||||
|
|
||||||
permissions - string or list of permission strings.
|
See the `spawn` command and `evennia.prototypes.spawner.spawn` for more info.
|
||||||
locks - a lock-string.
|
|
||||||
aliases - string or list of strings.
|
|
||||||
|
|
||||||
ndb_<name> - value of a nattribute (the "ndb_" part is ignored).
|
|
||||||
any other keywords are interpreted as Attributes and their values.
|
|
||||||
|
|
||||||
See the `@spawn` command and `evennia.utils.spawner` for more info.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
## example of module-based prototypes using
|
||||||
|
## the variable name as `prototype_key` and
|
||||||
|
## simple Attributes
|
||||||
|
|
||||||
# from random import randint
|
# from random import randint
|
||||||
#
|
#
|
||||||
# GOBLIN = {
|
# GOBLIN = {
|
||||||
|
|
@ -43,7 +59,8 @@ See the `@spawn` command and `evennia.utils.spawner` for more info.
|
||||||
# "health": lambda: randint(20,30),
|
# "health": lambda: randint(20,30),
|
||||||
# "resists": ["cold", "poison"],
|
# "resists": ["cold", "poison"],
|
||||||
# "attacks": ["fists"],
|
# "attacks": ["fists"],
|
||||||
# "weaknesses": ["fire", "light"]
|
# "weaknesses": ["fire", "light"],
|
||||||
|
# "tags": = [("greenskin", "monster"), ("humanoid", "monster")]
|
||||||
# }
|
# }
|
||||||
#
|
#
|
||||||
# GOBLIN_WIZARD = {
|
# GOBLIN_WIZARD = {
|
||||||
|
|
|
||||||
|
|
@ -343,22 +343,23 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
||||||
singular (str): The singular form to display.
|
singular (str): The singular form to display.
|
||||||
plural (str): The determined plural form of the key, including the count.
|
plural (str): The determined plural form of the key, including the count.
|
||||||
"""
|
"""
|
||||||
|
plural_category = "plural_key"
|
||||||
key = kwargs.get("key", self.key)
|
key = kwargs.get("key", self.key)
|
||||||
key = ansi.ANSIString(key) # this is needed to allow inflection of colored names
|
key = ansi.ANSIString(key) # this is needed to allow inflection of colored names
|
||||||
try:
|
try:
|
||||||
plural = _INFLECT.plural(key, 2)
|
plural = _INFLECT.plural(key, count)
|
||||||
plural = "%s %s" % (_INFLECT.number_to_words(count, threshold=12), plural)
|
plural = "{} {}".format(_INFLECT.number_to_words(count, threshold=12), plural)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# this is raised by inflect if the input is not a proper noun
|
# this is raised by inflect if the input is not a proper noun
|
||||||
plural = key
|
plural = key
|
||||||
singular = _INFLECT.an(key)
|
singular = _INFLECT.an(key)
|
||||||
if not self.aliases.get(plural, category="plural_key"):
|
if not self.aliases.get(plural, category=plural_category):
|
||||||
# we need to wipe any old plurals/an/a in case key changed in the interrim
|
# we need to wipe any old plurals/an/a in case key changed in the interrim
|
||||||
self.aliases.clear(category="plural_key")
|
self.aliases.clear(category=plural_category)
|
||||||
self.aliases.add(plural, category="plural_key")
|
self.aliases.add(plural, category=plural_category)
|
||||||
# save the singular form as an alias here too so we can display "an egg" and also
|
# save the singular form as an alias here too so we can display "an egg" and also
|
||||||
# look at 'an egg'.
|
# look at 'an egg'.
|
||||||
self.aliases.add(singular, category="plural_key")
|
self.aliases.add(singular, category=plural_category)
|
||||||
return singular, plural
|
return singular, plural
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,13 @@ import hashlib
|
||||||
import time
|
import time
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db.models import Q, Subquery
|
||||||
|
from django.core.paginator import Paginator
|
||||||
from evennia.scripts.scripts import DefaultScript
|
from evennia.scripts.scripts import DefaultScript
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
|
from evennia.typeclasses.attributes import Attribute
|
||||||
from evennia.utils.create import create_script
|
from evennia.utils.create import create_script
|
||||||
|
from evennia.utils.evmore import EvMore
|
||||||
from evennia.utils.utils import (
|
from evennia.utils.utils import (
|
||||||
all_from_module,
|
all_from_module,
|
||||||
make_iter,
|
make_iter,
|
||||||
|
|
@ -163,7 +167,8 @@ for mod in settings.PROTOTYPE_MODULES:
|
||||||
if "prototype_locks" in prot
|
if "prototype_locks" in prot
|
||||||
else "use:all();edit:false()"
|
else "use:all();edit:false()"
|
||||||
),
|
),
|
||||||
"prototype_tags": list(set(make_iter(prot.get("prototype_tags", [])) + ["module"])),
|
"prototype_tags": list(set(list(
|
||||||
|
make_iter(prot.get("prototype_tags", []))) + ["module"])),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
_MODULE_PROTOTYPES[actual_prot_key] = prot
|
_MODULE_PROTOTYPES[actual_prot_key] = prot
|
||||||
|
|
@ -320,7 +325,7 @@ def delete_prototype(prototype_key, caller=None):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def search_prototype(key=None, tags=None, require_single=False):
|
def search_prototype(key=None, tags=None, require_single=False, return_iterators=False):
|
||||||
"""
|
"""
|
||||||
Find prototypes based on key and/or tags, or all prototypes.
|
Find prototypes based on key and/or tags, or all prototypes.
|
||||||
|
|
||||||
|
|
@ -331,11 +336,17 @@ def search_prototype(key=None, tags=None, require_single=False):
|
||||||
tag category.
|
tag category.
|
||||||
require_single (bool): If set, raise KeyError if the result
|
require_single (bool): If set, raise KeyError if the result
|
||||||
was not found or if there are multiple matches.
|
was not found or if there are multiple matches.
|
||||||
|
return_iterators (bool): Optimized return for large numbers of db-prototypes.
|
||||||
|
If set, separate returns of module based prototypes and paginate
|
||||||
|
the db-prototype return.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
matches (list): All found prototype dicts. Empty list if
|
matches (list): Default return, all found prototype dicts. Empty list if
|
||||||
no match was found. Note that if neither `key` nor `tags`
|
no match was found. Note that if neither `key` nor `tags`
|
||||||
were given, *all* available prototypes will be returned.
|
were given, *all* available prototypes will be returned.
|
||||||
|
list, queryset: If `return_iterators` are found, this is a list of
|
||||||
|
module-based prototypes followed by a *paginated* queryset of
|
||||||
|
db-prototypes.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
KeyError: If `require_single` is True and there are 0 or >1 matches.
|
KeyError: If `require_single` is True and there are 0 or >1 matches.
|
||||||
|
|
@ -381,33 +392,51 @@ def search_prototype(key=None, tags=None, require_single=False):
|
||||||
# exact match on tag(s)
|
# exact match on tag(s)
|
||||||
tags = make_iter(tags)
|
tags = make_iter(tags)
|
||||||
tag_categories = ["db_prototype" for _ in tags]
|
tag_categories = ["db_prototype" for _ in tags]
|
||||||
db_matches = DbPrototype.objects.get_by_tag(tags, tag_categories)
|
db_matches = DbPrototype.objects.get_by_tag(
|
||||||
|
tags, tag_categories)
|
||||||
else:
|
else:
|
||||||
db_matches = DbPrototype.objects.all().order_by("id")
|
db_matches = DbPrototype.objects.all()
|
||||||
|
|
||||||
if key:
|
if key:
|
||||||
# exact or partial match on key
|
# exact or partial match on key
|
||||||
|
exact_match = (
|
||||||
|
db_matches
|
||||||
|
.filter(
|
||||||
|
Q(db_key__iexact=key))
|
||||||
|
.order_by("db_key")
|
||||||
|
)
|
||||||
|
if not exact_match:
|
||||||
|
# try with partial match instead
|
||||||
db_matches = (
|
db_matches = (
|
||||||
db_matches.filter(db_key=key) or db_matches.filter(db_key__icontains=key)
|
db_matches
|
||||||
).order_by("id")
|
.filter(
|
||||||
# return prototype
|
Q(db_key__icontains=key))
|
||||||
db_prototypes = [dbprot.prototype for dbprot in db_matches]
|
.order_by("db_key")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
db_matches = exact_match
|
||||||
|
|
||||||
matches = db_prototypes + module_prototypes
|
# convert to prototype
|
||||||
nmatches = len(matches)
|
db_ids = db_matches.values_list("id", flat=True)
|
||||||
if nmatches > 1 and key:
|
db_matches = (
|
||||||
key = key.lower()
|
Attribute.objects
|
||||||
# avoid duplicates if an exact match exist between the two types
|
.filter(scriptdb__pk__in=db_ids, db_key="prototype")
|
||||||
filter_matches = [
|
.values_list("db_value", flat=True)
|
||||||
mta for mta in matches if mta.get("prototype_key") and mta["prototype_key"] == key
|
.order_by("scriptdb__db_key")
|
||||||
]
|
)
|
||||||
if filter_matches and len(filter_matches) < nmatches:
|
if key and require_single:
|
||||||
matches = filter_matches
|
nmodules = len(module_prototypes)
|
||||||
|
ndbprots = db_matches.count()
|
||||||
|
if nmodules + ndbprots != 1:
|
||||||
|
raise KeyError(f"Found {nmodules + ndbprots} matching prototypes.")
|
||||||
|
|
||||||
nmatches = len(matches)
|
if return_iterators:
|
||||||
if nmatches != 1 and require_single:
|
# trying to get the entire set of prototypes - we must paginate
|
||||||
raise KeyError("Found {} matching prototypes.".format(nmatches))
|
# the result instead of trying to fetch the entire set at once
|
||||||
|
return db_matches, module_prototypes
|
||||||
return matches
|
else:
|
||||||
|
# full fetch, no pagination (compatibility mode)
|
||||||
|
return list(db_matches) + module_prototypes
|
||||||
|
|
||||||
|
|
||||||
def search_objects_with_prototype(prototype_key):
|
def search_objects_with_prototype(prototype_key):
|
||||||
|
|
@ -424,7 +453,109 @@ def search_objects_with_prototype(prototype_key):
|
||||||
return ObjectDB.objects.get_by_tag(key=prototype_key, category=PROTOTYPE_TAG_CATEGORY)
|
return ObjectDB.objects.get_by_tag(key=prototype_key, category=PROTOTYPE_TAG_CATEGORY)
|
||||||
|
|
||||||
|
|
||||||
def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True):
|
class PrototypeEvMore(EvMore):
|
||||||
|
"""
|
||||||
|
Listing 1000+ prototypes can be very slow. So we customize EvMore to
|
||||||
|
display an EvTable per paginated page rather than to try creating an
|
||||||
|
EvTable for the entire dataset and then paginate it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, caller, *args, session=None, **kwargs):
|
||||||
|
"""Store some extra properties on the EvMore class"""
|
||||||
|
self.show_non_use = kwargs.pop("show_non_use", False)
|
||||||
|
self.show_non_edit = kwargs.pop("show_non_edit", False)
|
||||||
|
super().__init__(caller, *args, session=session, **kwargs)
|
||||||
|
|
||||||
|
def init_pages(self, inp):
|
||||||
|
"""
|
||||||
|
This will be initialized with a tuple (mod_prototype_list, paginated_db_query)
|
||||||
|
and we must handle these separately since they cannot be paginated in the same
|
||||||
|
way. We will build the prototypes so that the db-prototypes come first (they
|
||||||
|
are likely the most volatile), followed by the mod-prototypes.
|
||||||
|
"""
|
||||||
|
dbprot_query, modprot_list = inp
|
||||||
|
# set the number of entries per page to half the reported height of the screen
|
||||||
|
# to account for long descs etc
|
||||||
|
dbprot_paged = Paginator(dbprot_query, max(1, int(self.height / 2)))
|
||||||
|
|
||||||
|
# we separate the different types of data, so we track how many pages there are
|
||||||
|
# of each.
|
||||||
|
n_mod = len(modprot_list)
|
||||||
|
self._npages_mod = n_mod // self.height + (0 if n_mod % self.height == 0 else 1)
|
||||||
|
self._db_count = dbprot_paged.count
|
||||||
|
self._npages_db = dbprot_paged.num_pages if self._db_count > 0 else 0
|
||||||
|
# total number of pages
|
||||||
|
self._npages = self._npages_mod + self._npages_db
|
||||||
|
self._data = (dbprot_paged, modprot_list)
|
||||||
|
self._paginator = self.prototype_paginator
|
||||||
|
|
||||||
|
def prototype_paginator(self, pageno):
|
||||||
|
"""
|
||||||
|
The listing is separated in db/mod prototypes, so we need to figure out which
|
||||||
|
one to pick based on the page number. Also, pageno starts from 0.
|
||||||
|
"""
|
||||||
|
dbprot_pages, modprot_list = self._data
|
||||||
|
|
||||||
|
if self._db_count and pageno < self._npages_db:
|
||||||
|
return dbprot_pages.page(pageno + 1)
|
||||||
|
else:
|
||||||
|
# get the correct slice, adjusted for the db-prototypes
|
||||||
|
pageno = max(0, pageno - self._npages_db)
|
||||||
|
return modprot_list[pageno * self.height: pageno * self.height + self.height]
|
||||||
|
|
||||||
|
def page_formatter(self, page):
|
||||||
|
"""Input is a queryset page from django.Paginator"""
|
||||||
|
caller = self._caller
|
||||||
|
|
||||||
|
# get use-permissions of readonly attributes (edit is always False)
|
||||||
|
display_tuples = []
|
||||||
|
|
||||||
|
table = EvTable(
|
||||||
|
"|wKey|n",
|
||||||
|
"|wSpawn/Edit|n",
|
||||||
|
"|wTags|n",
|
||||||
|
"|wDesc|n",
|
||||||
|
border="tablecols",
|
||||||
|
crop=True,
|
||||||
|
width=self.width
|
||||||
|
)
|
||||||
|
|
||||||
|
for prototype in page:
|
||||||
|
lock_use = caller.locks.check_lockstring(
|
||||||
|
caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True
|
||||||
|
)
|
||||||
|
if not self.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", default=True
|
||||||
|
)
|
||||||
|
if not self.show_non_edit and not lock_edit:
|
||||||
|
continue
|
||||||
|
ptags = []
|
||||||
|
for ptag in prototype.get("prototype_tags", []):
|
||||||
|
if is_iter(ptag):
|
||||||
|
if len(ptag) > 1:
|
||||||
|
ptags.append("{}".format(ptag[0]))
|
||||||
|
else:
|
||||||
|
ptags.append(ptag[0])
|
||||||
|
else:
|
||||||
|
ptags.append(str(ptag))
|
||||||
|
|
||||||
|
table.add_row(
|
||||||
|
prototype.get("prototype_key", "<unset>"),
|
||||||
|
"{}/{}".format("Y" if lock_use else "N", "Y" if lock_edit else "N"),
|
||||||
|
", ".join(list(set(ptags))),
|
||||||
|
prototype.get("prototype_desc", "<unset>"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return str(table)
|
||||||
|
|
||||||
|
|
||||||
|
def list_prototypes(caller, key=None, tags=None, show_non_use=False,
|
||||||
|
show_non_edit=True, session=None):
|
||||||
"""
|
"""
|
||||||
Collate a list of found prototypes based on search criteria and access.
|
Collate a list of found prototypes based on search criteria and access.
|
||||||
|
|
||||||
|
|
@ -434,66 +565,26 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed
|
||||||
tags (str or list, optional): Tag key or keys to query for.
|
tags (str or list, optional): Tag key or keys to query for.
|
||||||
show_non_use (bool, optional): Show also prototypes the caller may not use.
|
show_non_use (bool, optional): Show also prototypes the caller may not use.
|
||||||
show_non_edit (bool, optional): Show also prototypes the caller may not edit.
|
show_non_edit (bool, optional): Show also prototypes the caller may not edit.
|
||||||
|
session (Session, optional): If given, this is used for display formatting.
|
||||||
Returns:
|
Returns:
|
||||||
table (EvTable or None): An EvTable representation of the prototypes. None
|
PrototypeEvMore: An EvMore subclass optimized for prototype listings.
|
||||||
if no prototypes were found.
|
None: If no matches were found. In this case the caller has already been notified.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# this allows us to pass lists of empty strings
|
# this allows us to pass lists of empty strings
|
||||||
tags = [tag for tag in make_iter(tags) if tag]
|
tags = [tag for tag in make_iter(tags) if tag]
|
||||||
|
|
||||||
# get prototypes for readonly and db-based prototypes
|
dbprot_query, modprot_list = search_prototype(key, tags, return_iterators=True)
|
||||||
prototypes = search_prototype(key, tags)
|
|
||||||
|
|
||||||
# get use-permissions of readonly attributes (edit is always False)
|
if not dbprot_query and not modprot_list:
|
||||||
display_tuples = []
|
caller.msg("No prototypes found.", session=session)
|
||||||
for prototype in sorted(prototypes, key=lambda d: d.get("prototype_key", "")):
|
return None
|
||||||
lock_use = caller.locks.check_lockstring(
|
|
||||||
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", default=True
|
|
||||||
)
|
|
||||||
if not show_non_edit and not lock_edit:
|
|
||||||
continue
|
|
||||||
ptags = []
|
|
||||||
for ptag in prototype.get("prototype_tags", []):
|
|
||||||
if is_iter(ptag):
|
|
||||||
if len(ptag) > 1:
|
|
||||||
ptags.append("{} (category: {}".format(ptag[0], ptag[1]))
|
|
||||||
else:
|
|
||||||
ptags.append(ptag[0])
|
|
||||||
else:
|
|
||||||
ptags.append(str(ptag))
|
|
||||||
|
|
||||||
display_tuples.append(
|
|
||||||
(
|
|
||||||
prototype.get("prototype_key", "<unset>"),
|
|
||||||
prototype.get("prototype_desc", "<unset>"),
|
|
||||||
"{}/{}".format("Y" if lock_use else "N", "Y" if lock_edit else "N"),
|
|
||||||
",".join(ptags),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not display_tuples:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
table = []
|
|
||||||
width = 78
|
|
||||||
for i in range(len(display_tuples[0])):
|
|
||||||
table.append([str(display_tuple[i]) for display_tuple in display_tuples])
|
|
||||||
table = EvTable("Key", "Desc", "Spawn/Edit", "Tags", table=table, crop=True, width=width)
|
|
||||||
table.reformat_column(0, width=22)
|
|
||||||
table.reformat_column(1, width=29)
|
|
||||||
table.reformat_column(2, width=11, align="c")
|
|
||||||
table.reformat_column(3, width=16)
|
|
||||||
return table
|
|
||||||
|
|
||||||
|
# get specific prototype (one value or exception)
|
||||||
|
return PrototypeEvMore(caller, (dbprot_query, modprot_list),
|
||||||
|
session=session,
|
||||||
|
show_non_use=show_non_use,
|
||||||
|
show_non_edit=show_non_edit)
|
||||||
|
|
||||||
def validate_prototype(
|
def validate_prototype(
|
||||||
prototype, protkey=None, protparents=None, is_prototype_base=True, strict=True, _flags=None
|
prototype, protkey=None, protparents=None, is_prototype_base=True, strict=True, _flags=None
|
||||||
|
|
@ -569,7 +660,7 @@ def validate_prototype(
|
||||||
protparent = protparents.get(protstring)
|
protparent = protparents.get(protstring)
|
||||||
if not protparent:
|
if not protparent:
|
||||||
_flags["errors"].append(
|
_flags["errors"].append(
|
||||||
"Prototype {}'s prototype_parent '{}' was not found.".format((protkey, protstring))
|
"Prototype {}'s prototype_parent '{}' was not found.".format(protkey, protstring)
|
||||||
)
|
)
|
||||||
if id(prototype) in _flags["visited"]:
|
if id(prototype) in _flags["visited"]:
|
||||||
_flags["errors"].append(
|
_flags["errors"].append(
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ Unit tests for the prototypes and spawner
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from random import randint
|
from random import randint, sample
|
||||||
import mock
|
import mock
|
||||||
|
import uuid
|
||||||
|
from time import time
|
||||||
from anything import Something
|
from anything import Something
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from evennia.utils.test_resources import EvenniaTest
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
|
|
@ -628,8 +630,10 @@ class TestPrototypeStorage(EvenniaTest):
|
||||||
|
|
||||||
# partial match
|
# partial match
|
||||||
with mock.patch("evennia.prototypes.prototypes._MODULE_PROTOTYPES", {}):
|
with mock.patch("evennia.prototypes.prototypes._MODULE_PROTOTYPES", {}):
|
||||||
self.assertEqual(list(protlib.search_prototype("prot")), [prot1b, prot2, prot3])
|
self.assertCountEqual(
|
||||||
self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3])
|
protlib.search_prototype("prot"), [prot1b, prot2, prot3])
|
||||||
|
self.assertCountEqual(
|
||||||
|
protlib.search_prototype(tags="foo1"), [prot1b, prot2, prot3])
|
||||||
|
|
||||||
self.assertTrue(str(str(protlib.list_prototypes(self.char1))))
|
self.assertTrue(str(str(protlib.list_prototypes(self.char1))))
|
||||||
|
|
||||||
|
|
@ -1073,3 +1077,29 @@ class TestOLCMenu(TestEvMenu):
|
||||||
["node_index", "node_index", "node_index"],
|
["node_index", "node_index", "node_index"],
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
class PrototypeCrashTest(EvenniaTest):
|
||||||
|
|
||||||
|
# increase this to 1000 for optimization testing
|
||||||
|
num_prototypes = 10
|
||||||
|
|
||||||
|
def create(self, num=None):
|
||||||
|
if not num:
|
||||||
|
num = self.num_prototypes
|
||||||
|
# print(f"Creating {num} additional prototypes...")
|
||||||
|
for x in range(num):
|
||||||
|
prot = {
|
||||||
|
'prototype_key': str(uuid.uuid4()),
|
||||||
|
'some_attributes': [str(uuid.uuid4()) for x in range(10)],
|
||||||
|
'prototype_tags': list(sample(['demo', 'test', 'stuff'], 2)),
|
||||||
|
}
|
||||||
|
protlib.save_prototype(prot)
|
||||||
|
|
||||||
|
def test_prototype_dos(self, *args, **kwargs):
|
||||||
|
num_prototypes = self.num_prototypes
|
||||||
|
for x in range(2):
|
||||||
|
self.create(num_prototypes)
|
||||||
|
# print("Attempting to list prototypes...")
|
||||||
|
# start_time = time()
|
||||||
|
self.char1.execute_cmd('spawn/list')
|
||||||
|
# print(f"Prototypes listed in {time()-start_time} seconds.")
|
||||||
|
|
|
||||||
|
|
@ -437,7 +437,7 @@ class DefaultScript(ScriptBase):
|
||||||
if self.is_active and not force_restart:
|
if self.is_active and not force_restart:
|
||||||
# The script is already running, but make sure we have a _task if
|
# The script is already running, but make sure we have a _task if
|
||||||
# this is after a cache flush
|
# this is after a cache flush
|
||||||
if not self.ndb._task and self.db_interval >= 0:
|
if not self.ndb._task and self.db_interval > 0:
|
||||||
self.ndb._task = ExtendedLoopingCall(self._step_task)
|
self.ndb._task = ExtendedLoopingCall(self._step_task)
|
||||||
try:
|
try:
|
||||||
start_delay, callcount = SCRIPT_FLUSH_TIMERS[self.id]
|
start_delay, callcount = SCRIPT_FLUSH_TIMERS[self.id]
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class ConnectionWizard(object):
|
||||||
resp = str(default)
|
resp = str(default)
|
||||||
|
|
||||||
if resp.lower() in options:
|
if resp.lower() in options:
|
||||||
self.display(f" Selected '{resp}'.")
|
# self.display(f" Selected '{resp}'.")
|
||||||
desc, callback, kwargs = options[resp.lower()]
|
desc, callback, kwargs = options[resp.lower()]
|
||||||
callback(self, **kwargs)
|
callback(self, **kwargs)
|
||||||
elif resp.lower() in ("quit", "q"):
|
elif resp.lower() in ("quit", "q"):
|
||||||
|
|
@ -161,8 +161,10 @@ class ConnectionWizard(object):
|
||||||
|
|
||||||
def node_start(wizard):
|
def node_start(wizard):
|
||||||
text = """
|
text = """
|
||||||
This wizard helps activate external networks with Evennia. It will create
|
This wizard helps to attach your Evennia server to external networks. It
|
||||||
a config that will be attached to the bottom of the game settings file.
|
will save to a file `server/conf/connection_settings.py` that will be
|
||||||
|
imported from the bottom of your game settings file. Once generated you can
|
||||||
|
also modify that file directly.
|
||||||
|
|
||||||
Make sure you have at least started the game once before continuing!
|
Make sure you have at least started the game once before continuing!
|
||||||
|
|
||||||
|
|
@ -174,11 +176,18 @@ def node_start(wizard):
|
||||||
node_game_index_start,
|
node_game_index_start,
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
# "2": ("Add MSSP information (for mud-list crawlers)",
|
"2": ("MSSP setup (for mud-list crawlers)",
|
||||||
# node_mssp_start, {}),
|
node_mssp_start, {}
|
||||||
|
),
|
||||||
# "3": ("Add Grapevine listing",
|
# "3": ("Add Grapevine listing",
|
||||||
# node_grapevine_start, {}),
|
# node_grapevine_start, {}),
|
||||||
"2": ("View and Save created settings", node_view_and_apply_settings, {}),
|
# "4": ("Add IRC link",
|
||||||
|
# "node_irc_start", {}),
|
||||||
|
# "5" ("Add RSS feed",
|
||||||
|
# "node_rss_start", {}),
|
||||||
|
"s": ("View and (optionally) Save created settings",
|
||||||
|
node_view_and_apply_settings, {}),
|
||||||
|
"q": ("Quit", lambda *args: sys.exit(), {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
wizard.display(text)
|
wizard.display(text)
|
||||||
|
|
@ -189,13 +198,13 @@ def node_start(wizard):
|
||||||
|
|
||||||
|
|
||||||
def node_game_index_start(wizard, **kwargs):
|
def node_game_index_start(wizard, **kwargs):
|
||||||
text = f"""
|
text = """
|
||||||
The Evennia game index (http://games.evennia.com) lists both active Evennia
|
The Evennia game index (http://games.evennia.com) lists both active Evennia
|
||||||
games as well as games in various stages of development.
|
games as well as games in various stages of development.
|
||||||
|
|
||||||
You can put up your game in the index also if you are not (yet) open for
|
You can put up your game in the index also if you are not (yet) open for
|
||||||
players. If so, put 'None' for the connection details. Just tell us you
|
players. If so, put 'None' for the connection details - you are just telling
|
||||||
are out there and make us excited about your upcoming game!
|
us that you are out there, making us excited about your upcoming game!
|
||||||
|
|
||||||
Please check the listing online first to see that your exact game name is
|
Please check the listing online first to see that your exact game name is
|
||||||
not colliding with an existing game-name in the list (be nice!).
|
not colliding with an existing game-name in the list (be nice!).
|
||||||
|
|
@ -222,9 +231,9 @@ def node_game_index_fields(wizard, status=None):
|
||||||
- pre-alpha: a game in its very early stages, mostly unfinished or unstarted
|
- pre-alpha: a game in its very early stages, mostly unfinished or unstarted
|
||||||
- alpha: a working concept, probably lots of bugs and incomplete features
|
- alpha: a working concept, probably lots of bugs and incomplete features
|
||||||
- beta: a working game, but expect bugs and changing features
|
- beta: a working game, but expect bugs and changing features
|
||||||
- launched: a full, working game that may still be expanded upon and improved later
|
- launched: a full, working game (that may still be expanded upon and improved later)
|
||||||
|
|
||||||
Current value:
|
Current value (return to keep):
|
||||||
{status_default}
|
{status_default}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -233,6 +242,31 @@ def node_game_index_fields(wizard, status=None):
|
||||||
wizard.display(text)
|
wizard.display(text)
|
||||||
wizard.game_index_listing["game_status"] = wizard.ask_choice("Select one: ", options)
|
wizard.game_index_listing["game_status"] = wizard.ask_choice("Select one: ", options)
|
||||||
|
|
||||||
|
# game name
|
||||||
|
|
||||||
|
name_default = settings.SERVERNAME
|
||||||
|
text = f"""
|
||||||
|
Your game's name should usually be the same as `settings.SERVERNAME`, but
|
||||||
|
you can set it to something else here if you want.
|
||||||
|
|
||||||
|
Current value:
|
||||||
|
{name_default}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def name_validator(inp):
|
||||||
|
tmax = 80
|
||||||
|
tlen = len(inp)
|
||||||
|
if tlen > tmax:
|
||||||
|
print(f"The name must be shorter than {tmax} characters (was {tlen}).")
|
||||||
|
wizard.ask_continue()
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
wizard.display(text)
|
||||||
|
wizard.game_index_listing['game_name'] = wizard.ask_input(
|
||||||
|
default=name_default, validator=name_validator
|
||||||
|
)
|
||||||
|
|
||||||
# short desc
|
# short desc
|
||||||
|
|
||||||
sdesc_default = wizard.game_index_listing.get("short_description", None)
|
sdesc_default = wizard.game_index_listing.get("short_description", None)
|
||||||
|
|
@ -249,7 +283,7 @@ def node_game_index_fields(wizard, status=None):
|
||||||
def sdesc_validator(inp):
|
def sdesc_validator(inp):
|
||||||
tmax = 255
|
tmax = 255
|
||||||
tlen = len(inp)
|
tlen = len(inp)
|
||||||
if tlen > 255:
|
if tlen > tmax:
|
||||||
print(f"The short desc must be shorter than {tmax} characters (was {tlen}).")
|
print(f"The short desc must be shorter than {tmax} characters (was {tlen}).")
|
||||||
wizard.ask_continue()
|
wizard.ask_continue()
|
||||||
return False
|
return False
|
||||||
|
|
@ -341,7 +375,7 @@ def node_game_index_fields(wizard, status=None):
|
||||||
Evennia is its own web server and runs your game's website. Enter the
|
Evennia is its own web server and runs your game's website. Enter the
|
||||||
URL of the website here, like http://yourwebsite.com, here.
|
URL of the website here, like http://yourwebsite.com, here.
|
||||||
|
|
||||||
Wtite 'None' if you are not offering a publicly visible website at this time.
|
Write 'None' if you are not offering a publicly visible website at this time.
|
||||||
|
|
||||||
Current value:
|
Current value:
|
||||||
{website_default}
|
{website_default}
|
||||||
|
|
@ -359,7 +393,7 @@ def node_game_index_fields(wizard, status=None):
|
||||||
your specific URL here (when clicking this link you should launch into the
|
your specific URL here (when clicking this link you should launch into the
|
||||||
web client)
|
web client)
|
||||||
|
|
||||||
Wtite 'None' if you don't want to list a publicly accessible webclient.
|
Write 'None' if you don't want to list a publicly accessible webclient.
|
||||||
|
|
||||||
Current value:
|
Current value:
|
||||||
{webclient_default}
|
{webclient_default}
|
||||||
|
|
@ -388,24 +422,26 @@ def node_game_index_fields(wizard, status=None):
|
||||||
|
|
||||||
def node_mssp_start(wizard):
|
def node_mssp_start(wizard):
|
||||||
|
|
||||||
mssp_module = mod_import(settings.MSSP_META_MODULE)
|
mssp_module = mod_import(settings.MSSP_META_MODULE or "server.conf.mssp")
|
||||||
|
try:
|
||||||
filename = mssp_module.__file__
|
filename = mssp_module.__file__
|
||||||
|
except AttributeError:
|
||||||
|
filename = "server/conf/mssp.py"
|
||||||
|
|
||||||
text = f"""
|
text = f"""
|
||||||
MSSP (Mud Server Status Protocol) allows online MUD-listing sites/crawlers
|
MSSP (Mud Server Status Protocol) has a vast amount of options so it must
|
||||||
to continuously monitor your game and list information about it. Some of
|
be modified outside this wizard by directly editing its config file here:
|
||||||
this, like active player-count, Evennia will automatically add for you,
|
|
||||||
whereas many fields are manually added info about your game.
|
'{filename}'
|
||||||
|
|
||||||
|
MSSP allows traditional online MUD-listing sites/crawlers to continuously
|
||||||
|
monitor your game and list information about it. Some of this, like active
|
||||||
|
player-count, Evennia will automatically add for you, whereas most fields
|
||||||
|
you need to set manually.
|
||||||
|
|
||||||
To use MSSP you should generally have a publicly open game that external
|
To use MSSP you should generally have a publicly open game that external
|
||||||
players can connect to. You also need to register at a MUD listing site to
|
players can connect to. You also need to register at a MUD listing site to
|
||||||
tell them to list your game.
|
tell them to crawl your game.
|
||||||
|
|
||||||
MSSP has a large number of configuration options and we found it was simply
|
|
||||||
a lot easier to set them in a file rather than using this wizard. So to
|
|
||||||
configure MSSP, edit the empty template listing found here:
|
|
||||||
|
|
||||||
'{filename}'
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
wizard.display(text)
|
wizard.display(text)
|
||||||
|
|
@ -456,25 +492,31 @@ def node_view_and_apply_settings(wizard):
|
||||||
pp = pprint.PrettyPrinter(indent=4)
|
pp = pprint.PrettyPrinter(indent=4)
|
||||||
saves = False
|
saves = False
|
||||||
|
|
||||||
game_index_txt = "No changes to save for Game Index."
|
# game index
|
||||||
if hasattr(wizard, "game_index_listing"):
|
game_index_save_text = ""
|
||||||
if wizard.game_index_listing != settings.GAME_INDEX_LISTING:
|
game_index_listing = (wizard.game_index_listing if
|
||||||
game_index_txt = "No changes to save for Game Index."
|
hasattr(wizard, "game_index_listing") else None)
|
||||||
else:
|
if not game_index_listing and settings.GAME_INDEX_ENABLED:
|
||||||
game_index_txt = "GAME_INDEX_ENABLED = True\n" "GAME_INDEX_LISTING = \\\n" + pp.pformat(
|
game_index_listing = settings.GAME_INDEX_LISTING
|
||||||
wizard.game_index_listing
|
if game_index_listing:
|
||||||
|
game_index_save_text = (
|
||||||
|
"GAME_INDEX_ENABLED = True\n"
|
||||||
|
"GAME_INDEX_LISTING = \\\n" + pp.pformat(game_index_listing)
|
||||||
)
|
)
|
||||||
saves = True
|
saves = True
|
||||||
|
else:
|
||||||
|
game_index_save_text = "# No Game Index settings found."
|
||||||
|
|
||||||
text = game_index_txt
|
# potentially add other wizards in the future
|
||||||
|
text = game_index_save_text
|
||||||
|
|
||||||
wizard.display(f"Settings to save:\n\n{text}")
|
wizard.display(f"Settings to save:\n\n{text}")
|
||||||
|
|
||||||
if saves:
|
if saves:
|
||||||
if wizard.ask_yesno("Do you want to save these settings?") == "yes":
|
if wizard.ask_yesno("\nDo you want to save these settings?") == "yes":
|
||||||
wizard.save_output = text
|
wizard.save_output = text
|
||||||
_save_changes(wizard)
|
_save_changes(wizard)
|
||||||
wizard.display("... saved!")
|
wizard.display("... saved!\nThe changes will apply after you reload your server.")
|
||||||
else:
|
else:
|
||||||
wizard.display("... cancelled.")
|
wizard.display("... cancelled.")
|
||||||
wizard.ask_continue()
|
wizard.ask_continue()
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,8 @@ SRESET = chr(19) # shutdown server in reset mode
|
||||||
# requirements
|
# requirements
|
||||||
PYTHON_MIN = "3.7"
|
PYTHON_MIN = "3.7"
|
||||||
TWISTED_MIN = "18.0.0"
|
TWISTED_MIN = "18.0.0"
|
||||||
DJANGO_MIN = "2.1"
|
DJANGO_MIN = "2.2.5"
|
||||||
DJANGO_REC = "2.2"
|
DJANGO_LT = "3.0"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sys.path[1] = EVENNIA_ROOT
|
sys.path[1] = EVENNIA_ROOT
|
||||||
|
|
@ -374,8 +374,8 @@ ERROR_NOTWISTED = """
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ERROR_DJANGO_MIN = """
|
ERROR_DJANGO_MIN = """
|
||||||
ERROR: Django {dversion} found. Evennia requires version {django_min}
|
ERROR: Django {dversion} found. Evennia requires at least version {django_min} (but
|
||||||
or higher.
|
no higher than {django_lt}).
|
||||||
|
|
||||||
If you are using a virtualenv, use the command `pip install --upgrade -e evennia` where
|
If you are using a virtualenv, use the command `pip install --upgrade -e evennia` where
|
||||||
`evennia` is the folder to where you cloned the Evennia library. If not
|
`evennia` is the folder to where you cloned the Evennia library. If not
|
||||||
|
|
@ -386,14 +386,9 @@ ERROR_DJANGO_MIN = """
|
||||||
any warnings and don't run `makemigrate` even if told to.
|
any warnings and don't run `makemigrate` even if told to.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NOTE_DJANGO_MIN = """
|
|
||||||
NOTE: Django {dversion} found. This will work, but Django {django_rec} is
|
|
||||||
recommended for production.
|
|
||||||
"""
|
|
||||||
|
|
||||||
NOTE_DJANGO_NEW = """
|
NOTE_DJANGO_NEW = """
|
||||||
NOTE: Django {dversion} found. This is newer than Evennia's
|
NOTE: Django {dversion} found. This is newer than Evennia's
|
||||||
recommended version ({django_rec}). It might work, but may be new
|
recommended version ({django_rec}). It might work, but is new
|
||||||
enough to not be fully tested yet. Report any issues.
|
enough to not be fully tested yet. Report any issues.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -1283,12 +1278,11 @@ def check_main_evennia_dependencies():
|
||||||
# only the main version (1.5, not 1.5.4.0)
|
# only the main version (1.5, not 1.5.4.0)
|
||||||
dversion_main = ".".join(dversion.split(".")[:2])
|
dversion_main = ".".join(dversion.split(".")[:2])
|
||||||
if LooseVersion(dversion) < LooseVersion(DJANGO_MIN):
|
if LooseVersion(dversion) < LooseVersion(DJANGO_MIN):
|
||||||
print(ERROR_DJANGO_MIN.format(dversion=dversion_main, django_min=DJANGO_MIN))
|
print(ERROR_DJANGO_MIN.format(dversion=dversion_main, django_min=DJANGO_MIN,
|
||||||
|
django_lt=DJANGO_LT))
|
||||||
error = True
|
error = True
|
||||||
elif LooseVersion(DJANGO_MIN) <= LooseVersion(dversion) < LooseVersion(DJANGO_REC):
|
elif LooseVersion(DJANGO_LT) <= LooseVersion(dversion_main):
|
||||||
print(NOTE_DJANGO_MIN.format(dversion=dversion_main, django_rec=DJANGO_REC))
|
print(NOTE_DJANGO_NEW.format(dversion=dversion_main, django_rec=DJANGO_LT))
|
||||||
elif LooseVersion(DJANGO_REC) < LooseVersion(dversion_main):
|
|
||||||
print(NOTE_DJANGO_NEW.format(dversion=dversion_main, django_rec=DJANGO_REC))
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print(ERROR_NODJANGO)
|
print(ERROR_NODJANGO)
|
||||||
error = True
|
error = True
|
||||||
|
|
@ -1368,10 +1362,10 @@ def create_settings_file(init=True, secret_settings=False):
|
||||||
if not init:
|
if not init:
|
||||||
# if not --init mode, settings file may already exist from before
|
# if not --init mode, settings file may already exist from before
|
||||||
if os.path.exists(settings_path):
|
if os.path.exists(settings_path):
|
||||||
inp = eval(input("%s already exists. Do you want to reset it? y/[N]> " % settings_path))
|
inp = input("%s already exists. Do you want to reset it? y/[N]> " % settings_path)
|
||||||
if not inp.lower() == "y":
|
if not inp.lower() == "y":
|
||||||
print("Aborted.")
|
print("Aborted.")
|
||||||
return
|
sys.exit()
|
||||||
else:
|
else:
|
||||||
print("Reset the settings file.")
|
print("Reset the settings file.")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# start the Server
|
# start the Server
|
||||||
print("Portal starting server ... {}".format(server_twistd_cmd))
|
print("Portal starting server ... ")
|
||||||
process = None
|
process = None
|
||||||
with open(settings.SERVER_LOG_FILE, "a") as logfile:
|
with open(settings.SERVER_LOG_FILE, "a") as logfile:
|
||||||
# we link stdout to a file in order to catch
|
# we link stdout to a file in order to catch
|
||||||
|
|
|
||||||
|
|
@ -740,9 +740,9 @@ CHANNEL_CONNECTINFO = None
|
||||||
GAME_INDEX_ENABLED = False
|
GAME_INDEX_ENABLED = False
|
||||||
# This dict
|
# This dict
|
||||||
GAME_INDEX_LISTING = {
|
GAME_INDEX_LISTING = {
|
||||||
"game_name": SERVERNAME,
|
"game_name": "Mygame", # usually SERVERNAME
|
||||||
"game_status": "pre-alpha", # pre-alpha, alpha, beta or launched
|
"game_status": "pre-alpha", # pre-alpha, alpha, beta or launched
|
||||||
"short_description": GAME_SLOGAN,
|
"short_description": "", # could be GAME_SLOGAN
|
||||||
"long_description": "",
|
"long_description": "",
|
||||||
"listing_contact": "", # email
|
"listing_contact": "", # email
|
||||||
"telnet_hostname": "", # mygame.com
|
"telnet_hostname": "", # mygame.com
|
||||||
|
|
|
||||||
|
|
@ -778,6 +778,7 @@ class InMemoryAttributeBackend(IAttributeBackend):
|
||||||
See parent class.
|
See parent class.
|
||||||
|
|
||||||
strvalue has no meaning for InMemory attributes.
|
strvalue has no meaning for InMemory attributes.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
new_attr = self._attrclass(
|
new_attr = self._attrclass(
|
||||||
pk=self._next_id(), key=key, category=category, lock_storage=lockstring, value=value
|
pk=self._next_id(), key=key, category=category, lock_storage=lockstring, value=value
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ the `caller.msg()` construct every time the page is updated.
|
||||||
"""
|
"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
from django.core.paginator import Paginator
|
||||||
from evennia import Command, CmdSet
|
from evennia import Command, CmdSet
|
||||||
from evennia.commands import cmdhandler
|
from evennia.commands import cmdhandler
|
||||||
from evennia.utils.ansi import ANSIString
|
from evennia.utils.ansi import ANSIString
|
||||||
|
|
@ -140,7 +141,7 @@ class EvMore(object):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
caller,
|
caller,
|
||||||
text,
|
inp,
|
||||||
always_page=False,
|
always_page=False,
|
||||||
session=None,
|
session=None,
|
||||||
justify=False,
|
justify=False,
|
||||||
|
|
@ -152,29 +153,28 @@ class EvMore(object):
|
||||||
):
|
):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
EvMore pager
|
Initialization of the EvMore pager
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
caller (Object or Account): Entity reading the text.
|
caller (Object or Account): Entity reading the text.
|
||||||
text (str, EvTable or iterator): The text or data to put under paging.
|
inp (str, EvTable, Paginator or iterator): The text or data to put under paging.
|
||||||
|
- If a string, paginage normally. If this text contains
|
||||||
- If a string, paginate normally. If this text contains one or more `\\\\f` format
|
one or more `\f` format symbol, automatic pagination and justification
|
||||||
symbols, automatic pagination and justification are force-disabled and page-breaks
|
are force-disabled and page-breaks will only happen after each `\f`.
|
||||||
will only happen after each `\\\\f`.
|
|
||||||
- If `EvTable`, the EvTable will be paginated with the same
|
- If `EvTable`, the EvTable will be paginated with the same
|
||||||
setting on each page if it is too long. The table
|
setting on each page if it is too long. The table
|
||||||
decorations will be considered in the size of the page.
|
decorations will be considered in the size of the page.
|
||||||
- Otherwise `text` is converted to an iterator, where each step is
|
- Otherwise `inp` is converted to an iterator, where each step is
|
||||||
expected to be a line in the final display. Each line
|
expected to be a line in the final display. Each line
|
||||||
will be run through `iter_callable`.
|
will be run through `iter_callable`.
|
||||||
always_page (bool, optional): If `False`, the
|
always_page (bool, optional): If `False`, the
|
||||||
pager will only kick in if `text` is too big
|
pager will only kick in if `inp` is too big
|
||||||
to fit the screen.
|
to fit the screen.
|
||||||
session (Session, optional): If given, this session will be used
|
session (Session, optional): If given, this session will be used
|
||||||
to determine the screen width and will receive all output.
|
to determine the screen width and will receive all output.
|
||||||
justify (bool, optional): If set, auto-justify long lines. This must be turned
|
justify (bool, optional): If set, auto-justify long lines. This must be turned
|
||||||
off for fixed-width or formatted output, like tables. It's force-disabled
|
off for fixed-width or formatted output, like tables. It's force-disabled
|
||||||
if `text` is an EvTable.
|
if `inp` is an EvTable.
|
||||||
justify_kwargs (dict, optional): Keywords for the justifiy function. Used only
|
justify_kwargs (dict, optional): Keywords for the justifiy function. Used only
|
||||||
if `justify` is True. If this is not set, default arguments will be used.
|
if `justify` is True. If this is not set, default arguments will be used.
|
||||||
exit_on_lastpage (bool, optional): If reaching the last page without the
|
exit_on_lastpage (bool, optional): If reaching the last page without the
|
||||||
|
|
@ -185,12 +185,6 @@ class EvMore(object):
|
||||||
the caller when the more page exits. Note that this will be using whatever
|
the caller when the more page exits. Note that this will be using whatever
|
||||||
cmdset the user had *before* the evmore pager was activated (so none of
|
cmdset the user had *before* the evmore pager was activated (so none of
|
||||||
the evmore commands will be available when this is run).
|
the evmore commands will be available when this is run).
|
||||||
page_formatter (callable, optional): If given, this function will be passed the
|
|
||||||
contents of each extracted page. This is useful when paginating
|
|
||||||
data consisting something other than a string or a list of strings. Especially
|
|
||||||
queryset data is likely to always need this argument specified. Note however,
|
|
||||||
that all size calculations assume this function to return one single line
|
|
||||||
per element on the page!
|
|
||||||
kwargs (any, optional): These will be passed on to the `caller.msg` method.
|
kwargs (any, optional): These will be passed on to the `caller.msg` method.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
@ -198,16 +192,21 @@ class EvMore(object):
|
||||||
```python
|
```python
|
||||||
super_long_text = " ... "
|
super_long_text = " ... "
|
||||||
EvMore(caller, super_long_text)
|
EvMore(caller, super_long_text)
|
||||||
|
```
|
||||||
|
Paginator
|
||||||
|
```python
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
query = ObjectDB.objects.all()
|
query = ObjectDB.objects.all()
|
||||||
pages = Paginator(query, 10) # 10 objs per page
|
pages = Paginator(query, 10) # 10 objs per page
|
||||||
EvMore(caller, pages) # will repr() each object per line, 10 to a page
|
EvMore(caller, pages)
|
||||||
|
```
|
||||||
multi_page_table = [ [[..],[..]], ...]
|
Every page an EvTable
|
||||||
EvMore(caller, multi_page_table, use_evtable=True,
|
```python
|
||||||
evtable_args=("Header1", "Header2"),
|
from evennia import EvTable
|
||||||
evtable_kwargs={"align": "r", "border": "tablecols"})
|
def _to_evtable(page):
|
||||||
|
table = ... # convert page to a table
|
||||||
|
return EvTable(*headers, table=table, ...)
|
||||||
|
EvMore(caller, pages, page_formatter=_to_evtable)
|
||||||
```
|
```
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -228,141 +227,40 @@ class EvMore(object):
|
||||||
self.exit_on_lastpage = exit_on_lastpage
|
self.exit_on_lastpage = exit_on_lastpage
|
||||||
self.exit_cmd = exit_cmd
|
self.exit_cmd = exit_cmd
|
||||||
self._exit_msg = "Exited |wmore|n pager."
|
self._exit_msg = "Exited |wmore|n pager."
|
||||||
self._page_formatter = page_formatter
|
|
||||||
self._kwargs = kwargs
|
self._kwargs = kwargs
|
||||||
|
|
||||||
self._data = None
|
self._data = None
|
||||||
self._paginator = None
|
|
||||||
self._pages = []
|
self._pages = []
|
||||||
self._npages = 1
|
|
||||||
self._npos = 0
|
self._npos = 0
|
||||||
|
|
||||||
|
self._npages = 1
|
||||||
|
self._paginator = self.paginator_index
|
||||||
|
self._page_formatter = str
|
||||||
|
|
||||||
# set up individual pages for different sessions
|
# set up individual pages for different sessions
|
||||||
height = max(4, session.protocol_flags.get("SCREENHEIGHT", {0: _SCREEN_HEIGHT})[0] - 4)
|
height = max(4, session.protocol_flags.get("SCREENHEIGHT", {0: _SCREEN_HEIGHT})[0] - 4)
|
||||||
self.width = session.protocol_flags.get("SCREENWIDTH", {0: _SCREEN_WIDTH})[0]
|
self.width = session.protocol_flags.get("SCREENWIDTH", {0: _SCREEN_WIDTH})[0]
|
||||||
# always limit number of chars to 10 000 per page
|
# always limit number of chars to 10 000 per page
|
||||||
self.height = min(10000 // max(1, self.width), height)
|
self.height = min(10000 // max(1, self.width), height)
|
||||||
|
|
||||||
if inherits_from(text, "evennia.utils.evtable.EvTable"):
|
# does initial parsing of input
|
||||||
# an EvTable
|
self.init_pages(inp)
|
||||||
self.init_evtable(text)
|
|
||||||
elif isinstance(text, QuerySet):
|
|
||||||
# a queryset
|
|
||||||
self.init_queryset(text)
|
|
||||||
elif not isinstance(text, str):
|
|
||||||
# anything else not a str
|
|
||||||
self.init_iterable(text)
|
|
||||||
elif "\f" in text:
|
|
||||||
# string with \f line-break markers in it
|
|
||||||
self.init_f_str(text)
|
|
||||||
else:
|
|
||||||
# a string
|
|
||||||
self.init_str(text)
|
|
||||||
|
|
||||||
# kick things into gear
|
# kick things into gear
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
# page formatter
|
# EvMore functional methods
|
||||||
|
|
||||||
def format_page(self, page):
|
|
||||||
"""
|
|
||||||
Page formatter. Uses the page_formatter callable by default.
|
|
||||||
This allows to easier override the class if needed.
|
|
||||||
"""
|
|
||||||
return self._page_formatter(page)
|
|
||||||
|
|
||||||
# paginators - responsible for extracting a specific page number
|
|
||||||
|
|
||||||
def paginator_index(self, pageno):
|
|
||||||
"""Paginate to specific, known index"""
|
|
||||||
return self._data[pageno]
|
|
||||||
|
|
||||||
def paginator_slice(self, pageno):
|
|
||||||
"""
|
|
||||||
Paginate by slice. This is done with an eye on memory efficiency (usually for
|
|
||||||
querysets); to avoid fetching all objects at the same time.
|
|
||||||
"""
|
|
||||||
return self._data[pageno * self.height : pageno * self.height + self.height]
|
|
||||||
|
|
||||||
# inits for different input types
|
|
||||||
|
|
||||||
def init_evtable(self, table):
|
|
||||||
"""The input is an EvTable."""
|
|
||||||
if table.height:
|
|
||||||
# enforced height of each paged table, plus space for evmore extras
|
|
||||||
self.height = table.height - 4
|
|
||||||
|
|
||||||
# convert table to string
|
|
||||||
text = str(table)
|
|
||||||
self._justify = False
|
|
||||||
self._justify_kwargs = None # enforce
|
|
||||||
self.init_str(text)
|
|
||||||
|
|
||||||
def init_queryset(self, qs):
|
|
||||||
"""The input is a queryset"""
|
|
||||||
nsize = qs.count() # we assume each will be a line
|
|
||||||
self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1)
|
|
||||||
self._data = qs
|
|
||||||
self._paginator = self.paginator_slice
|
|
||||||
|
|
||||||
def init_iterable(self, inp):
|
|
||||||
"""The input is something other than a string - convert to iterable of strings"""
|
|
||||||
inp = make_iter(inp)
|
|
||||||
nsize = len(inp)
|
|
||||||
self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1)
|
|
||||||
self._data = inp
|
|
||||||
self._paginator = self.paginator_slice
|
|
||||||
|
|
||||||
def init_f_str(self, text):
|
|
||||||
"""
|
|
||||||
The input contains `\\f` markers. We use `\\f` to indicate the user wants to
|
|
||||||
enforce their line breaks on their own. If so, we do no automatic
|
|
||||||
line-breaking/justification at all.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): The string to format with f-markers.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._data = text.split("\f")
|
|
||||||
self._npages = len(self._data)
|
|
||||||
self._paginator = self.paginator_index
|
|
||||||
|
|
||||||
def init_str(self, text):
|
|
||||||
"""The input is a string"""
|
|
||||||
|
|
||||||
if self._justify:
|
|
||||||
# we must break very long lines into multiple ones. Note that this
|
|
||||||
# will also remove spurious whitespace.
|
|
||||||
justify_kwargs = self._justify_kwargs or {}
|
|
||||||
width = self._justify_kwargs.get("width", self.width)
|
|
||||||
justify_kwargs["width"] = width
|
|
||||||
justify_kwargs["align"] = self._justify_kwargs.get("align", "l")
|
|
||||||
justify_kwargs["indent"] = self._justify_kwargs.get("indent", 0)
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
for line in text.split("\n"):
|
|
||||||
if len(line) > width:
|
|
||||||
lines.extend(justify(line, **justify_kwargs).split("\n"))
|
|
||||||
else:
|
|
||||||
lines.append(line)
|
|
||||||
else:
|
|
||||||
# no justification. Simple division by line
|
|
||||||
lines = text.split("\n")
|
|
||||||
|
|
||||||
self._data = [
|
|
||||||
_LBR.join(lines[i : i + self.height]) for i in range(0, len(lines), self.height)
|
|
||||||
]
|
|
||||||
self._npages = len(self._data)
|
|
||||||
self._paginator = self.paginator_index
|
|
||||||
|
|
||||||
# display helpers and navigation
|
|
||||||
|
|
||||||
def display(self, show_footer=True):
|
def display(self, show_footer=True):
|
||||||
"""
|
"""
|
||||||
Pretty-print the page.
|
Pretty-print the page.
|
||||||
"""
|
"""
|
||||||
|
pos = 0
|
||||||
|
text = "[no content]"
|
||||||
|
if self._npages > 0:
|
||||||
pos = self._npos
|
pos = self._npos
|
||||||
text = self.format_page(self._paginator(pos))
|
text = self.page_formatter(self.paginator(pos))
|
||||||
if show_footer:
|
if show_footer:
|
||||||
page = _DISPLAY.format(text=text, pageno=pos + 1, pagemax=self._npages)
|
page = _DISPLAY.format(text=text, pageno=pos + 1, pagemax=self._npages)
|
||||||
else:
|
else:
|
||||||
|
|
@ -442,6 +340,180 @@ class EvMore(object):
|
||||||
self.page_top()
|
self.page_top()
|
||||||
|
|
||||||
|
|
||||||
|
# default paginators - responsible for extracting a specific page number
|
||||||
|
|
||||||
|
def paginator_index(self, pageno):
|
||||||
|
"""Paginate to specific, known index"""
|
||||||
|
return self._data[pageno]
|
||||||
|
|
||||||
|
def paginator_slice(self, pageno):
|
||||||
|
"""
|
||||||
|
Paginate by slice. This is done with an eye on memory efficiency (usually for
|
||||||
|
querysets); to avoid fetching all objects at the same time.
|
||||||
|
"""
|
||||||
|
return self._data[pageno * self.height: pageno * self.height + self.height]
|
||||||
|
|
||||||
|
def paginator_django(self, pageno):
|
||||||
|
"""
|
||||||
|
Paginate using the django queryset Paginator API. Note that his is indexed from 1.
|
||||||
|
"""
|
||||||
|
return self._data.page(pageno + 1)
|
||||||
|
|
||||||
|
# default helpers to set up particular input types
|
||||||
|
|
||||||
|
def init_evtable(self, table):
|
||||||
|
"""The input is an EvTable."""
|
||||||
|
if table.height:
|
||||||
|
# enforced height of each paged table, plus space for evmore extras
|
||||||
|
self.height = table.height - 4
|
||||||
|
|
||||||
|
# convert table to string
|
||||||
|
text = str(table)
|
||||||
|
self._justify = False
|
||||||
|
self._justify_kwargs = None # enforce
|
||||||
|
self.init_str(text)
|
||||||
|
|
||||||
|
def init_queryset(self, qs):
|
||||||
|
"""The input is a queryset"""
|
||||||
|
nsize = qs.count() # we assume each will be a line
|
||||||
|
self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1)
|
||||||
|
self._data = qs
|
||||||
|
|
||||||
|
def init_django_paginator(self, pages):
|
||||||
|
"""
|
||||||
|
The input is a django Paginator object.
|
||||||
|
"""
|
||||||
|
self._npages = pages.num_pages
|
||||||
|
self._data = pages
|
||||||
|
|
||||||
|
def init_iterable(self, inp):
|
||||||
|
"""The input is something other than a string - convert to iterable of strings"""
|
||||||
|
inp = make_iter(inp)
|
||||||
|
nsize = len(inp)
|
||||||
|
self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1)
|
||||||
|
self._data = inp
|
||||||
|
|
||||||
|
def init_f_str(self, text):
|
||||||
|
"""
|
||||||
|
The input contains `\\f` markers. We use `\\f` to indicate the user wants to
|
||||||
|
enforce their line breaks on their own. If so, we do no automatic
|
||||||
|
line-breaking/justification at all.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The string to format with f-markers.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._data = text.split("\f")
|
||||||
|
self._npages = len(self._data)
|
||||||
|
|
||||||
|
def init_str(self, text):
|
||||||
|
"""The input is a string"""
|
||||||
|
|
||||||
|
if self._justify:
|
||||||
|
# we must break very long lines into multiple ones. Note that this
|
||||||
|
# will also remove spurious whitespace.
|
||||||
|
justify_kwargs = self._justify_kwargs or {}
|
||||||
|
width = self._justify_kwargs.get("width", self.width)
|
||||||
|
justify_kwargs["width"] = width
|
||||||
|
justify_kwargs["align"] = self._justify_kwargs.get("align", "l")
|
||||||
|
justify_kwargs["indent"] = self._justify_kwargs.get("indent", 0)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for line in text.split("\n"):
|
||||||
|
if len(line) > width:
|
||||||
|
lines.extend(justify(line, **justify_kwargs).split("\n"))
|
||||||
|
else:
|
||||||
|
lines.append(line)
|
||||||
|
else:
|
||||||
|
# no justification. Simple division by line
|
||||||
|
lines = text.split("\n")
|
||||||
|
|
||||||
|
self._data = [
|
||||||
|
_LBR.join(lines[i : i + self.height]) for i in range(0, len(lines), self.height)
|
||||||
|
]
|
||||||
|
self._npages = len(self._data)
|
||||||
|
|
||||||
|
# Hooks for customizing input handling and formatting (override in a child class)
|
||||||
|
|
||||||
|
def init_pages(self, inp):
|
||||||
|
"""
|
||||||
|
Initialize the pagination. By default, will analyze input type to determine
|
||||||
|
how pagination automatically.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
inp (any): Incoming data to be paginated. By default, handles pagination of
|
||||||
|
strings, querysets, django.Paginator, EvTables and any iterables with strings.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
If overridden, this method must perform the following actions:
|
||||||
|
- read and re-store `self._data` (the incoming data set) if needed for pagination to work.
|
||||||
|
- set `self._npages` to the total number of pages. Default is 1.
|
||||||
|
- set `self._paginator` to a callable that will take a page number 1...N and return
|
||||||
|
the data to display on that page (not any decorations or next/prev buttons). If only
|
||||||
|
wanting to change the paginator, override `self.paginator` instead.
|
||||||
|
- set `self._page_formatter` to a callable that will receive the page from `self._paginator`
|
||||||
|
and format it with one element per line. Default is `str`. Or override `self.page_formatter`
|
||||||
|
directly instead.
|
||||||
|
|
||||||
|
By default, helper methods are called that perform these actions
|
||||||
|
depending on supported inputs.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if inherits_from(inp, "evennia.utils.evtable.EvTable"):
|
||||||
|
# an EvTable
|
||||||
|
self.init_evtable(inp)
|
||||||
|
self._paginator = self.paginator_index
|
||||||
|
elif isinstance(inp, QuerySet):
|
||||||
|
# a queryset
|
||||||
|
self.init_queryset(inp)
|
||||||
|
self._paginator = self.paginator_slice
|
||||||
|
elif isinstance(inp, Paginator):
|
||||||
|
self.init_django_paginator(inp)
|
||||||
|
self._paginator = self.paginator_django
|
||||||
|
elif not isinstance(inp, str):
|
||||||
|
# anything else not a str
|
||||||
|
self.init_iterable(inp)
|
||||||
|
self._paginator = self.paginator_slice
|
||||||
|
elif "\f" in inp:
|
||||||
|
# string with \f line-break markers in it
|
||||||
|
self.init_f_str(inp)
|
||||||
|
self._paginator = self.paginator_index
|
||||||
|
else:
|
||||||
|
# a string
|
||||||
|
self.init_str(inp)
|
||||||
|
self._paginator = self.paginator_index
|
||||||
|
|
||||||
|
def paginator(self, pageno):
|
||||||
|
"""
|
||||||
|
Paginator. The data operated upon is in `self._data`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pageno (int): The page number to view, from 0...N-1
|
||||||
|
Returns:
|
||||||
|
str: The page to display (without any decorations, those are added
|
||||||
|
by EvMore).
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._paginator(pageno)
|
||||||
|
|
||||||
|
def page_formatter(self, page):
|
||||||
|
"""
|
||||||
|
Page formatter. Every page passes through this method. Override
|
||||||
|
it to customize behvaior per-page. A common use is to generate a new
|
||||||
|
EvTable for every page (this is more efficient than to generate one huge
|
||||||
|
EvTable across many pages and feed it into EvMore all at once).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page (any): A piece of data representing one page to display. This must
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A ready-formatted page to display. Extra footer with help about
|
||||||
|
switching to the next/prev page will be added automatically
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._page_formatter(page)
|
||||||
|
|
||||||
|
|
||||||
# helper function
|
# helper function
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ class TestCreateMessage(EvenniaTest):
|
||||||
locks=locks,
|
locks=locks,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
)
|
)
|
||||||
self.assertEqual(msg.receivers, [self.char1, self.char2])
|
self.assertEqual(set(msg.receivers), set([self.char1, self.char2]))
|
||||||
self.assertTrue(all(lock in msg.locks.all() for lock in locks.split(";")))
|
self.assertTrue(all(lock in msg.locks.all() for lock in locks.split(";")))
|
||||||
self.assertEqual(msg.tags.all(), tags)
|
self.assertEqual(msg.tags.all(), tags)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2099,6 +2099,10 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
|
||||||
for num, result in enumerate(matches):
|
for num, result in enumerate(matches):
|
||||||
# we need to consider Commands, where .aliases is a list
|
# we need to consider Commands, where .aliases is a list
|
||||||
aliases = result.aliases.all() if hasattr(result.aliases, "all") else result.aliases
|
aliases = result.aliases.all() if hasattr(result.aliases, "all") else result.aliases
|
||||||
|
# remove any pluralization aliases
|
||||||
|
aliases = [alias for alias in aliases if
|
||||||
|
hasattr(alias, "category")
|
||||||
|
and alias.category not in ("plural_key", )]
|
||||||
error += _MULTIMATCH_TEMPLATE.format(
|
error += _MULTIMATCH_TEMPLATE.format(
|
||||||
number=num + 1,
|
number=num + 1,
|
||||||
name=result.get_display_name(caller)
|
name=result.get_display_name(caller)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# general
|
# general
|
||||||
attrs >= 19.2.0
|
attrs >= 19.2.0
|
||||||
django >= 2.2.5, < 2.3
|
django >= 2.2.5, < 3.0
|
||||||
twisted >= 20.3.0, < 21.0.0
|
twisted >= 20.3.0, < 21.0.0
|
||||||
pytz
|
pytz
|
||||||
djangorestframework >= 3.10.3, < 3.12
|
djangorestframework >= 3.10.3, < 3.12
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue