mirror of
https://github.com/evennia/evennia.git
synced 2026-03-29 12:07:17 +02:00
Resolve merge conflicts
This commit is contained in:
commit
a6a42001a1
25 changed files with 726 additions and 444 deletions
12
CHANGELOG.md
12
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
|
||||
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
|
||||
sliced to only return the required data per page. EvMore takes a new kwarg
|
||||
`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.
|
||||
sliced to only return the required data per page.
|
||||
- Improve performance of `find` and `objects` commands on large data sets (strikaco)
|
||||
- New `CHANNEL_HANDLER_CLASS` setting allows for replacing the ChannelHandler entirely.
|
||||
- Made `py` interactive mode support regular quit() and more verbose.
|
||||
|
|
@ -88,7 +86,13 @@ without arguments starts a full interactive Python console.
|
|||
- Make `INLINEFUNC_STACK_MAXSIZE` default visible in `settings_default.py`.
|
||||
- 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.
|
||||
|
||||
- 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)
|
||||
|
||||
|
|
|
|||
|
|
@ -1262,7 +1262,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
]
|
||||
except Exception:
|
||||
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)
|
||||
if _MUDINFO_CHANNEL:
|
||||
_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):
|
||||
"""
|
||||
Progressively call the command that was given in argument. Used
|
||||
|
|
@ -206,7 +227,15 @@ def _progressive_cmd_run(cmd, generator, response=None):
|
|||
else:
|
||||
value = generator.send(response)
|
||||
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:
|
||||
if isinstance(value, (int, float)):
|
||||
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)))
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -632,19 +640,23 @@ def cmdhandler(
|
|||
if isinstance(ret, types.GeneratorType):
|
||||
# cmd.func() is a generator, execute progressively
|
||||
_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:
|
||||
ret = yield ret
|
||||
# post-command hook
|
||||
yield cmd.at_post_cmd()
|
||||
|
||||
# post-command hook
|
||||
yield cmd.at_post_cmd()
|
||||
|
||||
if cmd.save_for_next:
|
||||
# store a reference to this command, possibly
|
||||
# accessible by the next command.
|
||||
caller.ndb.last_cmd = yield copy(cmd)
|
||||
else:
|
||||
caller.ndb.last_cmd = None
|
||||
if cmd.save_for_next:
|
||||
# store a reference to this command, possibly
|
||||
# accessible by the next command.
|
||||
caller.ndb.last_cmd = yield copy(cmd)
|
||||
else:
|
||||
caller.ndb.last_cmd = None
|
||||
|
||||
# return result to the deferred
|
||||
returnValue(ret)
|
||||
|
|
|
|||
|
|
@ -3076,9 +3076,9 @@ class CmdScript(COMMAND_DEFAULT_CLASS):
|
|||
result.append("No scripts defined on %s." % obj.get_display_name(caller))
|
||||
elif not self.switches:
|
||||
# view all scripts
|
||||
from evennia.commands.default.system import format_script_list
|
||||
|
||||
result.append(format_script_list(scripts))
|
||||
from evennia.commands.default.system import ScriptEvMore
|
||||
ScriptEvMore(self.caller, scripts.order_by("id"), session=self.session)
|
||||
return
|
||||
elif "start" in self.switches:
|
||||
num = sum([obj.scripts.start(script.key) for script in scripts])
|
||||
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/list [tag, tag, ...]
|
||||
spawn/list modules - list only module-based prototypes
|
||||
spawn/show [<prototype_key>]
|
||||
spawn/update <prototype_key>
|
||||
|
||||
|
|
@ -3476,16 +3477,11 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
elif query:
|
||||
self.caller.msg(f"No prototype named '{query}' was found.")
|
||||
else:
|
||||
self.caller.msg(f"No prototypes found.")
|
||||
self.caller.msg("No prototypes found.")
|
||||
|
||||
def _list_prototypes(self, key=None, tags=None):
|
||||
"""Display prototypes as a list, optionally limited by key/tags. """
|
||||
table = protlib.list_prototypes(self.caller, key=key, tags=tags)
|
||||
if not table:
|
||||
return True
|
||||
EvMore(
|
||||
self.caller, str(table), exit_on_lastpage=True, justify_kwargs=False,
|
||||
)
|
||||
protlib.list_prototypes(self.caller, key=key, tags=tags, session=self.session)
|
||||
|
||||
@interactive
|
||||
def _update_existing_objects(self, caller, prototype_key, quiet=False):
|
||||
|
|
|
|||
|
|
@ -37,7 +37,10 @@ __all__ = (
|
|||
"CmdCdesc",
|
||||
"CmdPage",
|
||||
"CmdIRC2Chan",
|
||||
"CmdIRCStatus",
|
||||
"CmdRSS2Chan",
|
||||
"CmdGrapevine2Chan",
|
||||
|
||||
)
|
||||
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import twisted
|
|||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.paginator import Paginator
|
||||
from evennia.server.sessionhandler import SESSIONS
|
||||
from evennia.scripts.models import ScriptDB
|
||||
from evennia.objects.models import ObjectDB
|
||||
|
|
@ -408,59 +409,71 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
|
|||
)
|
||||
|
||||
|
||||
# helper function. Kept outside so it can be imported and run
|
||||
# by other commands.
|
||||
class ScriptEvMore(EvMore):
|
||||
"""
|
||||
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 format_script_list(scripts):
|
||||
"""Takes a list of scripts and formats the output."""
|
||||
if not scripts:
|
||||
return "<No scripts>"
|
||||
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)
|
||||
|
||||
table = EvTable(
|
||||
"|wdbref|n",
|
||||
"|wobj|n",
|
||||
"|wkey|n",
|
||||
"|wintval|n",
|
||||
"|wnext|n",
|
||||
"|wrept|n",
|
||||
"|wdb",
|
||||
"|wtypeclass|n",
|
||||
"|wdesc|n",
|
||||
align="r",
|
||||
border="tablecols",
|
||||
)
|
||||
def page_formatter(self, scripts):
|
||||
"""Takes a page of scripts and formats the output
|
||||
into an EvTable."""
|
||||
|
||||
for script in scripts:
|
||||
if not scripts:
|
||||
return "<No scripts>"
|
||||
|
||||
nextrep = script.time_until_next_repeat()
|
||||
if nextrep is None:
|
||||
nextrep = "PAUSED" if script.db._paused_time else "--"
|
||||
else:
|
||||
nextrep = "%ss" % nextrep
|
||||
|
||||
maxrepeat = script.repeats
|
||||
remaining = script.remaining_repeats() or 0
|
||||
if maxrepeat:
|
||||
rept = "%i/%i" % (maxrepeat - remaining, maxrepeat)
|
||||
else:
|
||||
rept = "-/-"
|
||||
|
||||
table.add_row(
|
||||
script.id,
|
||||
f"{script.obj.key}({script.obj.dbref})"
|
||||
if (hasattr(script, "obj") and script.obj)
|
||||
else "<Global>",
|
||||
script.key,
|
||||
script.interval if script.interval > 0 else "--",
|
||||
nextrep,
|
||||
rept,
|
||||
"*" if script.persistent else "-",
|
||||
script.typeclass_path.rsplit(".", 1)[-1],
|
||||
crop(script.desc, width=20),
|
||||
table = EvTable(
|
||||
"|wdbref|n",
|
||||
"|wobj|n",
|
||||
"|wkey|n",
|
||||
"|wintval|n",
|
||||
"|wnext|n",
|
||||
"|wrept|n",
|
||||
"|wdb",
|
||||
"|wtypeclass|n",
|
||||
"|wdesc|n",
|
||||
align="r",
|
||||
border="tablecols",
|
||||
width=self.width
|
||||
)
|
||||
|
||||
return "%s" % table
|
||||
for script in scripts:
|
||||
|
||||
nextrep = script.time_until_next_repeat()
|
||||
if nextrep is None:
|
||||
nextrep = "PAUSED" if script.db._paused_time else "--"
|
||||
else:
|
||||
nextrep = "%ss" % nextrep
|
||||
|
||||
maxrepeat = script.repeats
|
||||
remaining = script.remaining_repeats() or 0
|
||||
if maxrepeat:
|
||||
rept = "%i/%i" % (maxrepeat - remaining, maxrepeat)
|
||||
else:
|
||||
rept = "-/-"
|
||||
|
||||
table.add_row(
|
||||
script.id,
|
||||
f"{script.obj.key}({script.obj.dbref})"
|
||||
if (hasattr(script, "obj") and script.obj)
|
||||
else "<Global>",
|
||||
script.key,
|
||||
script.interval if script.interval > 0 else "--",
|
||||
nextrep,
|
||||
rept,
|
||||
"*" if script.persistent else "-",
|
||||
script.typeclass_path.rsplit(".", 1)[-1],
|
||||
crop(script.desc, width=20),
|
||||
)
|
||||
|
||||
return str(table)
|
||||
|
||||
|
||||
class CmdScripts(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -549,7 +562,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
caller.msg(string)
|
||||
else:
|
||||
# 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")
|
||||
elif self.switches and self.switches[0] in ("validate", "valid", "val"):
|
||||
# run validation on all found scripts
|
||||
|
|
@ -559,7 +572,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
caller.msg(string)
|
||||
else:
|
||||
# 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):
|
||||
|
|
|
|||
|
|
@ -1159,7 +1159,7 @@ class TestBuilding(CommandTest):
|
|||
"= Obj",
|
||||
"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(
|
||||
building.CmdScript(), "/start Obj", "0 scripts started on Obj"
|
||||
|
|
@ -1252,10 +1252,11 @@ class TestBuilding(CommandTest):
|
|||
)
|
||||
|
||||
def test_spawn(self):
|
||||
def getObject(commandTest, objKeyStr):
|
||||
|
||||
def get_object(commandTest, obj_key):
|
||||
# A helper function to get a spawned object and
|
||||
# check that it exists in the process.
|
||||
query = search_object(objKeyStr)
|
||||
query = search_object(obj_key)
|
||||
commandTest.assertIsNotNone(query)
|
||||
commandTest.assertTrue(bool(query))
|
||||
obj = query[0]
|
||||
|
|
@ -1284,7 +1285,7 @@ class TestBuilding(CommandTest):
|
|||
)
|
||||
|
||||
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(
|
||||
building.CmdSpawn(),
|
||||
|
|
@ -1294,11 +1295,11 @@ class TestBuilding(CommandTest):
|
|||
)
|
||||
|
||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||
|
||||
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.
|
||||
testchar = getObject(self, "Test Char")
|
||||
testchar = get_object(self, "Test Char")
|
||||
self.assertEqual(testchar.location, self.char1.location)
|
||||
testchar.delete()
|
||||
|
||||
|
|
@ -1315,7 +1316,7 @@ class TestBuilding(CommandTest):
|
|||
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref,
|
||||
"Spawned goblin",
|
||||
)
|
||||
goblin = getObject(self, "goblin")
|
||||
goblin = get_object(self, "goblin")
|
||||
# Tests that the spawned object's type is a DefaultCharacter.
|
||||
self.assertIsInstance(goblin, DefaultCharacter)
|
||||
self.assertEqual(goblin.location, spawnLoc)
|
||||
|
|
@ -1334,7 +1335,7 @@ class TestBuilding(CommandTest):
|
|||
# Tests "spawn <prototype_name>"
|
||||
self.call(building.CmdSpawn(), "testball", "Spawned Ball")
|
||||
|
||||
ball = getObject(self, "Ball")
|
||||
ball = get_object(self, "Ball")
|
||||
self.assertEqual(ball.location, self.char1.location)
|
||||
self.assertIsInstance(ball, DefaultObject)
|
||||
ball.delete()
|
||||
|
|
@ -1344,7 +1345,7 @@ class TestBuilding(CommandTest):
|
|||
self.call(
|
||||
building.CmdSpawn(), "/n 'BALL'", "Spawned Ball"
|
||||
) # /n switch is abbreviated form of /noloc
|
||||
ball = getObject(self, "Ball")
|
||||
ball = get_object(self, "Ball")
|
||||
self.assertIsNone(ball.location)
|
||||
ball.delete()
|
||||
|
||||
|
|
@ -1363,7 +1364,7 @@ class TestBuilding(CommandTest):
|
|||
% spawnLoc.dbref,
|
||||
"Spawned Ball",
|
||||
)
|
||||
ball = getObject(self, "Ball")
|
||||
ball = get_object(self, "Ball")
|
||||
self.assertEqual(ball.location, spawnLoc)
|
||||
ball.delete()
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ class GenderCharacter(DefaultCharacter):
|
|||
pronoun = _GENDER_PRONOUN_MAP[gender][typ.lower()]
|
||||
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.
|
||||
Overloads the default msg() implementation to include
|
||||
|
|
@ -141,6 +141,10 @@ class GenderCharacter(DefaultCharacter):
|
|||
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:
|
||||
if text and isinstance(text, tuple):
|
||||
text = (_RE_GENDER_PRONOUN.sub(self._get_pronoun, text[0]), *text[1:])
|
||||
|
|
|
|||
|
|
@ -872,20 +872,17 @@ class TestCustomGameTime(EvenniaTest):
|
|||
|
||||
# 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):
|
||||
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, conditional=("<", 35)), True)
|
||||
self.assertEqual(dice.roll_dice(6, 6, conditional=(">", 33)), False)
|
||||
|
||||
def test_cmddice(self, mocked_randint):
|
||||
from evennia.contrib import dice
|
||||
|
||||
self.call(
|
||||
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
|
||||
|
||||
|
||||
from evennia.contrib import email_login
|
||||
from evennia.contrib import email_login # noqa
|
||||
|
||||
|
||||
class TestEmailLogin(CommandTest):
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
#
|
||||
|
||||
|
||||
# HEADER
|
||||
#HEADER
|
||||
|
||||
# everything in this block will be appended to the beginning of
|
||||
# all other #CODE blocks when they are executed.
|
||||
|
|
@ -51,7 +51,7 @@ from evennia import DefaultObject
|
|||
limbo = search_object("Limbo")[0]
|
||||
|
||||
|
||||
# CODE
|
||||
#CODE
|
||||
|
||||
# This is the first code block. Within each block, Python
|
||||
# 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
|
||||
caller.msg("A %s was created." % red_button.key)
|
||||
|
||||
# CODE
|
||||
#CODE
|
||||
|
||||
# this code block has 'table' and 'chair' set as deletable
|
||||
# objects. This means that when the batchcode processor runs in
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ needed on the Evennia side.
|
|||
|
||||
MSSPTable = {
|
||||
# Required fields
|
||||
"NAME": "Evennia",
|
||||
"NAME": "Mygame", # usually the same as SERVERNAME
|
||||
# Generic
|
||||
"CRAWL DELAY": "-1", # limit how often crawler updates the listing. -1 for no limit
|
||||
"HOSTNAME": "", # current or new hostname
|
||||
"PORT": ["4000"], # most important port should be *last* in list!
|
||||
"CRAWL DELAY": "-1", # limit how often crawler may update the listing. -1 for no limit
|
||||
"HOSTNAME": "", # telnet hostname
|
||||
"PORT": ["4000"], # telnet port - most important port should be *last* in list!
|
||||
"CODEBASE": "Evennia",
|
||||
"CONTACT": "", # email for contacting the mud
|
||||
"CREATED": "", # year MUD was created
|
||||
|
|
@ -33,7 +33,7 @@ MSSPTable = {
|
|||
"LANGUAGE": "", # name of language used, e.g. English
|
||||
"LOCATION": "", # full English name of server country
|
||||
"MINIMUM AGE": "0", # set to 0 if not applicable
|
||||
"WEBSITE": "www.evennia.com",
|
||||
"WEBSITE": "", # http:// address to your game website
|
||||
# Categorisation
|
||||
"FAMILY": "Custom", # evennia goes under 'Custom'
|
||||
"GENRE": "None", # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction
|
||||
|
|
@ -41,10 +41,10 @@ MSSPTable = {
|
|||
# Player versus Player, Player versus Environment,
|
||||
# Roleplaying, Simulation, Social or Strategy
|
||||
"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
|
||||
# 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",
|
||||
# World
|
||||
"AREAS": "0",
|
||||
|
|
@ -56,7 +56,7 @@ MSSPTable = {
|
|||
"LEVELS": "0", # use 0 if level-less
|
||||
"RACES": "0", # use 0 if race-less
|
||||
"SKILLS": "0", # use 0 if skill-less
|
||||
# Protocols set to 1 or 0)
|
||||
# Protocols set to 1 or 0; should usually not be changed)
|
||||
"ANSI": "1",
|
||||
"GMCP": "1",
|
||||
"MSDP": "1",
|
||||
|
|
|
|||
|
|
@ -2,40 +2,56 @@
|
|||
Prototypes
|
||||
|
||||
A prototype is a simple way to create individualized instances of a
|
||||
given `Typeclass`. For example, you might have a Sword typeclass that
|
||||
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.
|
||||
given typeclass. It is dictionary with specific key names.
|
||||
|
||||
Example prototypes are read by the `@spawn` command but is also easily
|
||||
available to use from code via `evennia.spawn` or `evennia.utils.spawner`.
|
||||
Each prototype should be a dictionary. Use the same name as the
|
||||
variable to refer to other prototypes.
|
||||
For example, you might have a Sword typeclass that 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.
|
||||
|
||||
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:
|
||||
prototype_parent - string pointing to parent prototype of this structure.
|
||||
key - string, the main object identifier.
|
||||
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).
|
||||
- `prototype_key` - the name of the prototype. This is required for db-prototypes,
|
||||
for module-prototypes, the global variable name of the dict is used instead
|
||||
- `prototype_parent` - string pointing to parent prototype if any. Prototype inherits
|
||||
in a similar way as classes, with children overriding values in their partents.
|
||||
- `key` - string, the main object identifier.
|
||||
- `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.
|
||||
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.
|
||||
See the `spawn` command and `evennia.prototypes.spawner.spawn` for more info.
|
||||
|
||||
"""
|
||||
|
||||
## example of module-based prototypes using
|
||||
## the variable name as `prototype_key` and
|
||||
## simple Attributes
|
||||
|
||||
# from random import randint
|
||||
#
|
||||
# GOBLIN = {
|
||||
|
|
@ -43,7 +59,8 @@ See the `@spawn` command and `evennia.utils.spawner` for more info.
|
|||
# "health": lambda: randint(20,30),
|
||||
# "resists": ["cold", "poison"],
|
||||
# "attacks": ["fists"],
|
||||
# "weaknesses": ["fire", "light"]
|
||||
# "weaknesses": ["fire", "light"],
|
||||
# "tags": = [("greenskin", "monster"), ("humanoid", "monster")]
|
||||
# }
|
||||
#
|
||||
# GOBLIN_WIZARD = {
|
||||
|
|
|
|||
|
|
@ -343,22 +343,23 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
singular (str): The singular form to display.
|
||||
plural (str): The determined plural form of the key, including the count.
|
||||
"""
|
||||
plural_category = "plural_key"
|
||||
key = kwargs.get("key", self.key)
|
||||
key = ansi.ANSIString(key) # this is needed to allow inflection of colored names
|
||||
try:
|
||||
plural = _INFLECT.plural(key, 2)
|
||||
plural = "%s %s" % (_INFLECT.number_to_words(count, threshold=12), plural)
|
||||
plural = _INFLECT.plural(key, count)
|
||||
plural = "{} {}".format(_INFLECT.number_to_words(count, threshold=12), plural)
|
||||
except IndexError:
|
||||
# this is raised by inflect if the input is not a proper noun
|
||||
plural = 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
|
||||
self.aliases.clear(category="plural_key")
|
||||
self.aliases.add(plural, category="plural_key")
|
||||
self.aliases.clear(category=plural_category)
|
||||
self.aliases.add(plural, category=plural_category)
|
||||
# save the singular form as an alias here too so we can display "an egg" and also
|
||||
# look at 'an egg'.
|
||||
self.aliases.add(singular, category="plural_key")
|
||||
self.aliases.add(singular, category=plural_category)
|
||||
return singular, plural
|
||||
|
||||
def search(
|
||||
|
|
|
|||
|
|
@ -9,9 +9,13 @@ import hashlib
|
|||
import time
|
||||
from ast import literal_eval
|
||||
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.objects.models import ObjectDB
|
||||
from evennia.typeclasses.attributes import Attribute
|
||||
from evennia.utils.create import create_script
|
||||
from evennia.utils.evmore import EvMore
|
||||
from evennia.utils.utils import (
|
||||
all_from_module,
|
||||
make_iter,
|
||||
|
|
@ -163,7 +167,8 @@ for mod in settings.PROTOTYPE_MODULES:
|
|||
if "prototype_locks" in prot
|
||||
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
|
||||
|
|
@ -320,7 +325,7 @@ def delete_prototype(prototype_key, caller=None):
|
|||
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.
|
||||
|
||||
|
|
@ -331,11 +336,17 @@ def search_prototype(key=None, tags=None, require_single=False):
|
|||
tag category.
|
||||
require_single (bool): If set, raise KeyError if the result
|
||||
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:
|
||||
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`
|
||||
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:
|
||||
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)
|
||||
tags = make_iter(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:
|
||||
db_matches = DbPrototype.objects.all().order_by("id")
|
||||
db_matches = DbPrototype.objects.all()
|
||||
|
||||
if key:
|
||||
# exact or partial match on key
|
||||
db_matches = (
|
||||
db_matches.filter(db_key=key) or db_matches.filter(db_key__icontains=key)
|
||||
).order_by("id")
|
||||
# return prototype
|
||||
db_prototypes = [dbprot.prototype for dbprot in db_matches]
|
||||
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
|
||||
.filter(
|
||||
Q(db_key__icontains=key))
|
||||
.order_by("db_key")
|
||||
)
|
||||
else:
|
||||
db_matches = exact_match
|
||||
|
||||
matches = db_prototypes + module_prototypes
|
||||
nmatches = len(matches)
|
||||
if nmatches > 1 and key:
|
||||
key = key.lower()
|
||||
# avoid duplicates if an exact match exist between the two types
|
||||
filter_matches = [
|
||||
mta for mta in matches if mta.get("prototype_key") and mta["prototype_key"] == key
|
||||
]
|
||||
if filter_matches and len(filter_matches) < nmatches:
|
||||
matches = filter_matches
|
||||
# convert to prototype
|
||||
db_ids = db_matches.values_list("id", flat=True)
|
||||
db_matches = (
|
||||
Attribute.objects
|
||||
.filter(scriptdb__pk__in=db_ids, db_key="prototype")
|
||||
.values_list("db_value", flat=True)
|
||||
.order_by("scriptdb__db_key")
|
||||
)
|
||||
if key and require_single:
|
||||
nmodules = len(module_prototypes)
|
||||
ndbprots = db_matches.count()
|
||||
if nmodules + ndbprots != 1:
|
||||
raise KeyError(f"Found {nmodules + ndbprots} matching prototypes.")
|
||||
|
||||
nmatches = len(matches)
|
||||
if nmatches != 1 and require_single:
|
||||
raise KeyError("Found {} matching prototypes.".format(nmatches))
|
||||
|
||||
return matches
|
||||
if return_iterators:
|
||||
# trying to get the entire set of prototypes - we must paginate
|
||||
# the result instead of trying to fetch the entire set at once
|
||||
return db_matches, module_prototypes
|
||||
else:
|
||||
# full fetch, no pagination (compatibility mode)
|
||||
return list(db_matches) + module_prototypes
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -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.
|
||||
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.
|
||||
session (Session, optional): If given, this is used for display formatting.
|
||||
Returns:
|
||||
table (EvTable or None): An EvTable representation of the prototypes. None
|
||||
if no prototypes were found.
|
||||
PrototypeEvMore: An EvMore subclass optimized for prototype listings.
|
||||
None: If no matches were found. In this case the caller has already been notified.
|
||||
|
||||
"""
|
||||
# this allows us to pass lists of empty strings
|
||||
tags = [tag for tag in make_iter(tags) if tag]
|
||||
|
||||
# get prototypes for readonly and db-based prototypes
|
||||
prototypes = search_prototype(key, tags)
|
||||
dbprot_query, modprot_list = search_prototype(key, tags, return_iterators=True)
|
||||
|
||||
# get use-permissions of readonly attributes (edit is always False)
|
||||
display_tuples = []
|
||||
for prototype in sorted(prototypes, key=lambda d: d.get("prototype_key", "")):
|
||||
lock_use = caller.locks.check_lockstring(
|
||||
caller, prototype.get("prototype_locks", ""), access_type="spawn", 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
|
||||
if not dbprot_query and not modprot_list:
|
||||
caller.msg("No prototypes found.", session=session)
|
||||
return None
|
||||
|
||||
# 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(
|
||||
prototype, protkey=None, protparents=None, is_prototype_base=True, strict=True, _flags=None
|
||||
|
|
@ -569,7 +660,7 @@ def validate_prototype(
|
|||
protparent = protparents.get(protstring)
|
||||
if not protparent:
|
||||
_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"]:
|
||||
_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 uuid
|
||||
from time import time
|
||||
from anything import Something
|
||||
from django.test.utils import override_settings
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
|
|
@ -628,8 +630,10 @@ class TestPrototypeStorage(EvenniaTest):
|
|||
|
||||
# partial match
|
||||
with mock.patch("evennia.prototypes.prototypes._MODULE_PROTOTYPES", {}):
|
||||
self.assertEqual(list(protlib.search_prototype("prot")), [prot1b, prot2, prot3])
|
||||
self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3])
|
||||
self.assertCountEqual(
|
||||
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))))
|
||||
|
||||
|
|
@ -1073,3 +1077,29 @@ class TestOLCMenu(TestEvMenu):
|
|||
["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:
|
||||
# The script is already running, but make sure we have a _task if
|
||||
# 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)
|
||||
try:
|
||||
start_delay, callcount = SCRIPT_FLUSH_TIMERS[self.id]
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class ConnectionWizard(object):
|
|||
resp = str(default)
|
||||
|
||||
if resp.lower() in options:
|
||||
self.display(f" Selected '{resp}'.")
|
||||
# self.display(f" Selected '{resp}'.")
|
||||
desc, callback, kwargs = options[resp.lower()]
|
||||
callback(self, **kwargs)
|
||||
elif resp.lower() in ("quit", "q"):
|
||||
|
|
@ -161,8 +161,10 @@ class ConnectionWizard(object):
|
|||
|
||||
def node_start(wizard):
|
||||
text = """
|
||||
This wizard helps activate external networks with Evennia. It will create
|
||||
a config that will be attached to the bottom of the game settings file.
|
||||
This wizard helps to attach your Evennia server to external networks. It
|
||||
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!
|
||||
|
||||
|
|
@ -174,11 +176,18 @@ def node_start(wizard):
|
|||
node_game_index_start,
|
||||
{},
|
||||
),
|
||||
# "2": ("Add MSSP information (for mud-list crawlers)",
|
||||
# node_mssp_start, {}),
|
||||
"2": ("MSSP setup (for mud-list crawlers)",
|
||||
node_mssp_start, {}
|
||||
),
|
||||
# "3": ("Add Grapevine listing",
|
||||
# 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)
|
||||
|
|
@ -189,13 +198,13 @@ def node_start(wizard):
|
|||
|
||||
|
||||
def node_game_index_start(wizard, **kwargs):
|
||||
text = f"""
|
||||
text = """
|
||||
The Evennia game index (http://games.evennia.com) lists both active Evennia
|
||||
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
|
||||
players. If so, put 'None' for the connection details. Just tell us you
|
||||
are out there and make us excited about your upcoming game!
|
||||
players. If so, put 'None' for the connection details - you are just telling
|
||||
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
|
||||
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
|
||||
- alpha: a working concept, probably lots of bugs and incomplete 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}
|
||||
"""
|
||||
|
||||
|
|
@ -233,6 +242,31 @@ def node_game_index_fields(wizard, status=None):
|
|||
wizard.display(text)
|
||||
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
|
||||
|
||||
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):
|
||||
tmax = 255
|
||||
tlen = len(inp)
|
||||
if tlen > 255:
|
||||
if tlen > tmax:
|
||||
print(f"The short desc must be shorter than {tmax} characters (was {tlen}).")
|
||||
wizard.ask_continue()
|
||||
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
|
||||
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:
|
||||
{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
|
||||
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:
|
||||
{webclient_default}
|
||||
|
|
@ -388,24 +422,26 @@ def node_game_index_fields(wizard, status=None):
|
|||
|
||||
def node_mssp_start(wizard):
|
||||
|
||||
mssp_module = mod_import(settings.MSSP_META_MODULE)
|
||||
filename = mssp_module.__file__
|
||||
mssp_module = mod_import(settings.MSSP_META_MODULE or "server.conf.mssp")
|
||||
try:
|
||||
filename = mssp_module.__file__
|
||||
except AttributeError:
|
||||
filename = "server/conf/mssp.py"
|
||||
|
||||
text = f"""
|
||||
MSSP (Mud Server Status Protocol) allows 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 many fields are manually added info about your game.
|
||||
MSSP (Mud Server Status Protocol) has a vast amount of options so it must
|
||||
be modified outside this wizard by directly editing its config file here:
|
||||
|
||||
'{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
|
||||
players can connect to. You also need to register at a MUD listing site to
|
||||
tell them to list 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}'
|
||||
tell them to crawl your game.
|
||||
"""
|
||||
|
||||
wizard.display(text)
|
||||
|
|
@ -456,25 +492,31 @@ def node_view_and_apply_settings(wizard):
|
|||
pp = pprint.PrettyPrinter(indent=4)
|
||||
saves = False
|
||||
|
||||
game_index_txt = "No changes to save for Game Index."
|
||||
if hasattr(wizard, "game_index_listing"):
|
||||
if wizard.game_index_listing != settings.GAME_INDEX_LISTING:
|
||||
game_index_txt = "No changes to save for Game Index."
|
||||
else:
|
||||
game_index_txt = "GAME_INDEX_ENABLED = True\n" "GAME_INDEX_LISTING = \\\n" + pp.pformat(
|
||||
wizard.game_index_listing
|
||||
)
|
||||
saves = True
|
||||
# game index
|
||||
game_index_save_text = ""
|
||||
game_index_listing = (wizard.game_index_listing if
|
||||
hasattr(wizard, "game_index_listing") else None)
|
||||
if not game_index_listing and settings.GAME_INDEX_ENABLED:
|
||||
game_index_listing = settings.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
|
||||
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}")
|
||||
|
||||
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
|
||||
_save_changes(wizard)
|
||||
wizard.display("... saved!")
|
||||
wizard.display("... saved!\nThe changes will apply after you reload your server.")
|
||||
else:
|
||||
wizard.display("... cancelled.")
|
||||
wizard.ask_continue()
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ SRESET = chr(19) # shutdown server in reset mode
|
|||
# requirements
|
||||
PYTHON_MIN = "3.7"
|
||||
TWISTED_MIN = "18.0.0"
|
||||
DJANGO_MIN = "2.1"
|
||||
DJANGO_REC = "2.2"
|
||||
DJANGO_MIN = "2.2.5"
|
||||
DJANGO_LT = "3.0"
|
||||
|
||||
try:
|
||||
sys.path[1] = EVENNIA_ROOT
|
||||
|
|
@ -374,8 +374,8 @@ ERROR_NOTWISTED = """
|
|||
"""
|
||||
|
||||
ERROR_DJANGO_MIN = """
|
||||
ERROR: Django {dversion} found. Evennia requires version {django_min}
|
||||
or higher.
|
||||
ERROR: Django {dversion} found. Evennia requires at least version {django_min} (but
|
||||
no higher than {django_lt}).
|
||||
|
||||
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
|
||||
|
|
@ -386,14 +386,9 @@ ERROR_DJANGO_MIN = """
|
|||
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 {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.
|
||||
"""
|
||||
|
||||
|
|
@ -1283,12 +1278,11 @@ def check_main_evennia_dependencies():
|
|||
# only the main version (1.5, not 1.5.4.0)
|
||||
dversion_main = ".".join(dversion.split(".")[:2])
|
||||
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
|
||||
elif LooseVersion(DJANGO_MIN) <= LooseVersion(dversion) < LooseVersion(DJANGO_REC):
|
||||
print(NOTE_DJANGO_MIN.format(dversion=dversion_main, django_rec=DJANGO_REC))
|
||||
elif LooseVersion(DJANGO_REC) < LooseVersion(dversion_main):
|
||||
print(NOTE_DJANGO_NEW.format(dversion=dversion_main, django_rec=DJANGO_REC))
|
||||
elif LooseVersion(DJANGO_LT) <= LooseVersion(dversion_main):
|
||||
print(NOTE_DJANGO_NEW.format(dversion=dversion_main, django_rec=DJANGO_LT))
|
||||
except ImportError:
|
||||
print(ERROR_NODJANGO)
|
||||
error = True
|
||||
|
|
@ -1368,10 +1362,10 @@ def create_settings_file(init=True, secret_settings=False):
|
|||
if not init:
|
||||
# if not --init mode, settings file may already exist from before
|
||||
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":
|
||||
print("Aborted.")
|
||||
return
|
||||
sys.exit()
|
||||
else:
|
||||
print("Reset the settings file.")
|
||||
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
|
|||
|
||||
"""
|
||||
# start the Server
|
||||
print("Portal starting server ... {}".format(server_twistd_cmd))
|
||||
print("Portal starting server ... ")
|
||||
process = None
|
||||
with open(settings.SERVER_LOG_FILE, "a") as logfile:
|
||||
# we link stdout to a file in order to catch
|
||||
|
|
|
|||
|
|
@ -740,9 +740,9 @@ CHANNEL_CONNECTINFO = None
|
|||
GAME_INDEX_ENABLED = False
|
||||
# This dict
|
||||
GAME_INDEX_LISTING = {
|
||||
"game_name": SERVERNAME,
|
||||
"game_name": "Mygame", # usually SERVERNAME
|
||||
"game_status": "pre-alpha", # pre-alpha, alpha, beta or launched
|
||||
"short_description": GAME_SLOGAN,
|
||||
"short_description": "", # could be GAME_SLOGAN
|
||||
"long_description": "",
|
||||
"listing_contact": "", # email
|
||||
"telnet_hostname": "", # mygame.com
|
||||
|
|
|
|||
|
|
@ -778,6 +778,7 @@ class InMemoryAttributeBackend(IAttributeBackend):
|
|||
See parent class.
|
||||
|
||||
strvalue has no meaning for InMemory attributes.
|
||||
|
||||
"""
|
||||
new_attr = self._attrclass(
|
||||
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.db.models.query import QuerySet
|
||||
from django.core.paginator import Paginator
|
||||
from evennia import Command, CmdSet
|
||||
from evennia.commands import cmdhandler
|
||||
from evennia.utils.ansi import ANSIString
|
||||
|
|
@ -140,7 +141,7 @@ class EvMore(object):
|
|||
def __init__(
|
||||
self,
|
||||
caller,
|
||||
text,
|
||||
inp,
|
||||
always_page=False,
|
||||
session=None,
|
||||
justify=False,
|
||||
|
|
@ -152,29 +153,28 @@ class EvMore(object):
|
|||
):
|
||||
|
||||
"""
|
||||
EvMore pager
|
||||
Initialization of the EvMore pager
|
||||
|
||||
Args:
|
||||
caller (Object or Account): Entity reading the text.
|
||||
text (str, EvTable or iterator): The text or data to put under paging.
|
||||
|
||||
- If a string, paginate normally. If this text contains one or more `\\\\f` format
|
||||
symbols, automatic pagination and justification are force-disabled and page-breaks
|
||||
will only happen after each `\\\\f`.
|
||||
inp (str, EvTable, Paginator or iterator): The text or data to put under paging.
|
||||
- If a string, paginage normally. If this text contains
|
||||
one or more `\f` format symbol, automatic pagination and justification
|
||||
are force-disabled and page-breaks will only happen after each `\f`.
|
||||
- If `EvTable`, the EvTable will be paginated with the same
|
||||
setting on each page if it is too long. The table
|
||||
decorations will be considered in the size of the page.
|
||||
- Otherwise `text` is converted to an iterator, where each step is
|
||||
expected to be a line in the final display. Each line
|
||||
will be run through `iter_callable`.
|
||||
setting on each page if it is too long. The table
|
||||
decorations will be considered in the size of the page.
|
||||
- Otherwise `inp` is converted to an iterator, where each step is
|
||||
expected to be a line in the final display. Each line
|
||||
will be run through `iter_callable`.
|
||||
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.
|
||||
session (Session, optional): If given, this session will be used
|
||||
to determine the screen width and will receive all output.
|
||||
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
|
||||
if `text` is an EvTable.
|
||||
if `inp` is an EvTable.
|
||||
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.
|
||||
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
|
||||
cmdset the user had *before* the evmore pager was activated (so none of
|
||||
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.
|
||||
|
||||
Examples:
|
||||
|
|
@ -198,16 +192,21 @@ class EvMore(object):
|
|||
```python
|
||||
super_long_text = " ... "
|
||||
EvMore(caller, super_long_text)
|
||||
|
||||
```
|
||||
Paginator
|
||||
```python
|
||||
from django.core.paginator import Paginator
|
||||
query = ObjectDB.objects.all()
|
||||
pages = Paginator(query, 10) # 10 objs per page
|
||||
EvMore(caller, pages) # will repr() each object per line, 10 to a page
|
||||
|
||||
multi_page_table = [ [[..],[..]], ...]
|
||||
EvMore(caller, multi_page_table, use_evtable=True,
|
||||
evtable_args=("Header1", "Header2"),
|
||||
evtable_kwargs={"align": "r", "border": "tablecols"})
|
||||
EvMore(caller, pages)
|
||||
```
|
||||
Every page an EvTable
|
||||
```python
|
||||
from evennia import EvTable
|
||||
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_cmd = exit_cmd
|
||||
self._exit_msg = "Exited |wmore|n pager."
|
||||
self._page_formatter = page_formatter
|
||||
self._kwargs = kwargs
|
||||
|
||||
self._data = None
|
||||
self._paginator = None
|
||||
|
||||
self._pages = []
|
||||
self._npages = 1
|
||||
self._npos = 0
|
||||
|
||||
self._npages = 1
|
||||
self._paginator = self.paginator_index
|
||||
self._page_formatter = str
|
||||
|
||||
# set up individual pages for different sessions
|
||||
height = max(4, session.protocol_flags.get("SCREENHEIGHT", {0: _SCREEN_HEIGHT})[0] - 4)
|
||||
self.width = session.protocol_flags.get("SCREENWIDTH", {0: _SCREEN_WIDTH})[0]
|
||||
# always limit number of chars to 10 000 per page
|
||||
self.height = min(10000 // max(1, self.width), height)
|
||||
|
||||
if inherits_from(text, "evennia.utils.evtable.EvTable"):
|
||||
# an EvTable
|
||||
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)
|
||||
# does initial parsing of input
|
||||
self.init_pages(inp)
|
||||
|
||||
# kick things into gear
|
||||
self.start()
|
||||
|
||||
# page formatter
|
||||
|
||||
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
|
||||
# EvMore functional methods
|
||||
|
||||
def display(self, show_footer=True):
|
||||
"""
|
||||
Pretty-print the page.
|
||||
"""
|
||||
pos = self._npos
|
||||
text = self.format_page(self._paginator(pos))
|
||||
pos = 0
|
||||
text = "[no content]"
|
||||
if self._npages > 0:
|
||||
pos = self._npos
|
||||
text = self.page_formatter(self.paginator(pos))
|
||||
if show_footer:
|
||||
page = _DISPLAY.format(text=text, pageno=pos + 1, pagemax=self._npages)
|
||||
else:
|
||||
|
|
@ -442,6 +340,180 @@ class EvMore(object):
|
|||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ class TestCreateMessage(EvenniaTest):
|
|||
locks=locks,
|
||||
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.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):
|
||||
# we need to consider Commands, where .aliases is a list
|
||||
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(
|
||||
number=num + 1,
|
||||
name=result.get_display_name(caller)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# general
|
||||
attrs >= 19.2.0
|
||||
django >= 2.2.5, < 2.3
|
||||
django >= 2.2.5, < 3.0
|
||||
twisted >= 20.3.0, < 21.0.0
|
||||
pytz
|
||||
djangorestframework >= 3.10.3, < 3.12
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue