From 40eb691cd4dcdfe86d3e00c6208002401556da71 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 7 Oct 2018 21:17:40 +0200 Subject: [PATCH 01/24] Create hash password when creating irc bot. --- evennia/commands/default/comms.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index d9fe0b0d20..04fcceba46 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -7,6 +7,8 @@ make sure to homogenize self.caller to always be the account object for easy handling. """ +import hashlib +import time from past.builtins import cmp from django.conf import settings from evennia.comms.models import ChannelDB, Msg @@ -918,8 +920,9 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS): self.msg("Account '%s' already exists and is not a bot." % botname) return else: + password = hashlib.md5(str(time.time())).hexdigest()[:11] try: - bot = create.create_account(botname, None, None, typeclass=botclass) + bot = create.create_account(botname, None, password, typeclass=botclass) except Exception as err: self.msg("|rError, could not create the bot:|n '%s'." % err) return From c153a1d7e4c93513d9c072a8f3b08bf1229d854d Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 8 Oct 2018 18:20:35 +0200 Subject: [PATCH 02/24] Resolve bug when trying to examine self when unprivileged --- evennia/accounts/accounts.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 2c33e5c1f8..3eab69a3ce 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -1001,7 +1001,10 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): if target and not is_iter(target): # 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: # list of targets - make list to disconnect from db characters = list(tar for tar in target if tar) if target else [] From 3b75780b40604564e846a9baedb4354800425b99 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 8 Oct 2018 18:50:33 +0200 Subject: [PATCH 03/24] Make tutorial_world roots give clearer errors. Allow home/quit from dark room. Resolves #1584. --- evennia/contrib/tutorial_world/objects.py | 10 +++++----- evennia/contrib/tutorial_world/rooms.py | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/evennia/contrib/tutorial_world/objects.py b/evennia/contrib/tutorial_world/objects.py index f83462ad6b..331b6b1a21 100644 --- a/evennia/contrib/tutorial_world/objects.py +++ b/evennia/contrib/tutorial_world/objects.py @@ -475,14 +475,14 @@ class CmdShiftRoot(Command): root_pos["blue"] -= 1 self.caller.msg("The root with blue flowers gets in the way and is pushed to the left.") 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": if direction == "left": root_pos[color] = max(-1, root_pos[color] - 1) 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"]: 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": root_pos[color] = min(1, root_pos[color] + 1) 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 self.caller.msg("The thick reddish root gets in the way and is pushed back to the left.") 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 elif color == "yellow": @@ -507,7 +507,7 @@ class CmdShiftRoot(Command): root_pos["green"] -= 1 self.caller.msg("The weedy green root is shifted upwards to make room.") 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": if direction == "up": root_pos[color] = max(-1, root_pos[color] - 1) @@ -522,7 +522,7 @@ class CmdShiftRoot(Command): root_pos["yellow"] -= 1 self.caller.msg("The root with yellow flowers gets in the way and is pushed upwards.") 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 self.obj.db.root_pos = root_pos diff --git a/evennia/contrib/tutorial_world/rooms.py b/evennia/contrib/tutorial_world/rooms.py index 780f774af7..58e13a1356 100644 --- a/evennia/contrib/tutorial_world/rooms.py +++ b/evennia/contrib/tutorial_world/rooms.py @@ -747,9 +747,16 @@ class CmdLookDark(Command): """ 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 caller.msg(random.choice(DARK_MESSAGES)) + caller.ndb.dark_searches += 1 else: # we could have found something! if any(obj for obj in caller.contents if utils.inherits_from(obj, LightSource)): @@ -791,7 +798,8 @@ class CmdDarkNoMatch(Command): def func(self): """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): @@ -814,7 +822,9 @@ class DarkCmdSet(CmdSet): self.add(CmdLookDark()) self.add(CmdDarkHelp()) 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): From 52c84b44b58a0af578814e48313d3f3a687e5d2f Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 8 Oct 2018 19:03:15 +0200 Subject: [PATCH 04/24] Handle prototype modules with non-dicts as global variables --- evennia/prototypes/prototypes.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index eac53a6504..fc8edb55ab 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -107,9 +107,10 @@ for mod in settings.PROTOTYPE_MODULES: # internally we store as (key, desc, locks, tags, prototype_dict) prots = [] for variable_name, prot in all_from_module(mod).items(): - if "prototype_key" not in prot: - prot['prototype_key'] = variable_name.lower() - prots.append((prot['prototype_key'], homogenize_prototype(prot))) + if isinstance(prot, dict): + if "prototype_key" not in 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 _MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots}) # make sure the prototype contains all meta info From 5f9047b161e2d5ec25263ba6d42dc33ab060662e Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 10 Oct 2018 23:26:20 +0200 Subject: [PATCH 05/24] Make Session.execute_cmd consistent with Account/Object by accepting the `session` keyword --- evennia/server/serversession.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index b7f74cef5d..c5de7cf5be 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -407,7 +407,7 @@ class ServerSession(Session): else: 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 called directly, instead incoming command instructions are @@ -417,6 +417,9 @@ class ServerSession(Session): Args: 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: Other keyword arguments will be added to the found command object instace as variables before it executes. This is @@ -426,7 +429,7 @@ class ServerSession(Session): """ # inject instruction into input stream kwargs["text"] = ((raw_string,), {}) - self.sessionhandler.data_in(self, **kwargs) + self.sessionhandler.data_in(session or self, **kwargs) def __eq__(self, other): """Handle session comparisons""" From 965e97329493bc7ac56f982ae072351d0b4b11f6 Mon Sep 17 00:00:00 2001 From: Will Hutcheson Date: Fri, 12 Oct 2018 16:37:27 -0500 Subject: [PATCH 06/24] Move delaccount functionality to @accounts/delete Implement #1477 --- evennia/commands/default/admin.py | 76 +--------------------- evennia/commands/default/cmdset_account.py | 1 - evennia/commands/default/system.py | 52 ++++++++++++++- 3 files changed, 51 insertions(+), 78 deletions(-) diff --git a/evennia/commands/default/admin.py b/evennia/commands/default/admin.py index 3bb4e7e512..08890dfcdc 100644 --- a/evennia/commands/default/admin.py +++ b/evennia/commands/default/admin.py @@ -16,7 +16,7 @@ COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] # limit members for API inclusion -__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdDelAccount", +__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall") @@ -133,7 +133,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS): 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 - 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. 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)) -class CmdDelAccount(COMMAND_DEFAULT_CLASS): - """ - delete an account from the server - - Usage: - @delaccount[/switch] [: 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 [: 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): """ admin command for emitting message to multiple objects diff --git a/evennia/commands/default/cmdset_account.py b/evennia/commands/default/cmdset_account.py index d7b887c017..8173e461c5 100644 --- a/evennia/commands/default/cmdset_account.py +++ b/evennia/commands/default/cmdset_account.py @@ -55,7 +55,6 @@ class AccountCmdSet(CmdSet): self.add(system.CmdPy()) # Admin commands - self.add(admin.CmdDelAccount()) self.add(admin.CmdNewPassword()) # Comm commands diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index c454de9aff..070420f4c3 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -18,7 +18,7 @@ from evennia.server.sessionhandler import SESSIONS from evennia.scripts.models import ScriptDB from evennia.objects.models import ObjectDB 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.evtable import EvTable from evennia.utils.utils import crop, class_from_module @@ -460,17 +460,22 @@ class CmdObjects(COMMAND_DEFAULT_CLASS): class CmdAccounts(COMMAND_DEFAULT_CLASS): """ - list all registered accounts + Manage registered accounts Usage: @accounts [nr] + @accounts/delete [: 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 amount of latest registered accounts If not given, defaults to 10. """ key = "@accounts" aliases = ["@listaccounts"] + switch_options = ("delete",) locks = "cmd:perm(listaccounts) or perm(Admin)" help_category = "System" @@ -478,6 +483,47 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS): """List the accounts""" 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 [: 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 + # 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(): nlim = int(self.args) else: From 3bd34087ce8f7479486fb2c7344d9fbd61166e61 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 13 Oct 2018 11:26:31 +0200 Subject: [PATCH 07/24] Add confirmation question to new accounts/delete switch --- evennia/commands/default/building.py | 7 +++---- evennia/commands/default/system.py | 13 +++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index f0ae108f00..19ab0f7508 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -737,12 +737,11 @@ class CmdDestroy(COMMAND_DEFAULT_CLASS): confirm += ", ".join(["#{}".format(obj.id) for obj in objs]) confirm += " [yes]/no?" if self.default_confirm == 'yes' else " yes/[no]" answer = "" - while answer.strip().lower() not in ("y", "yes", "n", "no"): - answer = yield(confirm) - answer = self.default_confirm if answer == '' else answer + answer = yield(confirm) + answer = self.default_confirm if answer == '' else answer if answer.strip().lower() in ("n", "no"): - caller.msg("Cancelled: no object was destroyed.") + caller.msg("Canceled: no object was destroyed.") delete = False if delete: diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index 070420f4c3..3a14a296b4 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -474,8 +474,8 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS): If not given, defaults to 10. """ key = "@accounts" - aliases = ["@listaccounts"] - switch_options = ("delete",) + aliases = ["@account", "@listaccounts"] + switch_options = ("delete", ) locks = "cmd:perm(listaccounts) or perm(Admin)" help_category = "System" @@ -512,6 +512,15 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS): 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 From bedebdd5247a706907c4c44794cbc991825cc13d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 13 Oct 2018 11:28:41 +0200 Subject: [PATCH 08/24] Update changelog. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index afe5ad1ba7..f70d8b7504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Evennia 0.9 (2018-2019) + +### Commands + +- Removed default `@delaccount` command, incorporating as `@account/delete` instead. Added confirmation + question. + + ## Evennia 0.8 (2018) ### Server/Portal From 3fbd74b33203ca9fa9b8e7816c64becaf3b83474 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 13 Oct 2018 16:59:07 +0200 Subject: [PATCH 09/24] Fix (again) of tag batch creation --- evennia/prototypes/spawner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index ac6ad854b1..d1c099fb57 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -716,7 +716,7 @@ def spawn(*prototypes, **kwargs): val = prot.pop("tags", []) tags = [] 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) if prototype_key: From 9f8c1a4f644e5a73f479386334f30696ee6c8496 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 16 Oct 2018 10:13:05 +0200 Subject: [PATCH 10/24] Add requirement changes to 0.8 changelog, for clarity --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afe5ad1ba7..13c450939f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Evennia 0.8 (2018) +### Requirements + +- Up requirements to Django 1.11.x, Twisted 18 and pillow 5.2.0 +- Add `autobahn` dependency for Websocket support, removing very old embedded txWS library (from a + time before websocket specification was still not fixed). +- Add `inflect` dependency for automatic pluralization of object names. + ### Server/Portal - Removed `evennia_runner`, completely refactor `evennia_launcher.py` (the 'evennia' program) @@ -85,7 +92,6 @@ ### 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. - 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) From fe969111ce274aab16d6baf2edd0adebc359e8be Mon Sep 17 00:00:00 2001 From: Tehom Date: Tue, 16 Oct 2018 19:51:43 -0400 Subject: [PATCH 11/24] Add stub for testing Telnet --- evennia/server/portal/tests.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/evennia/server/portal/tests.py b/evennia/server/portal/tests.py index be400144c6..b09f71ae03 100644 --- a/evennia/server/portal/tests.py +++ b/evennia/server/portal/tests.py @@ -11,6 +11,11 @@ except ImportError: import string from evennia.server.portal import irc +from twisted.test import proto_helpers +from twisted.trial.unittest import TestCase as TwistedTestCase + +from .telnet import TelnetServerFactory + class TestIRC(TestCase): @@ -73,3 +78,15 @@ class TestIRC(TestCase): s = r'|wthis|Xis|gis|Ma|C|complex|*string' 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() + self.proto = factory.buildProtocol(("localhost", 0)) + self.transport = proto_helpers.StringTransport() + + def test_connect(self): + self.proto.makeConnection(self.transport) + # TODO: Add rest of stuff for testing connection From 6b96e84fd03039f0512ec947ae29841409c96618 Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Tue, 16 Oct 2018 19:31:10 -0500 Subject: [PATCH 12/24] Tests for @desc obj= --- evennia/commands/default/tests.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index a3be98984a..6010b008da 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -263,6 +263,20 @@ class TestBuilding(CommandTest): def test_desc(self): self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#5).") + def test_empty_desc(self): + 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 == '' + assert self.room1.db.desc == r1d + + def test_desc_default_to_room(self): + 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' + def test_wipe(self): confirm = building.CmdDestroy.confirm building.CmdDestroy.confirm = False @@ -446,4 +460,4 @@ class TestUnconnectedCommand(CommandTest): settings.SERVERNAME, datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(), SESSIONS.account_count(), utils.get_evennia_version()) - self.call(unloggedin.CmdUnconnectedInfo(), "", expected) \ No newline at end of file + self.call(unloggedin.CmdUnconnectedInfo(), "", expected) From 40a37e501f1ffb94f448e886a13220abf402caa0 Mon Sep 17 00:00:00 2001 From: Tehom Date: Tue, 16 Oct 2018 20:33:12 -0400 Subject: [PATCH 13/24] Add some cleanup steps to prevent unclean reactor --- evennia/server/portal/telnet.py | 2 +- evennia/server/portal/tests.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 955ea5e918..83e2fa03a2 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -84,7 +84,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): from evennia.utils.utils import delay # 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 self.transport.setTcpKeepAlive(1) diff --git a/evennia/server/portal/tests.py b/evennia/server/portal/tests.py index b09f71ae03..53a732f121 100644 --- a/evennia/server/portal/tests.py +++ b/evennia/server/portal/tests.py @@ -8,13 +8,15 @@ try: except ImportError: import unittest +from mock import Mock import string from evennia.server.portal import irc from twisted.test import proto_helpers from twisted.trial.unittest import TestCase as TwistedTestCase -from .telnet import TelnetServerFactory +from .telnet import TelnetServerFactory, TelnetProtocol +from .portal import PORTAL_SESSIONS class TestIRC(TestCase): @@ -84,9 +86,19 @@ 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_connect(self): - self.proto.makeConnection(self.transport) + self.transport.client = ["localhost"] + self.transport.setTcpKeepAlive = Mock() + d = self.proto.makeConnection(self.transport) # TODO: Add rest of stuff for testing connection + # clean up to prevent Unclean reactor + self.proto.nop_keep_alive.stop() + self.proto._handshake_delay.cancel() + return d From dc44dc0176acdb6404f116a798393fae9134f85f Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Tue, 16 Oct 2018 19:49:19 -0500 Subject: [PATCH 14/24] In @desc command, validate rhs based on = sign present in orig args. Default MUX parsing assigns None to rhs if there is nothing on the right of the = sign. --- evennia/commands/default/building.py | 4 ++-- evennia/commands/default/tests.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 0afeea8fe5..aaafea30ac 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -589,12 +589,12 @@ class CmdDesc(COMMAND_DEFAULT_CLASS): self.edit_handler() return - if self.rhs: + if '=' in self.args: # We have an = obj = caller.search(self.lhs) if not obj: return - desc = self.rhs + desc = self.rhs or '' else: obj = caller.location or self.msg("|rYou can't describe oblivion.|n") if not obj: diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 6010b008da..6e5877608c 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -264,6 +264,9 @@ class TestBuilding(CommandTest): 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).") @@ -271,6 +274,7 @@ class TestBuilding(CommandTest): 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).") From e08efc68dc637235592aa25ca2fd4b2951544baa Mon Sep 17 00:00:00 2001 From: Henddher Pedroza Date: Tue, 16 Oct 2018 20:09:17 -0500 Subject: [PATCH 15/24] Harden assertions --- evennia/commands/default/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 6e5877608c..1b97a59f9f 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -270,7 +270,7 @@ class TestBuilding(CommandTest): 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 == '' + assert self.obj2.db.desc == '' and self.obj2.db.desc != o2d assert self.room1.db.desc == r1d def test_desc_default_to_room(self): @@ -279,7 +279,7 @@ class TestBuilding(CommandTest): 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' + assert self.room1.db.desc == 'Obj2' and self.room1.db.desc != r1d def test_wipe(self): confirm = building.CmdDestroy.confirm From b4383592016e532ebedb315616353712988bd257 Mon Sep 17 00:00:00 2001 From: Tehom Date: Wed, 17 Oct 2018 01:50:57 -0400 Subject: [PATCH 16/24] Add test of NOGOAHEAD --- evennia/server/portal/tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/evennia/server/portal/tests.py b/evennia/server/portal/tests.py index 53a732f121..4142ca297e 100644 --- a/evennia/server/portal/tests.py +++ b/evennia/server/portal/tests.py @@ -12,11 +12,13 @@ from mock import Mock import string from evennia.server.portal import irc +from twisted.conch.telnet import IAC, NOP, LINEMODE, GA, WILL, WONT, ECHO, NULL, DONT 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 class TestIRC(TestCase): @@ -98,6 +100,10 @@ class TestTelnet(TwistedTestCase): self.transport.setTcpKeepAlive = Mock() d = self.proto.makeConnection(self.transport) # TODO: Add rest of stuff for testing connection + 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) # clean up to prevent Unclean reactor self.proto.nop_keep_alive.stop() self.proto._handshake_delay.cancel() From ef0e0e0b4c6e2827f8832512632c580280c1af28 Mon Sep 17 00:00:00 2001 From: Tehom Date: Wed, 17 Oct 2018 12:49:48 -0400 Subject: [PATCH 17/24] Add various simple tests for different handshakes --- evennia/server/portal/tests.py | 49 +++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/evennia/server/portal/tests.py b/evennia/server/portal/tests.py index 4142ca297e..791e5172a4 100644 --- a/evennia/server/portal/tests.py +++ b/evennia/server/portal/tests.py @@ -12,13 +12,19 @@ from mock import Mock import string from evennia.server.portal import irc -from twisted.conch.telnet import IAC, NOP, LINEMODE, GA, WILL, WONT, ECHO, NULL, DONT +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): @@ -95,15 +101,50 @@ class TestTelnet(TwistedTestCase): self.transport = proto_helpers.StringTransport() self.addCleanup(factory.sessionhandler.disconnect_all) - def test_connect(self): + def test_mudlet_ttype(self): self.transport.client = ["localhost"] self.transport.setTcpKeepAlive = Mock() d = self.proto.makeConnection(self.transport) - # TODO: Add rest of stuff for testing connection + # test suppress_ga self.assertTrue(self.proto.protocol_flags["NOGOAHEAD"]) - self.proto.dataReceived(IAC + DONT+ SUPPRESS_GA) + 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() From 788120706228d9c62d08cab1fe037bae6dc9fd3c Mon Sep 17 00:00:00 2001 From: Tehom Date: Wed, 17 Oct 2018 22:43:33 -0400 Subject: [PATCH 18/24] Add tests for memplot --- evennia/server/profiling/memplot.py | 6 +++--- evennia/server/profiling/tests.py | 21 ++++++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/evennia/server/profiling/memplot.py b/evennia/server/profiling/memplot.py index e8d7e76b12..c6a227a370 100644 --- a/evennia/server/profiling/memplot.py +++ b/evennia/server/profiling/memplot.py @@ -13,14 +13,14 @@ import time # TODO! #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' -import ev -from evennia.utils.idmapper import base as _idmapper +import evennia +from evennia.utils.idmapper import models as _idmapper LOGFILE = "logs/memoryusage.log" INTERVAL = 30 # log every 30 seconds -class Memplot(ev.Script): +class Memplot(evennia.DefaultScript): """ Describes a memory plotting action. diff --git a/evennia/server/profiling/tests.py b/evennia/server/profiling/tests.py index b3e9fba8d5..cca4e0d99b 100644 --- a/evennia/server/profiling/tests.py +++ b/evennia/server/profiling/tests.py @@ -1,7 +1,8 @@ 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, c_login_nodig, c_logout, c_looks, c_moves, c_moves_n, c_moves_s, c_socialize) +import memplot class TestDummyrunnerSettings(TestCase): @@ -91,3 +92,21 @@ class TestDummyrunnerSettings(TestCase): def test_c_move_s(self): 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() From 7dadc86693d3b94f3743b1fea9d4dd99a5f885d4 Mon Sep 17 00:00:00 2001 From: Tehom Date: Wed, 17 Oct 2018 23:41:19 -0400 Subject: [PATCH 19/24] Implement local-first search for @link command --- evennia/commands/default/building.py | 17 ++++++++++++----- evennia/commands/default/tests.py | 8 ++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 19ab0f7508..ff8d56753b 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -10,7 +10,7 @@ from evennia.objects.models import ObjectDB from evennia.locks.lockhandler import LockException from evennia.commands.cmdhandler import get_and_merge_cmdsets 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.evmore import EvMore from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus @@ -1022,10 +1022,17 @@ class CmdLink(COMMAND_DEFAULT_CLASS): object_name = self.lhs - # get object - obj = caller.search(object_name, global_search=True) - if not obj: - return + # try to search locally first + results = caller.search(object_name, quiet=True) + if len(results) > 1: # local results was a multimatch. Inform them to be more specific + _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: # this means a target name was given diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 19277c168a..78e77e2b88 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -334,6 +334,14 @@ class TestBuilding(CommandTest): 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.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): self.call(building.CmdSetHome(), "Obj = Room2", "Obj's home location was changed from Room") From 040cc2aa9f33c3fc2e84fa382a362ab90ba2cfdd Mon Sep 17 00:00:00 2001 From: Tehom Date: Thu, 18 Oct 2018 04:24:03 -0400 Subject: [PATCH 20/24] Add portal uptime to @time command. --- evennia/commands/default/system.py | 1 + evennia/server/amp_client.py | 1 + evennia/server/portal/amp_server.py | 3 ++- evennia/server/portal/portal.py | 3 +++ evennia/server/sessionhandler.py | 2 ++ evennia/utils/gametime.py | 11 +++++++++++ 6 files changed, 20 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index 3a14a296b4..a6cc24263d 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -710,6 +710,7 @@ class CmdTime(COMMAND_DEFAULT_CLASS): """Show server time data in a table.""" table1 = EvTable("|wServer time", "", align="l", width=78) 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("First start", datetime.datetime.fromtimestamp(gametime.server_epoch())) table1.add_row("Current time", datetime.datetime.now()) diff --git a/evennia/server/amp_client.py b/evennia/server/amp_client.py index a4300adf4d..816ecf2705 100644 --- a/evennia/server/amp_client.py +++ b/evennia/server/amp_client.py @@ -221,6 +221,7 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol): server_restart_mode = kwargs.get("server_restart_mode", "shutdown") self.factory.server.run_init_hooks(server_restart_mode) server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata")) + server_sessionhandler.portal_start_time = kwargs.get("portal_start_time") elif operation == amp.SRELOAD: # server reload # shut down in reload mode diff --git a/evennia/server/portal/amp_server.py b/evennia/server/portal/amp_server.py index c07b5c121d..cdcd4a1552 100644 --- a/evennia/server/portal/amp_server.py +++ b/evennia/server/portal/amp_server.py @@ -428,7 +428,8 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol): self.send_AdminPortal2Server(amp.DUMMYSESSION, amp.PSYNC, 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() if self.factory.server_connection: diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index 91b3efc7bc..bb2a7b9f07 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -11,6 +11,7 @@ from builtins import object import sys import os +import time from os.path import dirname, abspath from twisted.application import internet, service @@ -114,6 +115,8 @@ class Portal(object): self.server_restart_mode = "shutdown" self.server_info_dict = {} + self.start_time = time.time() + # in non-interactive portal mode, this gets overwritten by # cmdline sent by the evennia launcher self.server_twistd_cmd = self._get_backup_server_twistd_cmd() diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 8e439b42dd..906ad1ec54 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -280,6 +280,8 @@ class ServerSessionHandler(SessionHandler): super(ServerSessionHandler, self).__init__(*args, **kwargs) self.server = None # set at server initialization self.server_data = {"servername": _SERVERNAME} + # will be set on psync + self.portal_start_time = 0.0 def _run_cmd_login(self, session): """ diff --git a/evennia/utils/gametime.py b/evennia/utils/gametime.py index 3736128819..48910c4bfd 100644 --- a/evennia/utils/gametime.py +++ b/evennia/utils/gametime.py @@ -107,6 +107,17 @@ def uptime(): 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(): """ Get the game epoch. From b083a445457fd285c0c2c71e6bda3d494dbcae33 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 18 Oct 2018 17:15:54 +0200 Subject: [PATCH 21/24] Some clarification in the launcher --- CHANGELOG.md | 2 -- evennia/server/evennia_launcher.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13c450939f..5b56c29d85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,6 @@ ### Requirements - Up requirements to Django 1.11.x, Twisted 18 and pillow 5.2.0 -- Add `autobahn` dependency for Websocket support, removing very old embedded txWS library (from a - time before websocket specification was still not fixed). - Add `inflect` dependency for automatic pluralization of object names. ### Server/Portal diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index 779a1e4aa2..ef6bf61055 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -325,7 +325,7 @@ MENU = \ | 7) Kill Server only (send kill signal to process) | | 8) Kill Portal + Server | +--- Information -----------------------------------------------+ - | 9) Tail log files (quickly see errors) | + | 9) Tail log files (quickly see errors - Ctrl-C to exit) | | 10) Status | | 11) Port info | +--- Testing ---------------------------------------------------+ From 5c3bdd1b4a6bace2b4305e542f6e07fa14f9b71a Mon Sep 17 00:00:00 2001 From: Johnny Date: Thu, 18 Oct 2018 21:30:02 +0000 Subject: [PATCH 22/24] Fixes object delete method so that deleted characters are removed from owner's playable character list. --- evennia/accounts/tests.py | 18 ++++++++++++++++++ evennia/objects/objects.py | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/evennia/accounts/tests.py b/evennia/accounts/tests.py index 78ee87f37d..1285891d32 100644 --- a/evennia/accounts/tests.py +++ b/evennia/accounts/tests.py @@ -7,6 +7,7 @@ from evennia.accounts.accounts import AccountSessionHandler from evennia.accounts.accounts import DefaultAccount from evennia.server.session import Session from evennia.utils import create +from evennia.utils.test_resources import EvenniaTest from django.conf import settings @@ -199,3 +200,20 @@ class TestDefaultAccount(TestCase): account.puppet_object(self.s1, obj) 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) + + +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) \ No newline at end of file diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 27d8147999..3d0b9e02b7 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -913,8 +913,12 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): # no need to disconnect, Account just jumps to OOC mode. # sever the connection (important!) 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(): self.account.unpuppet_object(session) + self.account = None for script in _ScriptDB.objects.get_all_scripts_on_obj(self): From d4f76b3d2e0033c96515552ab02f1843321b9f6c Mon Sep 17 00:00:00 2001 From: Tehom Date: Thu, 18 Oct 2018 23:12:22 -0400 Subject: [PATCH 23/24] Add a simple 'force' command to force objects to execute commands. --- evennia/commands/default/admin.py | 32 +++++++++++++++++++- evennia/commands/default/cmdset_character.py | 1 + evennia/commands/default/tests.py | 3 ++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/admin.py b/evennia/commands/default/admin.py index 08890dfcdc..c260abaa2e 100644 --- a/evennia/commands/default/admin.py +++ b/evennia/commands/default/admin.py @@ -17,7 +17,7 @@ PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] # limit members for API inclusion __all__ = ("CmdBoot", "CmdBan", "CmdUnban", - "CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall") + "CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall", "CmdForce") class CmdBoot(COMMAND_DEFAULT_CLASS): @@ -513,3 +513,33 @@ class CmdWall(COMMAND_DEFAULT_CLASS): message = "%s shouts \"%s\"" % (self.caller.name, self.args) self.msg("Announcing to all connected sessions ...") SESSIONS.announce_all(message) + + +class CmdForce(COMMAND_DEFAULT_CLASS): + """ + forces an object to execute a command + + Usage: + @force = + + 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)) diff --git a/evennia/commands/default/cmdset_character.py b/evennia/commands/default/cmdset_character.py index cfc8a30ca4..438996f536 100644 --- a/evennia/commands/default/cmdset_character.py +++ b/evennia/commands/default/cmdset_character.py @@ -57,6 +57,7 @@ class CharacterCmdSet(CmdSet): self.add(admin.CmdEmit()) self.add(admin.CmdPerm()) self.add(admin.CmdWall()) + self.add(admin.CmdForce()) # Building and world manipulation self.add(building.CmdTeleport()) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 19277c168a..b0f5211c6f 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -243,6 +243,9 @@ class TestAdmin(CommandTest): def test_ban(self): 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): From b5e87409de137d9c6fe1f0f4877d7ecc7c9e72aa Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 21 Oct 2018 15:45:17 +0200 Subject: [PATCH 24/24] Update devel changelog with latest mergers --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d00135dbf3..8a31aecb97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ - 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)