Merge branch 'develop' into accounts

This commit is contained in:
Johnny 2018-10-22 21:12:58 +00:00
commit 030a83bf9c
26 changed files with 338 additions and 115 deletions

View file

@ -1,7 +1,27 @@
# Changelog # Changelog
## Evennia 0.9 (2018-2019)
### Commands
- Removed default `@delaccount` command, incorporating as `@account/delete` instead. Added confirmation
question.
- Add new `@force` command to have another object perform a command.
- Add the Portal uptime to the `@time` command.
- Make the `@link` command first make a local search before a global search.
### Utils
- Added more unit tests.
## Evennia 0.8 (2018) ## Evennia 0.8 (2018)
### Requirements
- Up requirements to Django 1.11.x, Twisted 18 and pillow 5.2.0
- Add `inflect` dependency for automatic pluralization of object names.
### Server/Portal ### Server/Portal
- Removed `evennia_runner`, completely refactor `evennia_launcher.py` (the 'evennia' program) - Removed `evennia_runner`, completely refactor `evennia_launcher.py` (the 'evennia' program)
@ -85,7 +105,6 @@
### General ### General
- Up requirements to Django 1.11.x, Twisted 18 and pillow 5.2.0
- Start structuring the `CHANGELOG` to list features in more detail. - Start structuring the `CHANGELOG` to list features in more detail.
- Docker image `evennia/evennia:develop` is now auto-built, tracking the develop branch. - Docker image `evennia/evennia:develop` is now auto-built, tracking the develop branch.
- Inflection and grouping of multiple objects in default room (an box, three boxes) - Inflection and grouping of multiple objects in default room (an box, three boxes)

View file

@ -1318,7 +1318,10 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
if target and not is_iter(target): if target and not is_iter(target):
# single target - just show it # single target - just show it
return target.return_appearance(self) if hasattr(target, "return_appearance"):
return target.return_appearance(self)
else:
return "{} has no in-game appearance.".format(target)
else: else:
# list of targets - make list to disconnect from db # list of targets - make list to disconnect from db
characters = list(tar for tar in target if tar) if target else [] characters = list(tar for tar in target if tar) if target else []

View file

@ -10,6 +10,7 @@ from evennia.accounts.accounts import DefaultAccount, DefaultGuest
from evennia.server.session import Session from evennia.server.session import Session
from evennia.utils.test_resources import EvenniaTest from evennia.utils.test_resources import EvenniaTest
from evennia.utils import create from evennia.utils import create
from evennia.utils.test_resources import EvenniaTest
from django.conf import settings from django.conf import settings
@ -268,3 +269,20 @@ class TestDefaultAccount(EvenniaTest):
account.puppet_object(self.s1, obj) account.puppet_object(self.s1, obj)
self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("is already puppeted by another Account.")) self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("is already puppeted by another Account."))
self.assertIsNone(obj.at_post_puppet.call_args) self.assertIsNone(obj.at_post_puppet.call_args)
class TestAccountPuppetDeletion(EvenniaTest):
@override_settings(MULTISESSION_MODE=2)
def test_puppet_deletion(self):
# Check for existing chars
self.assertFalse(self.account.db._playable_characters, 'Account should not have any chars by default.')
# Add char1 to account's playable characters
self.account.db._playable_characters.append(self.char1)
self.assertTrue(self.account.db._playable_characters, 'Char was not added to account.')
# See what happens when we delete char1.
self.char1.delete()
# Playable char list should be empty.
self.assertFalse(self.account.db._playable_characters, 'Playable character list is not empty! %s' % self.account.db._playable_characters)

View file

@ -16,8 +16,8 @@ COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
# limit members for API inclusion # limit members for API inclusion
__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdDelAccount", __all__ = ("CmdBoot", "CmdBan", "CmdUnban",
"CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall") "CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall", "CmdForce")
class CmdBoot(COMMAND_DEFAULT_CLASS): class CmdBoot(COMMAND_DEFAULT_CLASS):
@ -133,7 +133,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
reason to be able to later remember why the ban was put in place. reason to be able to later remember why the ban was put in place.
It is often preferable to ban an account from the server than to It is often preferable to ban an account from the server than to
delete an account with @delaccount. If banned by name, that account delete an account with @accounts/delete. If banned by name, that account
account can no longer be logged into. account can no longer be logged into.
IP (Internet Protocol) address banning allows blocking all access IP (Internet Protocol) address banning allows blocking all access
@ -256,78 +256,6 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
logger.log_sec('Unbanned: %s (Caller: %s, IP: %s).' % (value.strip(), self.caller, self.session.address)) logger.log_sec('Unbanned: %s (Caller: %s, IP: %s).' % (value.strip(), self.caller, self.session.address))
class CmdDelAccount(COMMAND_DEFAULT_CLASS):
"""
delete an account from the server
Usage:
@delaccount[/switch] <name> [: reason]
Switch:
delobj - also delete the account's currently
assigned in-game object.
Completely deletes a user from the server database,
making their nick and e-mail again available.
"""
key = "@delaccount"
switch_options = ("delobj",)
locks = "cmd:perm(delaccount) or perm(Developer)"
help_category = "Admin"
def func(self):
"""Implements the command."""
caller = self.caller
args = self.args
if hasattr(caller, 'account'):
caller = caller.account
if not args:
self.msg("Usage: @delaccount <account/user name or #id> [: reason]")
return
reason = ""
if ':' in args:
args, reason = [arg.strip() for arg in args.split(':', 1)]
# We use account_search since we want to be sure to find also accounts
# that lack characters.
accounts = search.account_search(args)
if not accounts:
self.msg('Could not find an account by that name.')
return
if len(accounts) > 1:
string = "There were multiple matches:\n"
string += "\n".join(" %s %s" % (account.id, account.key) for account in accounts)
self.msg(string)
return
# one single match
account = accounts.first()
if not account.access(caller, 'delete'):
string = "You don't have the permissions to delete that account."
self.msg(string)
return
uname = account.username
# boot the account then delete
self.msg("Informing and disconnecting account ...")
string = "\nYour account '%s' is being *permanently* deleted.\n" % uname
if reason:
string += " Reason given:\n '%s'" % reason
account.msg(string)
logger.log_sec('Account Deleted: %s (Reason: %s, Caller: %s, IP: %s).' % (account, reason, caller, self.session.address))
account.delete()
self.msg("Account %s was successfully deleted." % uname)
class CmdEmit(COMMAND_DEFAULT_CLASS): class CmdEmit(COMMAND_DEFAULT_CLASS):
""" """
admin command for emitting message to multiple objects admin command for emitting message to multiple objects
@ -585,3 +513,33 @@ class CmdWall(COMMAND_DEFAULT_CLASS):
message = "%s shouts \"%s\"" % (self.caller.name, self.args) message = "%s shouts \"%s\"" % (self.caller.name, self.args)
self.msg("Announcing to all connected sessions ...") self.msg("Announcing to all connected sessions ...")
SESSIONS.announce_all(message) SESSIONS.announce_all(message)
class CmdForce(COMMAND_DEFAULT_CLASS):
"""
forces an object to execute a command
Usage:
@force <object>=<command string>
Example:
@force bob=get stick
"""
key = "@force"
locks = "cmd:perm(spawn) or perm(Builder)"
help_category = "Building"
perm_used = "edit"
def func(self):
"""Implements the force command"""
if not self.lhs or not self.rhs:
self.caller.msg("You must provide a target and a command string to execute.")
return
targ = self.caller.search(self.lhs)
if not targ:
return
if not targ.access(self.caller, self.perm_used):
self.caller.msg("You don't have permission to force them to execute commands.")
return
targ.execute_cmd(self.rhs)
self.caller.msg("You have forced %s to: %s" % (targ, self.rhs))

View file

@ -10,7 +10,7 @@ from evennia.objects.models import ObjectDB
from evennia.locks.lockhandler import LockException from evennia.locks.lockhandler import LockException
from evennia.commands.cmdhandler import get_and_merge_cmdsets from evennia.commands.cmdhandler import get_and_merge_cmdsets
from evennia.utils import create, utils, search from evennia.utils import create, utils, search
from evennia.utils.utils import inherits_from, class_from_module, get_all_typeclasses from evennia.utils.utils import inherits_from, class_from_module, get_all_typeclasses, variable_from_module
from evennia.utils.eveditor import EvEditor from evennia.utils.eveditor import EvEditor
from evennia.utils.evmore import EvMore from evennia.utils.evmore import EvMore
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
@ -612,12 +612,12 @@ class CmdDesc(COMMAND_DEFAULT_CLASS):
self.edit_handler() self.edit_handler()
return return
if self.rhs: if '=' in self.args:
# We have an = # We have an =
obj = caller.search(self.lhs) obj = caller.search(self.lhs)
if not obj: if not obj:
return return
desc = self.rhs desc = self.rhs or ''
else: else:
obj = caller.location or self.msg("|rYou can't describe oblivion.|n") obj = caller.location or self.msg("|rYou can't describe oblivion.|n")
if not obj: if not obj:
@ -737,12 +737,11 @@ class CmdDestroy(COMMAND_DEFAULT_CLASS):
confirm += ", ".join(["#{}".format(obj.id) for obj in objs]) confirm += ", ".join(["#{}".format(obj.id) for obj in objs])
confirm += " [yes]/no?" if self.default_confirm == 'yes' else " yes/[no]" confirm += " [yes]/no?" if self.default_confirm == 'yes' else " yes/[no]"
answer = "" answer = ""
while answer.strip().lower() not in ("y", "yes", "n", "no"): answer = yield(confirm)
answer = yield(confirm) answer = self.default_confirm if answer == '' else answer
answer = self.default_confirm if answer == '' else answer
if answer.strip().lower() in ("n", "no"): if answer.strip().lower() in ("n", "no"):
caller.msg("Cancelled: no object was destroyed.") caller.msg("Canceled: no object was destroyed.")
delete = False delete = False
if delete: if delete:
@ -1023,10 +1022,17 @@ class CmdLink(COMMAND_DEFAULT_CLASS):
object_name = self.lhs object_name = self.lhs
# get object # try to search locally first
obj = caller.search(object_name, global_search=True) results = caller.search(object_name, quiet=True)
if not obj: if len(results) > 1: # local results was a multimatch. Inform them to be more specific
return _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
return _AT_SEARCH_RESULT(results, caller, query=object_name)
elif len(results) == 1: # A unique local match
obj = results[0]
else: # No matches. Search globally
obj = caller.search(object_name, global_search=True)
if not obj:
return
if self.rhs: if self.rhs:
# this means a target name was given # this means a target name was given

View file

@ -55,7 +55,6 @@ class AccountCmdSet(CmdSet):
self.add(system.CmdPy()) self.add(system.CmdPy())
# Admin commands # Admin commands
self.add(admin.CmdDelAccount())
self.add(admin.CmdNewPassword()) self.add(admin.CmdNewPassword())
# Comm commands # Comm commands

View file

@ -57,6 +57,7 @@ class CharacterCmdSet(CmdSet):
self.add(admin.CmdEmit()) self.add(admin.CmdEmit())
self.add(admin.CmdPerm()) self.add(admin.CmdPerm())
self.add(admin.CmdWall()) self.add(admin.CmdWall())
self.add(admin.CmdForce())
# Building and world manipulation # Building and world manipulation
self.add(building.CmdTeleport()) self.add(building.CmdTeleport())

View file

@ -7,6 +7,8 @@ make sure to homogenize self.caller to always be the account object
for easy handling. for easy handling.
""" """
import hashlib
import time
from past.builtins import cmp from past.builtins import cmp
from django.conf import settings from django.conf import settings
from evennia.comms.models import ChannelDB, Msg from evennia.comms.models import ChannelDB, Msg
@ -921,8 +923,9 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
self.msg("Account '%s' already exists and is not a bot." % botname) self.msg("Account '%s' already exists and is not a bot." % botname)
return return
else: else:
password = hashlib.md5(str(time.time())).hexdigest()[:11]
try: try:
bot = create.create_account(botname, None, None, typeclass=botclass) bot = create.create_account(botname, None, password, typeclass=botclass)
except Exception as err: except Exception as err:
self.msg("|rError, could not create the bot:|n '%s'." % err) self.msg("|rError, could not create the bot:|n '%s'." % err)
return return

View file

@ -18,7 +18,7 @@ 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
from evennia.accounts.models import AccountDB from evennia.accounts.models import AccountDB
from evennia.utils import logger, utils, gametime, create from evennia.utils import logger, utils, gametime, create, search
from evennia.utils.eveditor import EvEditor from evennia.utils.eveditor import EvEditor
from evennia.utils.evtable import EvTable from evennia.utils.evtable import EvTable
from evennia.utils.utils import crop, class_from_module from evennia.utils.utils import crop, class_from_module
@ -460,17 +460,22 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
class CmdAccounts(COMMAND_DEFAULT_CLASS): class CmdAccounts(COMMAND_DEFAULT_CLASS):
""" """
list all registered accounts Manage registered accounts
Usage: Usage:
@accounts [nr] @accounts [nr]
@accounts/delete <name or #id> [: reason]
Lists statistics about the Accounts registered with the game. Switches:
delete - delete an account from the server
By default, lists statistics about the Accounts registered with the game.
It will list the <nr> amount of latest registered accounts It will list the <nr> amount of latest registered accounts
If not given, <nr> defaults to 10. If not given, <nr> defaults to 10.
""" """
key = "@accounts" key = "@accounts"
aliases = ["@listaccounts"] aliases = ["@account", "@listaccounts"]
switch_options = ("delete", )
locks = "cmd:perm(listaccounts) or perm(Admin)" locks = "cmd:perm(listaccounts) or perm(Admin)"
help_category = "System" help_category = "System"
@ -478,6 +483,56 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
"""List the accounts""" """List the accounts"""
caller = self.caller caller = self.caller
args = self.args
if "delete" in self.switches:
account = getattr(caller, "account")
if not account or not account.check_permstring("Developer"):
caller.msg("You are not allowed to delete accounts.")
return
if not args:
caller.msg("Usage: @accounts/delete <name or #id> [: reason]")
return
reason = ""
if ":" in args:
args, reason = [arg.strip() for arg in args.split(":", 1)]
# We use account_search since we want to be sure to find also accounts
# that lack characters.
accounts = search.account_search(args)
if not accounts:
self.msg("Could not find an account by that name.")
return
if len(accounts) > 1:
string = "There were multiple matches:\n"
string += "\n".join(" %s %s" % (account.id, account.key) for account in accounts)
self.msg(string)
return
account = accounts.first()
if not account.access(caller, "delete"):
self.msg("You don't have the permissions to delete that account.")
return
username = account.username
# ask for confirmation
confirm = ("It is often better to block access to an account rather than to delete it. "
"|yAre you sure you want to permanently delete "
"account '|n{}|y'|n yes/[no]?".format(username))
answer = yield(confirm)
if answer.lower() not in ('y', 'yes'):
caller.msg("Canceled deletion.")
return
# Boot the account then delete it.
self.msg("Informing and disconnecting account ...")
string = "\nYour account '%s' is being *permanently* deleted.\n" % username
if reason:
string += " Reason given:\n '%s'" % reason
account.msg(string)
logger.log_sec("Account Deleted: %s (Reason: %s, Caller: %s, IP: %s)." % (account, reason, caller, self.session.address))
account.delete()
self.msg("Account %s was successfully deleted." % username)
return
# No switches, default to displaying a list of accounts.
if self.args and self.args.isdigit(): if self.args and self.args.isdigit():
nlim = int(self.args) nlim = int(self.args)
else: else:
@ -655,6 +710,7 @@ class CmdTime(COMMAND_DEFAULT_CLASS):
"""Show server time data in a table.""" """Show server time data in a table."""
table1 = EvTable("|wServer time", "", align="l", width=78) table1 = EvTable("|wServer time", "", align="l", width=78)
table1.add_row("Current uptime", utils.time_format(gametime.uptime(), 3)) table1.add_row("Current uptime", utils.time_format(gametime.uptime(), 3))
table1.add_row("Portal uptime", utils.time_format(gametime.portal_uptime(), 3))
table1.add_row("Total runtime", utils.time_format(gametime.runtime(), 2)) table1.add_row("Total runtime", utils.time_format(gametime.runtime(), 2))
table1.add_row("First start", datetime.datetime.fromtimestamp(gametime.server_epoch())) table1.add_row("First start", datetime.datetime.fromtimestamp(gametime.server_epoch()))
table1.add_row("Current time", datetime.datetime.now()) table1.add_row("Current time", datetime.datetime.now())

View file

@ -243,6 +243,9 @@ class TestAdmin(CommandTest):
def test_ban(self): def test_ban(self):
self.call(admin.CmdBan(), "Char", "Name-Ban char was added.") self.call(admin.CmdBan(), "Char", "Name-Ban char was added.")
def test_force(self):
self.call(admin.CmdForce(), "Char2=say test", 'Char2(#7) says, "test"|You have forced Char2 to: say test')
class TestAccount(CommandTest): class TestAccount(CommandTest):
@ -315,6 +318,24 @@ class TestBuilding(CommandTest):
def test_desc(self): def test_desc(self):
self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#5).") self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#5).")
def test_empty_desc(self):
"""
empty desc sets desc as ''
"""
o2d = self.obj2.db.desc
r1d = self.room1.db.desc
self.call(building.CmdDesc(), "Obj2=", "The description was set on Obj2(#5).")
assert self.obj2.db.desc == '' and self.obj2.db.desc != o2d
assert self.room1.db.desc == r1d
def test_desc_default_to_room(self):
"""no rhs changes room's desc"""
o2d = self.obj2.db.desc
r1d = self.room1.db.desc
self.call(building.CmdDesc(), "Obj2", "The description was set on Room(#1).")
assert self.obj2.db.desc == o2d
assert self.room1.db.desc == 'Obj2' and self.room1.db.desc != r1d
def test_wipe(self): def test_wipe(self):
confirm = building.CmdDestroy.confirm confirm = building.CmdDestroy.confirm
building.CmdDestroy.confirm = False building.CmdDestroy.confirm = False
@ -334,6 +355,14 @@ class TestBuilding(CommandTest):
self.call(building.CmdOpen(), "TestExit1=Room2", "Created new Exit 'TestExit1' from Room to Room2") self.call(building.CmdOpen(), "TestExit1=Room2", "Created new Exit 'TestExit1' from Room to Room2")
self.call(building.CmdLink(), "TestExit1=Room", "Link created TestExit1 -> Room (one way).") self.call(building.CmdLink(), "TestExit1=Room", "Link created TestExit1 -> Room (one way).")
self.call(building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere.") self.call(building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere.")
self.char1.location = self.room2
self.call(building.CmdOpen(), "TestExit2=Room", "Created new Exit 'TestExit2' from Room2 to Room.")
# ensure it matches locally first
self.call(building.CmdLink(), "TestExit=Room2", "Link created TestExit2 -> Room2 (one way).")
# ensure can still match globally when not a local name
self.call(building.CmdLink(), "TestExit1=Room2", "Note: TestExit1(#8) did not have a destination set before. "
"Make sure you linked the right thing.\n"
"Link created TestExit1 -> Room2 (one way).")
def test_set_home(self): def test_set_home(self):
self.call(building.CmdSetHome(), "Obj = Room2", "Obj's home location was changed from Room") self.call(building.CmdSetHome(), "Obj = Room2", "Obj's home location was changed from Room")

View file

@ -475,14 +475,14 @@ class CmdShiftRoot(Command):
root_pos["blue"] -= 1 root_pos["blue"] -= 1
self.caller.msg("The root with blue flowers gets in the way and is pushed to the left.") self.caller.msg("The root with blue flowers gets in the way and is pushed to the left.")
else: else:
self.caller.msg("You cannot move the root in that direction.") self.caller.msg("The root hangs straight down - you can only move it left or right.")
elif color == "blue": elif color == "blue":
if direction == "left": if direction == "left":
root_pos[color] = max(-1, root_pos[color] - 1) root_pos[color] = max(-1, root_pos[color] - 1)
self.caller.msg("You shift the root with small blue flowers to the left.") self.caller.msg("You shift the root with small blue flowers to the left.")
if root_pos[color] != 0 and root_pos[color] == root_pos["red"]: if root_pos[color] != 0 and root_pos[color] == root_pos["red"]:
root_pos["red"] += 1 root_pos["red"] += 1
self.caller.msg("The reddish root is to big to fit as well, so that one falls away to the left.") self.caller.msg("The reddish root is too big to fit as well, so that one falls away to the left.")
elif direction == "right": elif direction == "right":
root_pos[color] = min(1, root_pos[color] + 1) root_pos[color] = min(1, root_pos[color] + 1)
self.caller.msg("You shove the root adorned with small blue flowers to the right.") self.caller.msg("You shove the root adorned with small blue flowers to the right.")
@ -490,7 +490,7 @@ class CmdShiftRoot(Command):
root_pos["red"] -= 1 root_pos["red"] -= 1
self.caller.msg("The thick reddish root gets in the way and is pushed back to the left.") self.caller.msg("The thick reddish root gets in the way and is pushed back to the left.")
else: else:
self.caller.msg("You cannot move the root in that direction.") self.caller.msg("The root hangs straight down - you can only move it left or right.")
# now the horizontal roots (yellow/green). They can be moved up/down # now the horizontal roots (yellow/green). They can be moved up/down
elif color == "yellow": elif color == "yellow":
@ -507,7 +507,7 @@ class CmdShiftRoot(Command):
root_pos["green"] -= 1 root_pos["green"] -= 1
self.caller.msg("The weedy green root is shifted upwards to make room.") self.caller.msg("The weedy green root is shifted upwards to make room.")
else: else:
self.caller.msg("You cannot move the root in that direction.") self.caller.msg("The root hangs across the wall - you can only move it up or down.")
elif color == "green": elif color == "green":
if direction == "up": if direction == "up":
root_pos[color] = max(-1, root_pos[color] - 1) root_pos[color] = max(-1, root_pos[color] - 1)
@ -522,7 +522,7 @@ class CmdShiftRoot(Command):
root_pos["yellow"] -= 1 root_pos["yellow"] -= 1
self.caller.msg("The root with yellow flowers gets in the way and is pushed upwards.") self.caller.msg("The root with yellow flowers gets in the way and is pushed upwards.")
else: else:
self.caller.msg("You cannot move the root in that direction.") self.caller.msg("The root hangs across the wall - you can only move it up or down.")
# we have moved the root. Store new position # we have moved the root. Store new position
self.obj.db.root_pos = root_pos self.obj.db.root_pos = root_pos

View file

@ -747,9 +747,16 @@ class CmdLookDark(Command):
""" """
caller = self.caller caller = self.caller
if random.random() < 0.75: # count how many searches we've done
nr_searches = caller.ndb.dark_searches
if nr_searches is None:
nr_searches = 0
caller.ndb.dark_searches = nr_searches
if nr_searches < 4 and random.random() < 0.90:
# we don't find anything # we don't find anything
caller.msg(random.choice(DARK_MESSAGES)) caller.msg(random.choice(DARK_MESSAGES))
caller.ndb.dark_searches += 1
else: else:
# we could have found something! # we could have found something!
if any(obj for obj in caller.contents if utils.inherits_from(obj, LightSource)): if any(obj for obj in caller.contents if utils.inherits_from(obj, LightSource)):
@ -791,7 +798,8 @@ class CmdDarkNoMatch(Command):
def func(self): def func(self):
"""Implements the command.""" """Implements the command."""
self.caller.msg("Until you find some light, there's not much you can do. Try feeling around.") self.caller.msg("Until you find some light, there's not much you can do. "
"Try feeling around, maybe you'll find something helpful!")
class DarkCmdSet(CmdSet): class DarkCmdSet(CmdSet):
@ -814,7 +822,9 @@ class DarkCmdSet(CmdSet):
self.add(CmdLookDark()) self.add(CmdLookDark())
self.add(CmdDarkHelp()) self.add(CmdDarkHelp())
self.add(CmdDarkNoMatch()) self.add(CmdDarkNoMatch())
self.add(default_cmds.CmdSay) self.add(default_cmds.CmdSay())
self.add(default_cmds.CmdQuit())
self.add(default_cmds.CmdHome())
class DarkRoom(TutorialRoom): class DarkRoom(TutorialRoom):

View file

@ -980,8 +980,12 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
# no need to disconnect, Account just jumps to OOC mode. # no need to disconnect, Account just jumps to OOC mode.
# sever the connection (important!) # sever the connection (important!)
if self.account: if self.account:
# Remove the object from playable characters list
if self in self.account.db._playable_characters:
self.account.db._playable_characters = [x for x in self.account.db._playable_characters if x != self]
for session in self.sessions.all(): for session in self.sessions.all():
self.account.unpuppet_object(session) self.account.unpuppet_object(session)
self.account = None self.account = None
for script in _ScriptDB.objects.get_all_scripts_on_obj(self): for script in _ScriptDB.objects.get_all_scripts_on_obj(self):

View file

@ -107,9 +107,10 @@ for mod in settings.PROTOTYPE_MODULES:
# internally we store as (key, desc, locks, tags, prototype_dict) # internally we store as (key, desc, locks, tags, prototype_dict)
prots = [] prots = []
for variable_name, prot in all_from_module(mod).items(): for variable_name, prot in all_from_module(mod).items():
if "prototype_key" not in prot: if isinstance(prot, dict):
prot['prototype_key'] = variable_name.lower() if "prototype_key" not in prot:
prots.append((prot['prototype_key'], homogenize_prototype(prot))) prot['prototype_key'] = variable_name.lower()
prots.append((prot['prototype_key'], homogenize_prototype(prot)))
# assign module path to each prototype_key for easy reference # assign module path to each prototype_key for easy reference
_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots}) _MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots})
# make sure the prototype contains all meta info # make sure the prototype contains all meta info

View file

@ -716,7 +716,7 @@ def spawn(*prototypes, **kwargs):
val = prot.pop("tags", []) val = prot.pop("tags", [])
tags = [] tags = []
for (tag, category, data) in val: for (tag, category, data) in val:
tags.append((init_spawn_value(val, str), category, data)) tags.append((init_spawn_value(tag, str), category, data))
prototype_key = prototype.get('prototype_key', None) prototype_key = prototype.get('prototype_key', None)
if prototype_key: if prototype_key:

View file

@ -221,6 +221,7 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
server_restart_mode = kwargs.get("server_restart_mode", "shutdown") server_restart_mode = kwargs.get("server_restart_mode", "shutdown")
self.factory.server.run_init_hooks(server_restart_mode) self.factory.server.run_init_hooks(server_restart_mode)
server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata")) server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata"))
server_sessionhandler.portal_start_time = kwargs.get("portal_start_time")
elif operation == amp.SRELOAD: # server reload elif operation == amp.SRELOAD: # server reload
# shut down in reload mode # shut down in reload mode

View file

@ -325,7 +325,7 @@ MENU = \
| 7) Kill Server only (send kill signal to process) | | 7) Kill Server only (send kill signal to process) |
| 8) Kill Portal + Server | | 8) Kill Portal + Server |
+--- Information -----------------------------------------------+ +--- Information -----------------------------------------------+
| 9) Tail log files (quickly see errors) | | 9) Tail log files (quickly see errors - Ctrl-C to exit) |
| 10) Status | | 10) Status |
| 11) Port info | | 11) Port info |
+--- Testing ---------------------------------------------------+ +--- Testing ---------------------------------------------------+

View file

@ -428,7 +428,8 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
self.send_AdminPortal2Server(amp.DUMMYSESSION, self.send_AdminPortal2Server(amp.DUMMYSESSION,
amp.PSYNC, amp.PSYNC,
server_restart_mode=server_restart_mode, server_restart_mode=server_restart_mode,
sessiondata=sessdata) sessiondata=sessdata,
portal_start_time=self.factory.portal.start_time)
self.factory.portal.sessions.at_server_connection() self.factory.portal.sessions.at_server_connection()
if self.factory.server_connection: if self.factory.server_connection:

View file

@ -11,6 +11,7 @@ from builtins import object
import sys import sys
import os import os
import time
from os.path import dirname, abspath from os.path import dirname, abspath
from twisted.application import internet, service from twisted.application import internet, service
@ -114,6 +115,8 @@ class Portal(object):
self.server_restart_mode = "shutdown" self.server_restart_mode = "shutdown"
self.server_info_dict = {} self.server_info_dict = {}
self.start_time = time.time()
# in non-interactive portal mode, this gets overwritten by # in non-interactive portal mode, this gets overwritten by
# cmdline sent by the evennia launcher # cmdline sent by the evennia launcher
self.server_twistd_cmd = self._get_backup_server_twistd_cmd() self.server_twistd_cmd = self._get_backup_server_twistd_cmd()

View file

@ -84,7 +84,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
from evennia.utils.utils import delay from evennia.utils.utils import delay
# timeout the handshakes in case the client doesn't reply at all # timeout the handshakes in case the client doesn't reply at all
delay(2, callback=self.handshake_done, timeout=True) self._handshake_delay = delay(2, callback=self.handshake_done, timeout=True)
# TCP/IP keepalive watches for dead links # TCP/IP keepalive watches for dead links
self.transport.setTcpKeepAlive(1) self.transport.setTcpKeepAlive(1)

View file

@ -8,9 +8,24 @@ try:
except ImportError: except ImportError:
import unittest import unittest
from mock import Mock
import string import string
from evennia.server.portal import irc from evennia.server.portal import irc
from twisted.conch.telnet import IAC, WILL, DONT, SB, SE, NAWS, DO
from twisted.test import proto_helpers
from twisted.trial.unittest import TestCase as TwistedTestCase
from .telnet import TelnetServerFactory, TelnetProtocol
from .portal import PORTAL_SESSIONS
from .suppress_ga import SUPPRESS_GA
from .naws import DEFAULT_HEIGHT, DEFAULT_WIDTH
from .ttype import TTYPE, IS
from .mccp import MCCP
from .mssp import MSSP
from .mxp import MXP
from .telnet_oob import MSDP, MSDP_VAL, MSDP_VAR
class TestIRC(TestCase): class TestIRC(TestCase):
@ -73,3 +88,64 @@ class TestIRC(TestCase):
s = r'|wthis|Xis|gis|Ma|C|complex|*string' s = r'|wthis|Xis|gis|Ma|C|complex|*string'
self.assertEqual(irc.parse_irc_to_ansi(irc.parse_ansi_to_irc(s)), s) self.assertEqual(irc.parse_irc_to_ansi(irc.parse_ansi_to_irc(s)), s)
class TestTelnet(TwistedTestCase):
def setUp(self):
super(TestTelnet, self).setUp()
factory = TelnetServerFactory()
factory.protocol = TelnetProtocol
factory.sessionhandler = PORTAL_SESSIONS
factory.sessionhandler.portal = Mock()
self.proto = factory.buildProtocol(("localhost", 0))
self.transport = proto_helpers.StringTransport()
self.addCleanup(factory.sessionhandler.disconnect_all)
def test_mudlet_ttype(self):
self.transport.client = ["localhost"]
self.transport.setTcpKeepAlive = Mock()
d = self.proto.makeConnection(self.transport)
# test suppress_ga
self.assertTrue(self.proto.protocol_flags["NOGOAHEAD"])
self.proto.dataReceived(IAC + DONT + SUPPRESS_GA)
self.assertFalse(self.proto.protocol_flags["NOGOAHEAD"])
self.assertEqual(self.proto.handshakes, 7)
# test naws
self.assertEqual(self.proto.protocol_flags['SCREENWIDTH'], {0: DEFAULT_WIDTH})
self.assertEqual(self.proto.protocol_flags['SCREENHEIGHT'], {0: DEFAULT_HEIGHT})
self.proto.dataReceived(IAC + WILL + NAWS)
self.proto.dataReceived([IAC, SB, NAWS, '', 'x', '', 'd', IAC, SE])
self.assertEqual(self.proto.protocol_flags['SCREENWIDTH'][0], 120)
self.assertEqual(self.proto.protocol_flags['SCREENHEIGHT'][0], 100)
self.assertEqual(self.proto.handshakes, 6)
# test ttype
self.assertTrue(self.proto.protocol_flags["FORCEDENDLINE"])
self.assertFalse(self.proto.protocol_flags["TTYPE"])
self.assertTrue(self.proto.protocol_flags["ANSI"])
self.proto.dataReceived(IAC + WILL + TTYPE)
self.proto.dataReceived([IAC, SB, TTYPE, IS, "MUDLET", IAC, SE])
self.assertTrue(self.proto.protocol_flags["XTERM256"])
self.assertEqual(self.proto.protocol_flags["CLIENTNAME"], "MUDLET")
self.proto.dataReceived([IAC, SB, TTYPE, IS, "XTERM", IAC, SE])
self.proto.dataReceived([IAC, SB, TTYPE, IS, "MTTS 137", IAC, SE])
self.assertEqual(self.proto.handshakes, 5)
# test mccp
self.proto.dataReceived(IAC + DONT + MCCP)
self.assertFalse(self.proto.protocol_flags['MCCP'])
self.assertEqual(self.proto.handshakes, 4)
# test mssp
self.proto.dataReceived(IAC + DONT + MSSP)
self.assertEqual(self.proto.handshakes, 3)
# test oob
self.proto.dataReceived(IAC + DO + MSDP)
self.proto.dataReceived([IAC, SB, MSDP, MSDP_VAR, "LIST", MSDP_VAL, "COMMANDS", IAC, SE])
self.assertTrue(self.proto.protocol_flags['OOB'])
self.assertEqual(self.proto.handshakes, 2)
# test mxp
self.proto.dataReceived(IAC + DONT + MXP)
self.assertFalse(self.proto.protocol_flags['MXP'])
self.assertEqual(self.proto.handshakes, 1)
# clean up to prevent Unclean reactor
self.proto.nop_keep_alive.stop()
self.proto._handshake_delay.cancel()
return d

View file

@ -13,14 +13,14 @@ import time
# TODO! # TODO!
#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) #sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
#os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' #os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings'
import ev import evennia
from evennia.utils.idmapper import base as _idmapper from evennia.utils.idmapper import models as _idmapper
LOGFILE = "logs/memoryusage.log" LOGFILE = "logs/memoryusage.log"
INTERVAL = 30 # log every 30 seconds INTERVAL = 30 # log every 30 seconds
class Memplot(ev.Script): class Memplot(evennia.DefaultScript):
""" """
Describes a memory plotting action. Describes a memory plotting action.

View file

@ -1,7 +1,8 @@
from django.test import TestCase from django.test import TestCase
from mock import Mock from mock import Mock, patch, mock_open
from .dummyrunner_settings import (c_creates_button, c_creates_obj, c_digs, c_examines, c_help, c_idles, c_login, from .dummyrunner_settings import (c_creates_button, c_creates_obj, c_digs, c_examines, c_help, c_idles, c_login,
c_login_nodig, c_logout, c_looks, c_moves, c_moves_n, c_moves_s, c_socialize) c_login_nodig, c_logout, c_looks, c_moves, c_moves_n, c_moves_s, c_socialize)
import memplot
class TestDummyrunnerSettings(TestCase): class TestDummyrunnerSettings(TestCase):
@ -91,3 +92,21 @@ class TestDummyrunnerSettings(TestCase):
def test_c_move_s(self): def test_c_move_s(self):
self.assertEqual(c_moves_s(self.client), "south") self.assertEqual(c_moves_s(self.client), "south")
class TestMemPlot(TestCase):
@patch.object(memplot, "_idmapper")
@patch.object(memplot, "os")
@patch.object(memplot, "open", new_callable=mock_open, create=True)
@patch.object(memplot, "time")
def test_memplot(self, mock_time, mocked_open, mocked_os, mocked_idmapper):
from evennia.utils.create import create_script
mocked_idmapper.cache_size.return_value = (9, 5000)
mock_time.time = Mock(return_value=6000.0)
script = create_script(memplot.Memplot)
script.db.starttime = 0.0
mocked_os.popen.read.return_value = 5000.0
script.at_repeat()
handle = mocked_open()
handle.write.assert_called_with('100.0, 0.001, 0.001, 9\n')
script.stop()

View file

@ -407,7 +407,7 @@ class ServerSession(Session):
else: else:
self.data_out(**kwargs) self.data_out(**kwargs)
def execute_cmd(self, raw_string, **kwargs): def execute_cmd(self, raw_string, session=None, **kwargs):
""" """
Do something as this object. This method is normally never Do something as this object. This method is normally never
called directly, instead incoming command instructions are called directly, instead incoming command instructions are
@ -417,6 +417,9 @@ class ServerSession(Session):
Args: Args:
raw_string (string): Raw command input raw_string (string): Raw command input
session (Session): This is here to make API consistent with
Account/Object.execute_cmd. If given, data is passed to
that Session, otherwise use self.
Kwargs: Kwargs:
Other keyword arguments will be added to the found command Other keyword arguments will be added to the found command
object instace as variables before it executes. This is object instace as variables before it executes. This is
@ -426,7 +429,7 @@ class ServerSession(Session):
""" """
# inject instruction into input stream # inject instruction into input stream
kwargs["text"] = ((raw_string,), {}) kwargs["text"] = ((raw_string,), {})
self.sessionhandler.data_in(self, **kwargs) self.sessionhandler.data_in(session or self, **kwargs)
def __eq__(self, other): def __eq__(self, other):
"""Handle session comparisons""" """Handle session comparisons"""

View file

@ -280,6 +280,8 @@ class ServerSessionHandler(SessionHandler):
super(ServerSessionHandler, self).__init__(*args, **kwargs) super(ServerSessionHandler, self).__init__(*args, **kwargs)
self.server = None # set at server initialization self.server = None # set at server initialization
self.server_data = {"servername": _SERVERNAME} self.server_data = {"servername": _SERVERNAME}
# will be set on psync
self.portal_start_time = 0.0
def _run_cmd_login(self, session): def _run_cmd_login(self, session):
""" """

View file

@ -107,6 +107,17 @@ def uptime():
return time.time() - SERVER_START_TIME return time.time() - SERVER_START_TIME
def portal_uptime():
"""
Get the current uptime of the portal.
Returns:
time (float): The uptime of the portal.
"""
from evennia.server.sessionhandler import SESSIONS
return time.time() - SESSIONS.portal_start_time
def game_epoch(): def game_epoch():
""" """
Get the game epoch. Get the game epoch.