From e1f289b08f0bcd754951c11a1c72ff5078f2b011 Mon Sep 17 00:00:00 2001 From: Cal Date: Wed, 20 Mar 2024 19:45:53 -0600 Subject: [PATCH 001/112] achievements contrib --- .../game_systems/achievements/README.md | 119 ++++++ .../game_systems/achievements/__init__.py | 0 .../game_systems/achievements/achievements.py | 370 ++++++++++++++++++ .../game_systems/achievements/tests.py | 177 +++++++++ 4 files changed, 666 insertions(+) create mode 100644 evennia/contrib/game_systems/achievements/README.md create mode 100644 evennia/contrib/game_systems/achievements/__init__.py create mode 100644 evennia/contrib/game_systems/achievements/achievements.py create mode 100644 evennia/contrib/game_systems/achievements/tests.py diff --git a/evennia/contrib/game_systems/achievements/README.md b/evennia/contrib/game_systems/achievements/README.md new file mode 100644 index 0000000000..d60d8061ce --- /dev/null +++ b/evennia/contrib/game_systems/achievements/README.md @@ -0,0 +1,119 @@ +# Achievements + +A simple, but reasonably comprehensive, system for tracking achievements. Achievements are defined using ordinary Python dicts, reminiscent of the core prototypes system, and while it's expected you'll use it only on Characters or Accounts, they can be tracked for any typeclassed object. + +## Installation + +Once you've defined your achievement dicts in a module, assign that module to the `ACHIEVEMENT_CONTRIB_MODULES` setting in your settings.py + +Optionally, you can specify what attribute achievement progress is stored in, with the setting `ACHIEVEMENT_CONTRIB_ATTRIBUTE`. By default it's just "achievements". + +There is also a command available to let players check their achievements - `from evennia.contrib.game_systems.achievements.achievements import CmdAchieve` and then add `CmdAchieve` to your default Character and/or Account cmdsets. + +#### Settings examples + +One module: + +```python +# in server/conf/settings.py + +ACHIEVEMENT_CONTRIB_MODULES = "world.achievements" +``` + +Multiple modules, with a custom-defined attribute: + +```python +# in server/conf/settings.py + +ACHIEVEMENT_CONTRIB_MODULES = ["world.crafting.achievements", "world.mobs.achievements"] +ACHIEVEMENT_CONTRIB_ATTRIBUTE = "achieve_progress" +``` + +A custom-defined attribute including category: + +```python +# in server/conf/settings.py + +ACHIEVEMENT_CONTRIB_MODULES = "world.achievements" +ACHIEVEMENT_CONTRIB_ATTRIBUTE = ("achievements", "systems") +``` + +## Usage + +### Defining your achievements + +This achievement system is designed to use ordinary dicts for the achievement data - however, there are certain keys which, if present in the dict, define how the achievement is progressed or completed. + +- **name** (str): The searchable name for the achievement. Doesn't need to be unique. +- **desc** (str): A longer description of the achievement. Common uses for this would be flavor text or hints on how to complete it. +- **category** (str): The type of actions this achievement tracks. e.g. purchasing 10 apples might have a category of "buy", or killing 10 rats might have a category of "defeat". +- **tracking** (str or list): The *specific* things this achievement tracks. e.g. the previous example of killing rats might have a `"tracking"` value of `"rat"`. An achievement can also track multiple things: instead of only tracking buying apples, you might want to track apples and pears. For that situation, you can assign it to a list of values to check against: e.g. `["apple", "pear"]` +- **tracking_type** (str): *Default value if unset: `"sum"`* There are two valid tracking types: "sum" (which is the default) and "separate". `"sum"` will increment a single counter every time any of the tracked items match. `"separate"` will have a counter for each individual item in the tracked items. (This is really only useful when `"tracking"` is a list.) +- **count** (int): *Default value if unset: 1* The number of tallies this achievement's requirements need to build up in order to complete the achievement. e.g. the previous example of killing rats would have a `"count"` value of `10`. For achievements using the "separate" tracking type, *each* item being tracked must tally up to this number to be completed +- **prereqs** (str or list): The *keys* of any achievements which must be completed before this achievement can start tracking progress. An achievement's key is the variable name it's assigned to in your achievement module. + +You can add any additional keys to your achievement dicts that you want, and they'll be included with all the rest of the achievement data when using the contrib's functions. This could be useful if you want to add extra metadata to your achievements for your own features. + +#### Examples + +A simple achievement which you can get just for logging in the first time. This achievement has no prerequisites and it only needs to be fulfilled once to complete, so it doesn't need to define most of the keys. +```python +# This achievement has the unique key of "FIRST_LOGIN_ACHIEVE" +FIRST_LOGIN_ACHIEVE = { + "name": "Welcome!", # the searchable, player-friendly display name + "desc": "We're glad to have you here.", # the longer description + "category": "login", # the type of action this tracks + "tracking": "first", # the specific login action +} +``` + +An achievement for killing 10 rats, and another for killing 10 *dire* rats which requires the "kill 10 rats" achievement to be completed first. +```python +ACHIEVE_TEN_RATS = { + "name": "The Usual", + "desc": "Why do all these inns have rat problems?", + "category": "defeat", + "tracking": "rat", + "count": 10, +} + +ACHIEVE_DIRE_RATS = { + "name": "Once More, But Bigger", + "desc": "Somehow, normal rats just aren't enough any more.", + "category": "defeat", + "tracking": "dire rat", + "count": 10, + "prereqs": "ACHIEVE_TEN_RATS", +} +``` + +An achievement for buying 5 each of apples, oranges, and pears. +```python +FRUIT_BASKET_ACHIEVEMENT = { + "name": "A Fan of Fruit", # note, there is no desc here - that's allowed! + "category": "buy", + "tracking": ("apple", "orange", "pear"), + "count": 5, + "tracking_type": "separate", +} +``` + +### `track_achievements` + +The primary mechanism for using the achievements system is the `track_achievements` function. In any actions or functions in your game's mechanics which you might want to track in an achievement, add a call to `track_achievements` to update the achievement progress for that individual. + +For example, you might have a collection achievement for buying 10 apples, and a general `buy` command players could use. In your `buy` command, after the purchase is completed, you could add the following line: + +```python + track_achievements(self.caller, "buy", obj.name, count=quantity) +``` + +In this case, `obj` is the fruit that was just purchased, and `quantity` is the amount they bought. + +The `track_achievements` function does also return a value: an iterable of keys for any achievements which were newly completed by that update. You can ignore this value, or you can use it to e.g. send a message to the player with their latest achievements. + +### The `achievements` command + +The contrib's provided command, `CmdAchieve`, aims to be usable as-is, with multiple switches to filter achievements by various progress statuses and the ability to search by achievement names. + +To make it easier to integrate into your own particular game (e.g. accommodating some of that extra achievement data you might have added), the code for formatting a particular achievement's data for display is in `CmdAchieve.format_achievement`, making it easy to overload for your custom display styling without reimplementing the whole command. diff --git a/evennia/contrib/game_systems/achievements/__init__.py b/evennia/contrib/game_systems/achievements/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/evennia/contrib/game_systems/achievements/achievements.py b/evennia/contrib/game_systems/achievements/achievements.py new file mode 100644 index 0000000000..7f0319d0fb --- /dev/null +++ b/evennia/contrib/game_systems/achievements/achievements.py @@ -0,0 +1,370 @@ +""" +Achievements + +This provides a system for adding and tracking player achievements in your game. + +Achievements are defined as dicts, loosely similar to the prototypes system. + +An example of an achievement dict: + + EXAMPLE_ACHIEVEMENT = { + "name": "Some Achievement", + "desc": "This is not a real achievement.", + "category": "crafting", + "tracking": "box", + "count": 5, + "prereqs": "ANOTHER_ACHIEVEMENT", + } + +The recognized fields for an achievement are: + +- name (str): The name of the achievement. This is not the key and does not need to be unique. +- desc (str): The longer description of the achievement. Common uses for this would be flavor text + or hints on how to complete it. +- category (str): The type of things this achievement tracks. e.g. visiting 10 locations might have + a category of "post move", or killing 10 rats might have a category of "defeat". +- tracking (str or list): The *specific* thing this achievement tracks. e.g. the above example of + 10 rats, the tracking field would be "rat". +- tracking_type: The options here are "sum" and "separate". "sum" means that matching any tracked + item will increase the total. "separate" means all tracked items are counted individually. + This is only useful when tracking is a list. The default is "sum". +- count (int): The total tallies the tracked item needs for this to be completed. e.g. for the rats + example, it would be 10. The default is 1 +- prereqs (str or list): An optional achievement key or list of keys that must be completed before + this achievement is available. + +To add achievement tracking, put `track_achievements` in your relevant hooks. + +Example: + + def at_use(self, user, **kwargs): + # track this use for any achievements about using an object named our name + finished_achievements = track_achievements(user, category="use", tracking=self.key) + +Despite the example, it's likely to be more useful to reference a tag than the object's key. +""" + +from collections import Counter +from django.conf import settings +from evennia.utils import logger +from evennia.utils.utils import all_from_module, is_iter, make_iter, string_partial_matching +from evennia.utils.evmore import EvMore +from evennia.commands.default.muxcommand import MuxCommand + +# this is either a string of the attribute name, or a tuple of strings of the attribute name and category +_ACHIEVEMENT_ATTR = make_iter(getattr(settings, "ACHIEVEMENT_ATTRIBUTE", "achievements")) + +_ACHIEVEMENT_INFO = None + + +def _load_achievements(): + """ + Loads the achievement data from settings, if it hasn't already been loaded. + + Returns: + achievements (dict) - the loaded achievement info + """ + global _ACHIEVEMENT_INFO + if _ACHIEVEMENT_INFO is None: + _ACHIEVEMENT_INFO = {} + if modules := getattr(settings, "ACHIEVEMENT_MODULES", None): + for module_path in make_iter(modules): + _ACHIEVEMENT_INFO |= { + key.lower(): val + for key, val in all_from_module(module_path).items() + if isinstance(val, dict) + } + else: + logger.log_warn("No achievement modules have been added to settings.") + return _ACHIEVEMENT_INFO + + +def track_achievements(achiever, category=None, tracking=None, count=1, **kwargs): + """ + Update and check achievement progress. + + Args: + achiever (Account or Character): The entity that's collecting achievement progress. + + Keyword args: + category (str or None): The category of an achievement. + tracking (str or None): The specific item being tracked in the achievement. + + Returns: + completed (tuple): The keys of any achievements that were completed by this update. + """ + if not (all_achievements := _load_achievements()): + # there are no achievements available, there's nothing to do + return tuple() + + # split out the achievement attribute info + attr_key = _ACHIEVEMENT_ATTR[0] + attr_cat = _ACHIEVEMENT_ATTR[1] if len(_ACHIEVEMENT_ATTR) > 1 else None + + # get the achiever's progress data, and detach from the db so we only read/write once + if progress_data := achiever.attributes.get(attr_key, default={}, category=attr_cat): + progress_data = progress_data.deserialize() + + # filter all of the achievements down to the relevant ones + relevant_achievements = ( + (key, val) + for key, val in all_achievements.items() + if (not category or category in make_iter(val["category"])) # filter by category + and (not tracking or tracking in make_iter(val["tracking"])) # filter by tracked item + and not progress_data.get(key, {}).get("completed") # filter by completion status + and all( + progress_data.get(prereq, {}).get("completed") + for prereq in make_iter(val.get("prereqs", [])) + ) # filter by prereqs + ) + + completed = [] + # loop through all the relevant achievements and update the progress data + for achieve_key, achieve_data in relevant_achievements: + if target_count := achieve_data.get("count"): + # check if we need to track things individually or not + separate_totals = achieve_data.get("tracking_type", "sum") == "separate" + if achieve_key not in progress_data: + progress_data[achieve_key] = {} + if separate_totals and is_iter(achieve_data["tracking"]): + # do the special handling for tallying totals separately + i = achieve_data["tracking"].index(tracking) + if "progress" not in progress_data[achieve_key]: + # initialize the item counts + progress_data[achieve_key]["progress"] = [ + 0 for _ in range(len(achieve_data["tracking"])) + ] + # increment the matching index count + progress_data[achieve_key]["progress"][i] += count + # have we reached the target on all items? if so, we've completed it + if min(progress_data[achieve_key]["progress"]) >= target_count: + completed.append(achieve_key) + else: + progress_count = progress_data[achieve_key].get("progress", 0) + # update the achievement data + progress_data[achieve_key]["progress"] = progress_count + count + # have we reached the target? if so, we've completed it + if progress_data[achieve_key]["progress"] >= target_count: + completed.append(achieve_key) + else: + # no count means you just need to do the thing to complete it + completed.append(achieve_key) + + for key in completed: + if key not in progress_data: + progress_data[key] = {} + progress_data[key]["completed"] = True + + # write the updated progress back to the achievement attribute + achiever.attributes.add(attr_key, progress_data, category=attr_cat) + + # return all the achievements we just completed + return tuple(completed) + + +def get_achievement(key): + """ + Get an achievement by its key. + + Args: + key (str): The achievement key. This is the variable name the achievement dict is assigned to. + + Returns: + dict or None: The achievement data, or None if it doesn't exist + """ + if not (all_achievements := _load_achievements()): + # there are no achievements available, there's nothing to do + return None + if data := all_achievements.get(key): + return dict(data) + return None + + +def all_achievements(): + """ + Returns a dict of all achievements in the game. + """ + # we do this to prevent accidental in-memory modification of reference data + return dict((key, dict(val)) for key, val in _load_achievements().items()) + + +def get_progress(achiever, key): + """ + Retrieve the progress data on a particular achievement for a particular achiever. + + Args: + achiever (Account or Character): The entity tracking achievement progress. + key (str): The achievement key + + Returns: + data (dict): The progress data + """ + # split out the achievement attribute info + attr_key = _ACHIEVEMENT_ATTR[0] + attr_cat = _ACHIEVEMENT_ATTR[1] if len(_ACHIEVEMENT_ATTR) > 1 else None + if progress_data := achiever.attributes.get(attr_key, default={}, category=attr_cat): + # detach the data from the db to avoid data corruption and return the data + return progress_data.deserialize().get(key, {}) + else: + # just return an empty dict + return {} + + +def search_achievement(search_term): + """ + Search for an achievement by name. + + Args: + search_term (str): The string to search for. + + Returns: + results (dict): A dict of key:data pairs of matching achievements. + """ + if not (all_achievements := _load_achievements()): + # there are no achievements available, there's nothing to do + return {} + keys, names = zip(*((key, val["name"]) for key, val in all_achievements.items())) + indices = string_partial_matching(names, search_term) + + return dict((keys[i], dict(all_achievements[keys[i]])) for i in indices) + + +class CmdAchieve(MuxCommand): + """ + view achievements + + Usage: + achievements[/switches] [args] + + Switches: + all View all achievements, including locked ones. + completed View achievements you've completed. + progress View achievements you have partially completed + + Check your achievement statuses or browse the list. Providing a command argument + will search all your currently unlocked achievements for matches, and the switches + will filter the list to something other than "all unlocked". Combining a command + argument with a switch will search only in that list. + + Examples: + achievements apples + achievements/all + achievements/progress rats + """ + + key = "achievements" + aliases = ( + "achievement", + "achieve", + ) + switch_options = ("progress", "completed", "done", "all") + + def format_achievement(self, achievement_data): + """ + Formats the raw achievement data for display. + + Args: + achievement_data (dict): The data to format. + + Returns + str: The display string to be sent to the caller. + + """ + template = """\ +|w{name}|n +{desc} +{status} +""".rstrip() + + if achievement_data.get("completed"): + # it's done! + status = "|gCompleted!|n" + elif not achievement_data.get("progress"): + status = "|yNot Started|n" + else: + count = achievement_data.get("count") + # is this achievement tracking items separately? + if is_iter(achievement_data["progress"]): + # we'll display progress as how many items have been completed + completed = Counter(val >= count for val in achievement_data["progress"])[True] + pct = (completed * 100) // count + else: + # we display progress as the percent of the total count + pct = (achievement_data["progress"] * 100) // count + status = f"{pct}% complete" + + return template.format( + name=achievement_data.get("name", ""), + desc=achievement_data.get("desc", ""), + status=status, + ) + + def func(self): + if self.args: + # we're doing a name lookup + if not (achievements := search_achievement(self.args.strip())): + self.msg(f"Could not find any achievements matching '{self.args.strip()}'.") + return + else: + # we're checking against all achievements + if not (achievements := all_achievements()): + self.msg("There are no achievements in this game.") + return + + # split out the achievement attribute info + attr_key = _ACHIEVEMENT_ATTR[0] + attr_cat = _ACHIEVEMENT_ATTR[1] if len(_ACHIEVEMENT_ATTR) > 1 else None + + # get the achiever's progress data, and detach from the db so we only read once + if progress_data := self.caller.attributes.get(attr_key, default={}, category=attr_cat): + progress_data = progress_data.deserialize() + # if the caller is not an account, we get their account progress too + if self.caller != self.account: + if account_progress := self.account.attributes.get( + attr_key, default={}, category=attr_cat + ): + progress_data |= account_progress.deserialize() + + # go through switch options + # we only show achievements that are in progress + if "progress" in self.switches: + # we filter our data to incomplete achievements, and combine the base achievement data into it + achievement_data = { + key: achievements[key] | data + for key, data in progress_data.items() + if not data.get("completed") + } + + # we only show achievements that are completed + elif "completed" in self.switches or "done" in self.switches: + # we filter our data to finished achievements, and combine the base achievement data into it + achievement_data = { + key: achievements[key] | data + for key, data in progress_data.items() + if data.get("completed") + } + + # we show ALL achievements + elif "all" in self.switches: + # we merge our progress data into the full dict of achievements + achievement_data = achievements | progress_data + + # we show all of the currently available achievements regardless of progress status + else: + achievement_data = { + key: data + for key, data in achievements.items() + if all( + progress_data.get(prereq, {}).get("completed") + for prereq in make_iter(data.get("prereqs", [])) + ) + } | progress_data + + if not achievement_data: + self.msg("There are no matching achievements.") + return + + achievement_str = "\n".join( + self.format_achievement(data) for _, data in achievement_data.items() + ) + EvMore(self.caller, achievement_str) diff --git a/evennia/contrib/game_systems/achievements/tests.py b/evennia/contrib/game_systems/achievements/tests.py new file mode 100644 index 0000000000..61d7cc6d78 --- /dev/null +++ b/evennia/contrib/game_systems/achievements/tests.py @@ -0,0 +1,177 @@ +from evennia.utils.test_resources import BaseEvenniaTest, BaseEvenniaCommandTest +from mock import patch +from . import achievements + +_dummy_achievements = { + "ACHIEVE_ONE": { + "name": "First Achievement", + "desc": "A first achievement for first achievers.", + "category": "login", + }, + "COUNTING_ACHIEVE": { + "name": "The Count", + "desc": "One, two, three! Three counters! Ah ah ah!", + "category": "get", + "tracking": "thing", + "count": 3, + }, + "COUNTING_TWO": { + "name": "Son of the Count", + "desc": "Four, five, six! Six counters!", + "category": "get", + "tracking": "thing", + "count": 3, + "prereqs": "COUNTING_ACHIEVE", + }, + "SEPARATE_ITEMS": { + "name": "Apples and Pears", + "desc": "Get some apples and some pears.", + "category": "get", + "tracking": ("apple", "pear"), + "tracking_type": "separate", + "count": 3, + }, +} + + +def _dummy_achieve_loader(): + """returns predefined achievement data for testing instead of loading""" + return _dummy_achievements + + +class TestAchievements(BaseEvenniaTest): + @patch( + "evennia.contrib.game_systems.achievements.achievements._load_achievements", + _dummy_achieve_loader, + ) + def test_completion(self): + """no defined count means a single match completes it""" + self.assertIn( + "ACHIEVE_ONE", + achievements.track_achievements(self.char1, category="login", track="first"), + ) + + @patch( + "evennia.contrib.game_systems.achievements.achievements._load_achievements", + _dummy_achieve_loader, + ) + def test_counter_progress(self): + """progressing a counter should update the achiever""" + # this should not complete any achievements; verify it returns the right empty result + self.assertEqual(achievements.track_achievements(self.char1, "get", "thing"), tuple()) + # first, verify that the data is created + self.assertTrue(self.char1.attributes.has("achievements")) + self.assertEqual(self.char1.db.achievements["COUNTING_ACHIEVE"]["progress"], 1) + # verify that it gets updated + achievements.track_achievements(self.char1, "get", "thing") + self.assertEqual(self.char1.db.achievements["COUNTING_ACHIEVE"]["progress"], 2) + + # also verify that `get_progress` returns the correct data + self.assertEqual(achievements.get_progress(self.char1, "COUNTING_ACHIEVE"), {"progress": 2}) + + @patch( + "evennia.contrib.game_systems.achievements.achievements._load_achievements", + _dummy_achieve_loader, + ) + def test_prereqs(self): + """verify progress is not counted on achievements with unmet prerequisites""" + achievements.track_achievements(self.char1, "get", "thing") + # this should mark progress on COUNTING_ACHIEVE, but NOT on COUNTING_TWO + self.assertEqual(achievements.get_progress(self.char1, "COUNTING_ACHIEVE"), {"progress": 1}) + self.assertEqual(achievements.get_progress(self.char1, "COUNTING_TWO"), {}) + + # now we complete COUNTING_ACHIEVE... + self.assertIn( + "COUNTING_ACHIEVE", achievements.track_achievements(self.char1, "get", "thing", count=2) + ) + # and track again to progress COUNTING_TWO + achievements.track_achievements(self.char1, "get", "thing") + self.assertEqual(achievements.get_progress(self.char1, "COUNTING_TWO"), {"progress": 1}) + + @patch( + "evennia.contrib.game_systems.achievements.achievements._load_achievements", + _dummy_achieve_loader, + ) + def test_separate_tracking(self): + """achievements with 'tracking_type': 'separate' should count progress for each item""" + # getting one item only increments that one item + achievements.track_achievements(self.char1, "get", "apple") + progress = achievements.get_progress(self.char1, "SEPARATE_ITEMS") + self.assertEqual(progress["progress"], [1, 0]) + # the other item then increments that item + achievements.track_achievements(self.char1, "get", "pear") + progress = achievements.get_progress(self.char1, "SEPARATE_ITEMS") + self.assertEqual(progress["progress"], [1, 1]) + # completing one does not complete the achievement + self.assertEqual( + achievements.track_achievements(self.char1, "get", "apple", count=2), tuple() + ) + # completing the second as well DOES complete the achievement + self.assertIn( + "SEPARATE_ITEMS", achievements.track_achievements(self.char1, "get", "pear", count=2) + ) + + @patch( + "evennia.contrib.game_systems.achievements.achievements._load_achievements", + _dummy_achieve_loader, + ) + def test_search_achievement(self): + """searching for achievements by name""" + results = achievements.search_achievement("count") + self.assertEqual(["COUNTING_ACHIEVE", "COUNTING_TWO"], list(results.keys())) + + +class TestAchieveCommand(BaseEvenniaCommandTest): + @patch( + "evennia.contrib.game_systems.achievements.achievements._load_achievements", + _dummy_achieve_loader, + ) + def test_switches(self): + # print only achievements that have no prereqs + expected_output = "\n".join( + f"{data['name']}\n{data['desc']}\nNot Started" + for key, data in _dummy_achievements.items() + if not data.get("prereqs") + ) + self.call(achievements.CmdAchieve(), "", expected_output) + # print all achievements + expected_output = "\n".join( + f"{data['name']}\n{data['desc']}\nNot Started" + for key, data in _dummy_achievements.items() + ) + self.call(achievements.CmdAchieve(), "/all", expected_output) + # these should both be empty + self.call(achievements.CmdAchieve(), "/progress", "There are no matching achievements.") + self.call(achievements.CmdAchieve(), "/done", "There are no matching achievements.") + # update one and complete one, then verify they show up correctly + achievements.track_achievements(self.char1, "login") + achievements.track_achievements(self.char1, "get", "thing") + self.call( + achievements.CmdAchieve(), + "/progress", + "The Count\nOne, two, three! Three counters! Ah ah ah!\n33% complete", + ) + self.call( + achievements.CmdAchieve(), + "/done", + "First Achievement\nA first achievement for first achievers.\nCompleted!", + ) + + @patch( + "evennia.contrib.game_systems.achievements.achievements._load_achievements", + _dummy_achieve_loader, + ) + def test_search(self): + # by default, only returns matching items that are trackable + self.call( + achievements.CmdAchieve(), + " count", + "The Count\nOne, two, three! Three counters! Ah ah ah!\nNot Started", + ) + # with switches, returns matching items from the switch set + self.call( + achievements.CmdAchieve(), + "/all count", + "The Count\nOne, two, three! Three counters! Ah ah ah!\nNot Started\n" + + "Son of the Count\nFour, five, six! Six counters!\nNot Started", + ) From be9ad0278aab8c33e2f0d90427e0647f854c7a07 Mon Sep 17 00:00:00 2001 From: Cal Date: Thu, 21 Mar 2024 00:13:47 -0600 Subject: [PATCH 002/112] minor fixes --- .../game_systems/achievements/achievements.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/evennia/contrib/game_systems/achievements/achievements.py b/evennia/contrib/game_systems/achievements/achievements.py index 7f0319d0fb..6383be62a4 100644 --- a/evennia/contrib/game_systems/achievements/achievements.py +++ b/evennia/contrib/game_systems/achievements/achievements.py @@ -52,7 +52,7 @@ from evennia.utils.evmore import EvMore from evennia.commands.default.muxcommand import MuxCommand # this is either a string of the attribute name, or a tuple of strings of the attribute name and category -_ACHIEVEMENT_ATTR = make_iter(getattr(settings, "ACHIEVEMENT_ATTRIBUTE", "achievements")) +_ACHIEVEMENT_ATTR = make_iter(getattr(settings, "ACHIEVEMENT_CONTRIB_ATTRIBUTE", "achievements")) _ACHIEVEMENT_INFO = None @@ -67,12 +67,12 @@ def _load_achievements(): global _ACHIEVEMENT_INFO if _ACHIEVEMENT_INFO is None: _ACHIEVEMENT_INFO = {} - if modules := getattr(settings, "ACHIEVEMENT_MODULES", None): + if modules := getattr(settings, "ACHIEVEMENT_CONTRIB_MODULES", None): for module_path in make_iter(modules): _ACHIEVEMENT_INFO |= { - key.lower(): val + key: val for key, val in all_from_module(module_path).items() - if isinstance(val, dict) + if isinstance(val, dict) and not key.startswith('_') } else: logger.log_warn("No achievement modules have been added to settings.") @@ -109,8 +109,8 @@ def track_achievements(achiever, category=None, tracking=None, count=1, **kwargs relevant_achievements = ( (key, val) for key, val in all_achievements.items() - if (not category or category in make_iter(val["category"])) # filter by category - and (not tracking or tracking in make_iter(val["tracking"])) # filter by tracked item + if (not category or category in make_iter(val.get("category",[]))) # filter by category + and (not tracking or not val.get("tracking") or tracking in make_iter(val.get("tracking",[]))) # filter by tracked item and not progress_data.get(key, {}).get("completed") # filter by completion status and all( progress_data.get(prereq, {}).get("completed") @@ -126,7 +126,7 @@ def track_achievements(achiever, category=None, tracking=None, count=1, **kwargs separate_totals = achieve_data.get("tracking_type", "sum") == "separate" if achieve_key not in progress_data: progress_data[achieve_key] = {} - if separate_totals and is_iter(achieve_data["tracking"]): + if separate_totals and is_iter(achieve_data.get("tracking")): # do the special handling for tallying totals separately i = achieve_data["tracking"].index(tracking) if "progress" not in progress_data[achieve_key]: @@ -352,13 +352,13 @@ class CmdAchieve(MuxCommand): # we show all of the currently available achievements regardless of progress status else: achievement_data = { - key: data + key: data | progress_data.get(key, {}) for key, data in achievements.items() if all( progress_data.get(prereq, {}).get("completed") for prereq in make_iter(data.get("prereqs", [])) ) - } | progress_data + } if not achievement_data: self.msg("There are no matching achievements.") From 30151b7e1f0a7d4fc652eb24a6fccfa88ad2a9be Mon Sep 17 00:00:00 2001 From: Cal Date: Fri, 29 Mar 2024 16:46:45 -0600 Subject: [PATCH 003/112] applying changes from feedback --- .../game_systems/achievements/README.md | 133 +++++++++----- .../game_systems/achievements/__init__.py | 8 + .../game_systems/achievements/achievements.py | 162 ++++++++++-------- 3 files changed, 191 insertions(+), 112 deletions(-) diff --git a/evennia/contrib/game_systems/achievements/README.md b/evennia/contrib/game_systems/achievements/README.md index d60d8061ce..06233c207e 100644 --- a/evennia/contrib/game_systems/achievements/README.md +++ b/evennia/contrib/game_systems/achievements/README.md @@ -2,63 +2,26 @@ A simple, but reasonably comprehensive, system for tracking achievements. Achievements are defined using ordinary Python dicts, reminiscent of the core prototypes system, and while it's expected you'll use it only on Characters or Accounts, they can be tracked for any typeclassed object. -## Installation - -Once you've defined your achievement dicts in a module, assign that module to the `ACHIEVEMENT_CONTRIB_MODULES` setting in your settings.py - -Optionally, you can specify what attribute achievement progress is stored in, with the setting `ACHIEVEMENT_CONTRIB_ATTRIBUTE`. By default it's just "achievements". - -There is also a command available to let players check their achievements - `from evennia.contrib.game_systems.achievements.achievements import CmdAchieve` and then add `CmdAchieve` to your default Character and/or Account cmdsets. - -#### Settings examples - -One module: - -```python -# in server/conf/settings.py - -ACHIEVEMENT_CONTRIB_MODULES = "world.achievements" -``` - -Multiple modules, with a custom-defined attribute: - -```python -# in server/conf/settings.py - -ACHIEVEMENT_CONTRIB_MODULES = ["world.crafting.achievements", "world.mobs.achievements"] -ACHIEVEMENT_CONTRIB_ATTRIBUTE = "achieve_progress" -``` - -A custom-defined attribute including category: - -```python -# in server/conf/settings.py - -ACHIEVEMENT_CONTRIB_MODULES = "world.achievements" -ACHIEVEMENT_CONTRIB_ATTRIBUTE = ("achievements", "systems") -``` - -## Usage - -### Defining your achievements +## Creating achievements This achievement system is designed to use ordinary dicts for the achievement data - however, there are certain keys which, if present in the dict, define how the achievement is progressed or completed. +- **key** (str): *Default value if unset: the variable name.* The unique, case-insensitive key identifying this achievement. - **name** (str): The searchable name for the achievement. Doesn't need to be unique. - **desc** (str): A longer description of the achievement. Common uses for this would be flavor text or hints on how to complete it. -- **category** (str): The type of actions this achievement tracks. e.g. purchasing 10 apples might have a category of "buy", or killing 10 rats might have a category of "defeat". -- **tracking** (str or list): The *specific* things this achievement tracks. e.g. the previous example of killing rats might have a `"tracking"` value of `"rat"`. An achievement can also track multiple things: instead of only tracking buying apples, you might want to track apples and pears. For that situation, you can assign it to a list of values to check against: e.g. `["apple", "pear"]` +- **category** (str): The category of conditions which this achievement tracks. It will most likely be an action and you will most likely specify it based on where you're checking from. e.g. killing 10 rats might have a category of "defeat", which you'd then check from your code that runs when a player defeats something. +- **tracking** (str or list): The *specific* condition this achievement tracks. e.g. the previous example of killing rats might have a `"tracking"` value of `"rat"`. This value will most likely be taken from a specific object in your code, like a tag on the defeated object, or the ID of a visited location. An achievement can also track multiple things: instead of only tracking buying apples, you might want to track apples and pears. For that situation, you can assign it to a list of values to check against: e.g. `["apple", "pear"]` - **tracking_type** (str): *Default value if unset: `"sum"`* There are two valid tracking types: "sum" (which is the default) and "separate". `"sum"` will increment a single counter every time any of the tracked items match. `"separate"` will have a counter for each individual item in the tracked items. (This is really only useful when `"tracking"` is a list.) - **count** (int): *Default value if unset: 1* The number of tallies this achievement's requirements need to build up in order to complete the achievement. e.g. the previous example of killing rats would have a `"count"` value of `10`. For achievements using the "separate" tracking type, *each* item being tracked must tally up to this number to be completed - **prereqs** (str or list): The *keys* of any achievements which must be completed before this achievement can start tracking progress. An achievement's key is the variable name it's assigned to in your achievement module. You can add any additional keys to your achievement dicts that you want, and they'll be included with all the rest of the achievement data when using the contrib's functions. This could be useful if you want to add extra metadata to your achievements for your own features. -#### Examples +### Example achievements -A simple achievement which you can get just for logging in the first time. This achievement has no prerequisites and it only needs to be fulfilled once to complete, so it doesn't need to define most of the keys. +A simple achievement which you can get just for logging in the first time. This achievement has no prerequisites and it only needs to be fulfilled once to complete, so it doesn't need to define most of the fields. ```python -# This achievement has the unique key of "FIRST_LOGIN_ACHIEVE" +# This achievement has the unique key of "first_login_achieve" FIRST_LOGIN_ACHIEVE = { "name": "Welcome!", # the searchable, player-friendly display name "desc": "We're glad to have you here.", # the longer description @@ -69,7 +32,9 @@ FIRST_LOGIN_ACHIEVE = { An achievement for killing 10 rats, and another for killing 10 *dire* rats which requires the "kill 10 rats" achievement to be completed first. ```python +# This achievement has the unique key of "ten_rats" instead of "achieve_ten_rats" ACHIEVE_TEN_RATS = { + "key": "ten_rats", "name": "The Usual", "desc": "Why do all these inns have rat problems?", "category": "defeat", @@ -98,6 +63,31 @@ FRUIT_BASKET_ACHIEVEMENT = { } ``` + +## Installation + +Once you've defined your achievement dicts in one or more module, assign that module to the `ACHIEVEMENT_CONTRIB_MODULES` setting in your settings.py + +> Note: If any achievements have the same unique key, whichever conflicting achievement is processed *last* will be the only one loaded into the game. Case is ignored, so "ten_rats", "Ten_Rats" and "TEN_RATS" will conflict. "ten_rats" and "ten rats" will not. + +```python +# in server/conf/settings.py + +ACHIEVEMENT_CONTRIB_MODULES = ["world.achievements"] +``` + +There is also a command available to let players check their achievements - `from evennia.contrib.game_systems.achievements.achievements import CmdAchieve` and then add `CmdAchieve` to your default Character and/or Account cmdsets. + +**Optional** - The achievements contrib stores individual progress data on the `achievements` attribute by default, visible via `obj.db.attributes`. You can change this in your settings if necessary, e.g.: + +```py +# in settings.py + +ACHIEVEMENT_CONTRIB_ATTRIBUTE = ("achievement_data", "systems") +``` + +## Usage + ### `track_achievements` The primary mechanism for using the achievements system is the `track_achievements` function. In any actions or functions in your game's mechanics which you might want to track in an achievement, add a call to `track_achievements` to update the achievement progress for that individual. @@ -105,15 +95,68 @@ The primary mechanism for using the achievements system is the `track_achievemen For example, you might have a collection achievement for buying 10 apples, and a general `buy` command players could use. In your `buy` command, after the purchase is completed, you could add the following line: ```python - track_achievements(self.caller, "buy", obj.name, count=quantity) + from contrib.game_systems.achievements import track_achievements + track_achievements(self.caller, category="buy", tracking=obj.name, count=quantity) ``` In this case, `obj` is the fruit that was just purchased, and `quantity` is the amount they bought. The `track_achievements` function does also return a value: an iterable of keys for any achievements which were newly completed by that update. You can ignore this value, or you can use it to e.g. send a message to the player with their latest achievements. +### `search_achievement` + +A utility function for searching achievements by name or description. It handles partial matching and returns a dictionary of matching achievements. The provided `achievement` command for in-game uses this function to find matching achievements from user inputs. + +#### Example: +```py +>>> from evennia.contrib.game_systems.achievements import search_achievement +>>> search_achievement("fruit") +{'fruit_basket_achievement': {'name': 'A Fan of Fruit', 'category': 'buy', 'tracking': ('apple', 'orange', 'pear'), 'count': 5, 'tracking_type': 'separate'}} +>>> search_achievement("rat") +{'ten_rats': {'key': 'ten_rats', 'name': 'The Usual', 'desc': 'Why do all these inns have rat problems?', 'category': 'defeat', 'tracking': 'rat', 'count': 10}, {'achieve_dire_rats': {'name': 'Once More, But Bigger', 'desc': 'Somehow, normal rats just aren't enough any more.', 'category': 'defeat', 'tracking': 'dire rat', 'count': 10, 'prereqs': "ACHIEVE_TEN_RATS"}} +``` + +### `get_achievement` + +A utility function for retrieving a specific achievement's data from the achievement's unique key. It cannot be used for searching, but if you already have an achievement's key - for example, from the results of `track_achievements` - you can retrieve the rest of its data this way. + +#### Example: + +```py +from evennia.contrib.game_systems.achievements import get_achievement + +def toast(achiever, completed_list): + if completed_list: + # we've completed some achievements! + completed_data = [get_achievement(key) for key in args] + names = [data.get('name') for data in completed] + achiever.msg(f"|wAchievement Get!|n {iter_to_str(name for name in names if name)}")) +``` + ### The `achievements` command The contrib's provided command, `CmdAchieve`, aims to be usable as-is, with multiple switches to filter achievements by various progress statuses and the ability to search by achievement names. To make it easier to integrate into your own particular game (e.g. accommodating some of that extra achievement data you might have added), the code for formatting a particular achievement's data for display is in `CmdAchieve.format_achievement`, making it easy to overload for your custom display styling without reimplementing the whole command. + +#### Example output + +``` +> achievements +The Usual +Why do all these inns have rat problems? +70% complete +A Fan of Fruit + +Not Started +``` +``` +> achievements/progress +The Usual +Why do all these inns have rat problems? +70% complete +``` +``` +> achievements/done +There are no matching achievements. +``` diff --git a/evennia/contrib/game_systems/achievements/__init__.py b/evennia/contrib/game_systems/achievements/__init__.py index e69de29bb2..15d57e7025 100644 --- a/evennia/contrib/game_systems/achievements/__init__.py +++ b/evennia/contrib/game_systems/achievements/__init__.py @@ -0,0 +1,8 @@ +from .achievements import ( + get_achievement, + search_achievement, + all_achievements, + track_achievements, + get_achievement_progress, + CmdAchieve, +) diff --git a/evennia/contrib/game_systems/achievements/achievements.py b/evennia/contrib/game_systems/achievements/achievements.py index 6383be62a4..f6ec9b0a8e 100644 --- a/evennia/contrib/game_systems/achievements/achievements.py +++ b/evennia/contrib/game_systems/achievements/achievements.py @@ -38,7 +38,7 @@ To add achievement tracking, put `track_achievements` in your relevant hooks. Example: def at_use(self, user, **kwargs): - # track this use for any achievements about using an object named our name + # track this use for any achievements that are categorized as "use" and are tracking something that matches our key finished_achievements = track_achievements(user, category="use", tracking=self.key) Despite the example, it's likely to be more useful to reference a tag than the object's key. @@ -53,30 +53,61 @@ from evennia.commands.default.muxcommand import MuxCommand # this is either a string of the attribute name, or a tuple of strings of the attribute name and category _ACHIEVEMENT_ATTR = make_iter(getattr(settings, "ACHIEVEMENT_CONTRIB_ATTRIBUTE", "achievements")) +_ATTR_KEY = _ACHIEVEMENT_ATTR[0] +_ATTR_CAT = _ACHIEVEMENT_ATTR[1] if len(_ACHIEVEMENT_ATTR) > 1 else None -_ACHIEVEMENT_INFO = None +# load the achievements data +_ACHIEVEMENT_DATA = {} +if modules := getattr(settings, "ACHIEVEMENT_CONTRIB_MODULES", None): + for module_path in make_iter(modules): + module_achieves = { + val.key("key", key).lower(): val + for key, val in all_from_module(module_path).items() + if isinstance(val, dict) and not key.startswith("_") + } + if any(key in _ACHIEVEMENT_DATA for key in module_achieves.keys()): + logger.log_warn( + "There are conflicting achievement keys! Only the last achievement registered to the key will be recognized." + ) + _ACHIEVEMENT_DATA |= module_achieves +else: + logger.log_warn("No achievement modules have been added to settings.") -def _load_achievements(): +def _read_player_data(achiever): """ - Loads the achievement data from settings, if it hasn't already been loaded. + helper function to get a player's achievement data from the database. + + Args: + achiever (Object or Account): The achieving entity Returns: - achievements (dict) - the loaded achievement info + dict: The deserialized achievement data. """ - global _ACHIEVEMENT_INFO - if _ACHIEVEMENT_INFO is None: - _ACHIEVEMENT_INFO = {} - if modules := getattr(settings, "ACHIEVEMENT_CONTRIB_MODULES", None): - for module_path in make_iter(modules): - _ACHIEVEMENT_INFO |= { - key: val - for key, val in all_from_module(module_path).items() - if isinstance(val, dict) and not key.startswith('_') - } - else: - logger.log_warn("No achievement modules have been added to settings.") - return _ACHIEVEMENT_INFO + if data := achiever.attributes.get(_ATTR_KEY, default={}, category=_ATTR_CAT): + # detach the data from the db + data.deserialize() + # return the data + return data + + +def _write_player_data(achiever, data): + """ + helper function to write a player's achievement data to the database. + + Args: + achiever (Object or Account): The achieving entity + data (dict): The full achievement data for this entity. + + Returns: + None + + Notes: + This function will overwrite any existing achievement data for the entity. + """ + if not isinstance(data, dict): + raise ValueError("Achievement data must be a dict.") + achiever.attributes.add(_ATTR_KEY, data, category=_ATTR_CAT) def track_achievements(achiever, category=None, tracking=None, count=1, **kwargs): @@ -91,26 +122,25 @@ def track_achievements(achiever, category=None, tracking=None, count=1, **kwargs tracking (str or None): The specific item being tracked in the achievement. Returns: - completed (tuple): The keys of any achievements that were completed by this update. + tuple: The keys of any achievements that were completed by this update. """ - if not (all_achievements := _load_achievements()): + if not _ACHIEVEMENT_DATA: # there are no achievements available, there's nothing to do return tuple() - # split out the achievement attribute info - attr_key = _ACHIEVEMENT_ATTR[0] - attr_cat = _ACHIEVEMENT_ATTR[1] if len(_ACHIEVEMENT_ATTR) > 1 else None - - # get the achiever's progress data, and detach from the db so we only read/write once - if progress_data := achiever.attributes.get(attr_key, default={}, category=attr_cat): - progress_data = progress_data.deserialize() + # get the achiever's progress data + progress_data = _read_player_data(achiever) # filter all of the achievements down to the relevant ones relevant_achievements = ( (key, val) - for key, val in all_achievements.items() - if (not category or category in make_iter(val.get("category",[]))) # filter by category - and (not tracking or not val.get("tracking") or tracking in make_iter(val.get("tracking",[]))) # filter by tracked item + for key, val in _ACHIEVEMENT_DATA.items() + if (not category or category in make_iter(val.get("category", []))) # filter by category + and ( + not tracking + or not val.get("tracking") + or tracking in make_iter(val.get("tracking", [])) + ) # filter by tracked item and not progress_data.get(key, {}).get("completed") # filter by completion status and all( progress_data.get(prereq, {}).get("completed") @@ -121,7 +151,7 @@ def track_achievements(achiever, category=None, tracking=None, count=1, **kwargs completed = [] # loop through all the relevant achievements and update the progress data for achieve_key, achieve_data in relevant_achievements: - if target_count := achieve_data.get("count"): + if target_count := achieve_data.get("count", 1): # check if we need to track things individually or not separate_totals = achieve_data.get("tracking_type", "sum") == "separate" if achieve_key not in progress_data: @@ -156,7 +186,7 @@ def track_achievements(achiever, category=None, tracking=None, count=1, **kwargs progress_data[key]["completed"] = True # write the updated progress back to the achievement attribute - achiever.attributes.add(attr_key, progress_data, category=attr_cat) + _write_player_data(achiever, progress_data) # return all the achievements we just completed return tuple(completed) @@ -172,10 +202,10 @@ def get_achievement(key): Returns: dict or None: The achievement data, or None if it doesn't exist """ - if not (all_achievements := _load_achievements()): + if not _ACHIEVEMENT_DATA: # there are no achievements available, there's nothing to do return None - if data := all_achievements.get(key): + if data := _ACHIEVEMENT_DATA.get(key.lower()): return dict(data) return None @@ -183,12 +213,15 @@ def get_achievement(key): def all_achievements(): """ Returns a dict of all achievements in the game. + + Returns: + dict """ - # we do this to prevent accidental in-memory modification of reference data - return dict((key, dict(val)) for key, val in _load_achievements().items()) + # we do this to mitigate accidental in-memory modification of reference data + return dict((key, dict(val)) for key, val in _ACHIEVEMENT_DATA.items()) -def get_progress(achiever, key): +def get_achievement_progress(achiever, key): """ Retrieve the progress data on a particular achievement for a particular achiever. @@ -197,14 +230,11 @@ def get_progress(achiever, key): key (str): The achievement key Returns: - data (dict): The progress data + dict: The progress data """ - # split out the achievement attribute info - attr_key = _ACHIEVEMENT_ATTR[0] - attr_cat = _ACHIEVEMENT_ATTR[1] if len(_ACHIEVEMENT_ATTR) > 1 else None - if progress_data := achiever.attributes.get(attr_key, default={}, category=attr_cat): - # detach the data from the db to avoid data corruption and return the data - return progress_data.deserialize().get(key, {}) + if progress_data := _read_player_data(achiever): + # get the specific key's data + return progress_data.get(key, {}) else: # just return an empty dict return {} @@ -212,21 +242,26 @@ def get_progress(achiever, key): def search_achievement(search_term): """ - Search for an achievement by name. + Search for an achievement containing the search term. If no matches are found in the achievement names, it searches + in the achievement descriptions. Args: search_term (str): The string to search for. Returns: - results (dict): A dict of key:data pairs of matching achievements. + dict: A dict of key:data pairs of matching achievements. """ - if not (all_achievements := _load_achievements()): + if not _ACHIEVEMENT_DATA: # there are no achievements available, there's nothing to do return {} - keys, names = zip(*((key, val["name"]) for key, val in all_achievements.items())) + keys, names, descs = zip( + *((key, val["name"], val["desc"]) for key, val in _ACHIEVEMENT_DATA.items()) + ) indices = string_partial_matching(names, search_term) + if not indices: + indices = string_partial_matching(descs, search_term) - return dict((keys[i], dict(all_achievements[keys[i]])) for i in indices) + return dict((keys[i], dict(_ACHIEVEMENT_DATA[keys[i]])) for i in indices) class CmdAchieve(MuxCommand): @@ -256,9 +291,16 @@ class CmdAchieve(MuxCommand): aliases = ( "achievement", "achieve", + "achieves", ) switch_options = ("progress", "completed", "done", "all") + template = """\ +|w{name}|n +{desc} +{status} +""".rstrip() + def format_achievement(self, achievement_data): """ Formats the raw achievement data for display. @@ -270,11 +312,6 @@ class CmdAchieve(MuxCommand): str: The display string to be sent to the caller. """ - template = """\ -|w{name}|n -{desc} -{status} -""".rstrip() if achievement_data.get("completed"): # it's done! @@ -293,7 +330,7 @@ class CmdAchieve(MuxCommand): pct = (achievement_data["progress"] * 100) // count status = f"{pct}% complete" - return template.format( + return self.template.format( name=achievement_data.get("name", ""), desc=achievement_data.get("desc", ""), status=status, @@ -311,19 +348,10 @@ class CmdAchieve(MuxCommand): self.msg("There are no achievements in this game.") return - # split out the achievement attribute info - attr_key = _ACHIEVEMENT_ATTR[0] - attr_cat = _ACHIEVEMENT_ATTR[1] if len(_ACHIEVEMENT_ATTR) > 1 else None - - # get the achiever's progress data, and detach from the db so we only read once - if progress_data := self.caller.attributes.get(attr_key, default={}, category=attr_cat): - progress_data = progress_data.deserialize() - # if the caller is not an account, we get their account progress too + # get the achiever's progress data + progress_data = _read_player_data(self.caller) if self.caller != self.account: - if account_progress := self.account.attributes.get( - attr_key, default={}, category=attr_cat - ): - progress_data |= account_progress.deserialize() + progress_data |= _read_player_data(self.account) # go through switch options # we only show achievements that are in progress From 758ffb80c8e29f89f375fd48484eb53b788c238a Mon Sep 17 00:00:00 2001 From: Cal Date: Fri, 29 Mar 2024 17:02:54 -0600 Subject: [PATCH 004/112] update achieve tests --- .../game_systems/achievements/tests.py | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/evennia/contrib/game_systems/achievements/tests.py b/evennia/contrib/game_systems/achievements/tests.py index 61d7cc6d78..bcff5c3464 100644 --- a/evennia/contrib/game_systems/achievements/tests.py +++ b/evennia/contrib/game_systems/achievements/tests.py @@ -34,15 +34,10 @@ _dummy_achievements = { } -def _dummy_achieve_loader(): - """returns predefined achievement data for testing instead of loading""" - return _dummy_achievements - - class TestAchievements(BaseEvenniaTest): @patch( - "evennia.contrib.game_systems.achievements.achievements._load_achievements", - _dummy_achieve_loader, + "evennia.contrib.game_systems.achievements.achievements._ACHIEVEMENT_DATA", + _dummy_achievements, ) def test_completion(self): """no defined count means a single match completes it""" @@ -52,8 +47,8 @@ class TestAchievements(BaseEvenniaTest): ) @patch( - "evennia.contrib.game_systems.achievements.achievements._load_achievements", - _dummy_achieve_loader, + "evennia.contrib.game_systems.achievements.achievements._ACHIEVEMENT_DATA", + _dummy_achievements, ) def test_counter_progress(self): """progressing a counter should update the achiever""" @@ -66,19 +61,19 @@ class TestAchievements(BaseEvenniaTest): achievements.track_achievements(self.char1, "get", "thing") self.assertEqual(self.char1.db.achievements["COUNTING_ACHIEVE"]["progress"], 2) - # also verify that `get_progress` returns the correct data - self.assertEqual(achievements.get_progress(self.char1, "COUNTING_ACHIEVE"), {"progress": 2}) + # also verify that `get_achievement_progress` returns the correct data + self.assertEqual(achievements.get_achievement_progress(self.char1, "COUNTING_ACHIEVE"), {"progress": 2}) @patch( - "evennia.contrib.game_systems.achievements.achievements._load_achievements", - _dummy_achieve_loader, + "evennia.contrib.game_systems.achievements.achievements._ACHIEVEMENT_DATA", + _dummy_achievements, ) def test_prereqs(self): """verify progress is not counted on achievements with unmet prerequisites""" achievements.track_achievements(self.char1, "get", "thing") # this should mark progress on COUNTING_ACHIEVE, but NOT on COUNTING_TWO - self.assertEqual(achievements.get_progress(self.char1, "COUNTING_ACHIEVE"), {"progress": 1}) - self.assertEqual(achievements.get_progress(self.char1, "COUNTING_TWO"), {}) + self.assertEqual(achievements.get_achievement_progress(self.char1, "COUNTING_ACHIEVE"), {"progress": 1}) + self.assertEqual(achievements.get_achievement_progress(self.char1, "COUNTING_TWO"), {}) # now we complete COUNTING_ACHIEVE... self.assertIn( @@ -86,21 +81,21 @@ class TestAchievements(BaseEvenniaTest): ) # and track again to progress COUNTING_TWO achievements.track_achievements(self.char1, "get", "thing") - self.assertEqual(achievements.get_progress(self.char1, "COUNTING_TWO"), {"progress": 1}) + self.assertEqual(achievements.get_achievement_progress(self.char1, "COUNTING_TWO"), {"progress": 1}) @patch( - "evennia.contrib.game_systems.achievements.achievements._load_achievements", - _dummy_achieve_loader, + "evennia.contrib.game_systems.achievements.achievements._ACHIEVEMENT_DATA", + _dummy_achievements, ) def test_separate_tracking(self): """achievements with 'tracking_type': 'separate' should count progress for each item""" # getting one item only increments that one item achievements.track_achievements(self.char1, "get", "apple") - progress = achievements.get_progress(self.char1, "SEPARATE_ITEMS") + progress = achievements.get_achievement_progress(self.char1, "SEPARATE_ITEMS") self.assertEqual(progress["progress"], [1, 0]) # the other item then increments that item achievements.track_achievements(self.char1, "get", "pear") - progress = achievements.get_progress(self.char1, "SEPARATE_ITEMS") + progress = achievements.get_achievement_progress(self.char1, "SEPARATE_ITEMS") self.assertEqual(progress["progress"], [1, 1]) # completing one does not complete the achievement self.assertEqual( @@ -112,8 +107,8 @@ class TestAchievements(BaseEvenniaTest): ) @patch( - "evennia.contrib.game_systems.achievements.achievements._load_achievements", - _dummy_achieve_loader, + "evennia.contrib.game_systems.achievements.achievements._ACHIEVEMENT_DATA", + _dummy_achievements, ) def test_search_achievement(self): """searching for achievements by name""" @@ -123,8 +118,8 @@ class TestAchievements(BaseEvenniaTest): class TestAchieveCommand(BaseEvenniaCommandTest): @patch( - "evennia.contrib.game_systems.achievements.achievements._load_achievements", - _dummy_achieve_loader, + "evennia.contrib.game_systems.achievements.achievements._ACHIEVEMENT_DATA", + _dummy_achievements, ) def test_switches(self): # print only achievements that have no prereqs @@ -158,8 +153,8 @@ class TestAchieveCommand(BaseEvenniaCommandTest): ) @patch( - "evennia.contrib.game_systems.achievements.achievements._load_achievements", - _dummy_achieve_loader, + "evennia.contrib.game_systems.achievements.achievements._ACHIEVEMENT_DATA", + _dummy_achievements, ) def test_search(self): # by default, only returns matching items that are trackable From b480847582301ba66f355a98ef14786031549cde Mon Sep 17 00:00:00 2001 From: Cal Date: Fri, 29 Mar 2024 17:53:27 -0600 Subject: [PATCH 005/112] fix achievement docs and code --- .../game_systems/achievements/README.md | 20 ++++++++--- .../game_systems/achievements/achievements.py | 35 +++++++++++-------- .../game_systems/achievements/tests.py | 12 +++++-- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/evennia/contrib/game_systems/achievements/README.md b/evennia/contrib/game_systems/achievements/README.md index 06233c207e..a406dcd6f0 100644 --- a/evennia/contrib/game_systems/achievements/README.md +++ b/evennia/contrib/game_systems/achievements/README.md @@ -2,6 +2,8 @@ A simple, but reasonably comprehensive, system for tracking achievements. Achievements are defined using ordinary Python dicts, reminiscent of the core prototypes system, and while it's expected you'll use it only on Characters or Accounts, they can be tracked for any typeclassed object. +The contrib provides several functions for tracking and accessing achievements, as well as a basic in-game command for viewing achievement status. + ## Creating achievements This achievement system is designed to use ordinary dicts for the achievement data - however, there are certain keys which, if present in the dict, define how the achievement is progressed or completed. @@ -92,14 +94,24 @@ ACHIEVEMENT_CONTRIB_ATTRIBUTE = ("achievement_data", "systems") The primary mechanism for using the achievements system is the `track_achievements` function. In any actions or functions in your game's mechanics which you might want to track in an achievement, add a call to `track_achievements` to update the achievement progress for that individual. -For example, you might have a collection achievement for buying 10 apples, and a general `buy` command players could use. In your `buy` command, after the purchase is completed, you could add the following line: +Using the "kill 10 rats" example achievement from earlier, you might have some code that triggers when a character is defeated: for the sake of example, we'll pretend we have an `at_defeated` method on the base Object class that gets called when the Object is defeated. + +Adding achievement tracking to it could then look something like this: ```python - from contrib.game_systems.achievements import track_achievements - track_achievements(self.caller, category="buy", tracking=obj.name, count=quantity) +from contrib.game_systems.achievements import track_achievements + +class Object(ObjectParent, DefaultObject): + # .... + + def at_defeated(self, victor): + """called when this object is defeated in combat""" + # we'll use the "mob_type" tag category as the tracked information for achievements + mob_type = self.tags.get(category="mob_type") + track_achievements(victor, category="defeated", tracking=mob_type, count=1) ``` -In this case, `obj` is the fruit that was just purchased, and `quantity` is the amount they bought. +If a player defeats something tagged `rat` with a tag category of `mob_type`, it'd now count towards the rat-killing achievement. The `track_achievements` function does also return a value: an iterable of keys for any achievements which were newly completed by that update. You can ignore this value, or you can use it to e.g. send a message to the player with their latest achievements. diff --git a/evennia/contrib/game_systems/achievements/achievements.py b/evennia/contrib/game_systems/achievements/achievements.py index f6ec9b0a8e..8de7868589 100644 --- a/evennia/contrib/game_systems/achievements/achievements.py +++ b/evennia/contrib/game_systems/achievements/achievements.py @@ -7,23 +7,27 @@ Achievements are defined as dicts, loosely similar to the prototypes system. An example of an achievement dict: - EXAMPLE_ACHIEVEMENT = { - "name": "Some Achievement", - "desc": "This is not a real achievement.", - "category": "crafting", - "tracking": "box", - "count": 5, - "prereqs": "ANOTHER_ACHIEVEMENT", + ACHIEVE_DIRE_RATS = { + "name": "Once More, But Bigger", + "desc": "Somehow, normal rats just aren't enough any more.", + "category": "defeat", + "tracking": "dire rat", + "count": 10, + "prereqs": "ACHIEVE_TEN_RATS", } The recognized fields for an achievement are: +- key (str): The unique, case-insensitive key identifying this achievement. The variable name is + used by default. - name (str): The name of the achievement. This is not the key and does not need to be unique. - desc (str): The longer description of the achievement. Common uses for this would be flavor text or hints on how to complete it. -- category (str): The type of things this achievement tracks. e.g. visiting 10 locations might have - a category of "post move", or killing 10 rats might have a category of "defeat". -- tracking (str or list): The *specific* thing this achievement tracks. e.g. the above example of +- category (str): The category of conditions which this achievement tracks. It will most likely be + an action and you will most likely specify it based on where you're checking from. + e.g. killing 10 rats might have a category of "defeat", which you'd then check from your code + that runs when a player defeats something. +- tracking (str or list): The specific condition this achievement tracks. e.g. for the above example of 10 rats, the tracking field would be "rat". - tracking_type: The options here are "sum" and "separate". "sum" means that matching any tracked item will increase the total. "separate" means all tracked items are counted individually. @@ -37,11 +41,12 @@ To add achievement tracking, put `track_achievements` in your relevant hooks. Example: - def at_use(self, user, **kwargs): - # track this use for any achievements that are categorized as "use" and are tracking something that matches our key - finished_achievements = track_achievements(user, category="use", tracking=self.key) + def at_defeated(self, victor): + # called when this object is defeated in combat + # we'll use the "mob_type" tag category as the tracked information for achievements + mob_type = self.tags.get(category="mob_type") + track_achievements(victor, category="defeated", tracking=mob_type, count=1) -Despite the example, it's likely to be more useful to reference a tag than the object's key. """ from collections import Counter @@ -86,7 +91,7 @@ def _read_player_data(achiever): """ if data := achiever.attributes.get(_ATTR_KEY, default={}, category=_ATTR_CAT): # detach the data from the db - data.deserialize() + data = data.deserialize() # return the data return data diff --git a/evennia/contrib/game_systems/achievements/tests.py b/evennia/contrib/game_systems/achievements/tests.py index bcff5c3464..b552972af3 100644 --- a/evennia/contrib/game_systems/achievements/tests.py +++ b/evennia/contrib/game_systems/achievements/tests.py @@ -62,7 +62,9 @@ class TestAchievements(BaseEvenniaTest): self.assertEqual(self.char1.db.achievements["COUNTING_ACHIEVE"]["progress"], 2) # also verify that `get_achievement_progress` returns the correct data - self.assertEqual(achievements.get_achievement_progress(self.char1, "COUNTING_ACHIEVE"), {"progress": 2}) + self.assertEqual( + achievements.get_achievement_progress(self.char1, "COUNTING_ACHIEVE"), {"progress": 2} + ) @patch( "evennia.contrib.game_systems.achievements.achievements._ACHIEVEMENT_DATA", @@ -72,7 +74,9 @@ class TestAchievements(BaseEvenniaTest): """verify progress is not counted on achievements with unmet prerequisites""" achievements.track_achievements(self.char1, "get", "thing") # this should mark progress on COUNTING_ACHIEVE, but NOT on COUNTING_TWO - self.assertEqual(achievements.get_achievement_progress(self.char1, "COUNTING_ACHIEVE"), {"progress": 1}) + self.assertEqual( + achievements.get_achievement_progress(self.char1, "COUNTING_ACHIEVE"), {"progress": 1} + ) self.assertEqual(achievements.get_achievement_progress(self.char1, "COUNTING_TWO"), {}) # now we complete COUNTING_ACHIEVE... @@ -81,7 +85,9 @@ class TestAchievements(BaseEvenniaTest): ) # and track again to progress COUNTING_TWO achievements.track_achievements(self.char1, "get", "thing") - self.assertEqual(achievements.get_achievement_progress(self.char1, "COUNTING_TWO"), {"progress": 1}) + self.assertEqual( + achievements.get_achievement_progress(self.char1, "COUNTING_TWO"), {"progress": 1} + ) @patch( "evennia.contrib.game_systems.achievements.achievements._ACHIEVEMENT_DATA", From d8a0ebc226b3fe3c035fdb37bbe48443d3019386 Mon Sep 17 00:00:00 2001 From: Cal Date: Fri, 29 Mar 2024 23:04:15 -0600 Subject: [PATCH 006/112] typo fixes --- .../game_systems/achievements/achievements.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/evennia/contrib/game_systems/achievements/achievements.py b/evennia/contrib/game_systems/achievements/achievements.py index 8de7868589..f0cd189e12 100644 --- a/evennia/contrib/game_systems/achievements/achievements.py +++ b/evennia/contrib/game_systems/achievements/achievements.py @@ -66,7 +66,7 @@ _ACHIEVEMENT_DATA = {} if modules := getattr(settings, "ACHIEVEMENT_CONTRIB_MODULES", None): for module_path in make_iter(modules): module_achieves = { - val.key("key", key).lower(): val + val.get("key", key).lower(): val for key, val in all_from_module(module_path).items() if isinstance(val, dict) and not key.startswith("_") } @@ -93,7 +93,7 @@ def _read_player_data(achiever): # detach the data from the db data = data.deserialize() # return the data - return data + return data or {} def _write_player_data(achiever, data): @@ -110,8 +110,6 @@ def _write_player_data(achiever, data): Notes: This function will overwrite any existing achievement data for the entity. """ - if not isinstance(data, dict): - raise ValueError("Achievement data must be a dict.") achiever.attributes.add(_ATTR_KEY, data, category=_ATTR_CAT) @@ -324,12 +322,12 @@ class CmdAchieve(MuxCommand): elif not achievement_data.get("progress"): status = "|yNot Started|n" else: - count = achievement_data.get("count") + count = achievement_data.get("count",1) # is this achievement tracking items separately? if is_iter(achievement_data["progress"]): # we'll display progress as how many items have been completed completed = Counter(val >= count for val in achievement_data["progress"])[True] - pct = (completed * 100) // count + pct = (completed * 100) // len(achievement_data['progress']) else: # we display progress as the percent of the total count pct = (achievement_data["progress"] * 100) // count @@ -380,7 +378,10 @@ class CmdAchieve(MuxCommand): # we show ALL achievements elif "all" in self.switches: # we merge our progress data into the full dict of achievements - achievement_data = achievements | progress_data + achievement_data = { + key: data | progress_data.get(key, {}) + for key, data in achievements.items() + } # we show all of the currently available achievements regardless of progress status else: From ac5e186e5a2eed3fecbe06acae34aa3ad2616b3e Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Tue, 2 Apr 2024 15:23:27 +1100 Subject: [PATCH 007/112] Add exit_order kwarg to get_display_exits to sort exit names --- evennia/objects/objects.py | 24 +++++++++++++++++++++++- evennia/objects/tests.py | 16 ++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index f11b43bc51..b17ba11938 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1572,12 +1572,34 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Args: looker (DefaultObject): Object doing the looking. **kwargs: Arbitrary data for use when overriding. + + Keyword Args: + exit_order (iterable of str): The order in which exits should be listed, with + unspecified exits appearing at the end, alphabetically. + Returns: str: The exits display data. + Examples: + :: + + For a room with exits in the order 'portal', 'south', 'north', and 'out': + obj.get_display_name(looker, exit_order=('north', 'south')) + -> "Exits: north, south, out, and portal." (ANSI codes not shown here) """ + def _sort_exit_names(names): + exit_order = kwargs.get("exit_order") + if not exit_order: + return names + sort_index = {name: key for key, name in enumerate(exit_order)} + names = sorted(names) + end_pos = len(names) + 1 + names.sort(key=lambda name:sort_index.get(name, end_pos)) + return names + exits = self.filter_visible(self.contents_get(content_type="exit"), looker, **kwargs) - exit_names = iter_to_str(exi.get_display_name(looker, **kwargs) for exi in exits) + exit_names = (exi.get_display_name(looker, **kwargs) for exi in exits) + exit_names = iter_to_str(_sort_exit_names(exit_names)) return f"|wExits:|n {exit_names}" if exit_names else "" diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index de23269cdf..e7587b36e0 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -10,6 +10,7 @@ from evennia.typeclasses.tags import ( TagProperty, ) from evennia.utils import create, search +from evennia.utils.ansi import strip_ansi from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase @@ -94,6 +95,21 @@ class DefaultObjectTest(BaseEvenniaTest): all_return_exit = ex1.get_return_exit(return_all=True) self.assertEqual(len(all_return_exit), 2) + def test_exit_order(self): + DefaultExit.create("south", self.room1, self.room2, account=self.account) + DefaultExit.create("portal", self.room1, self.room2, account=self.account) + DefaultExit.create("north", self.room1, self.room2, account=self.account) + DefaultExit.create("aperture", self.room1, self.room2, account=self.account) + + # in creation order + exits = strip_ansi(self.room1.get_display_exits(self.char1)) + self.assertEqual(exits, "Exits: out, south, portal, north, and aperture") + + # in specified order with unspecified exits alpbabetically on the end + exit_order = ('north', 'south', 'out') + exits = strip_ansi(self.room1.get_display_exits(self.char1, exit_order=exit_order)) + self.assertEqual(exits, "Exits: north, south, out, aperture, and portal") + def test_urls(self): "Make sure objects are returning URLs" self.assertTrue(self.char1.get_absolute_url()) From 9a825c7b45b326f18a7f5576c09071a697dff300 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Wed, 3 Apr 2024 17:32:14 +1100 Subject: [PATCH 008/112] Change "ANSI codes" to "markup" in get_display_exits usage example --- evennia/objects/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index b17ba11938..8bfa8aa2cf 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1585,7 +1585,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): For a room with exits in the order 'portal', 'south', 'north', and 'out': obj.get_display_name(looker, exit_order=('north', 'south')) - -> "Exits: north, south, out, and portal." (ANSI codes not shown here) + -> "Exits: north, south, out, and portal." (markup not shown here) """ def _sort_exit_names(names): exit_order = kwargs.get("exit_order") From 6dd9442cef2fe59eb5ac2f53b4a5b18729ba6b4d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 6 Apr 2024 23:16:41 +0200 Subject: [PATCH 009/112] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b603ec36ca..f92cdab5ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Main branch + +- [Feature][pull3470]: New `exit_order` kwarg to + `DefaultObject.get_display_exits` to easier customize the order in which + standard exits are displayed in a room (chiizujin) + +[pull3470]: https://github.com/evennia/evennia/pull/3470 + ## Evennia 4.1.1 April 6, 2024 From a552bf6fd41a032cbd0301365a3d2ca3cbf38371 Mon Sep 17 00:00:00 2001 From: mike Date: Sat, 6 Apr 2024 16:03:55 -0700 Subject: [PATCH 010/112] Added xterm truecolor support and tests. --- evennia/utils/ansi.py | 20 +++-- evennia/utils/hex_colors.py | 145 +++++++++++++++++++++++++++++++ evennia/utils/tests/test_ansi.py | 125 +++++++++++++++++++++++++- 3 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 evennia/utils/hex_colors.py diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index 2cc03f4aec..5d95c402ed 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -69,6 +69,9 @@ from django.conf import settings from evennia.utils import logger, utils from evennia.utils.utils import to_str +from evennia.utils.hex_colors import HexToTruecolor + +hex_sub = HexToTruecolor.hex_sub MXP_ENABLED = settings.MXP_ENABLED @@ -431,7 +434,7 @@ class ANSIParser(object): """ return self.unsafe_tokens.sub("", string) - def parse_ansi(self, string, strip_ansi=False, xterm256=False, mxp=False): + def parse_ansi(self, string, strip_ansi=False, xterm256=False, mxp=False, truecolor=False): """ Parses a string, subbing color codes according to the stored mapping. @@ -458,13 +461,18 @@ class ANSIParser(object): # check cached parsings global _PARSE_CACHE - cachekey = "%s-%s-%s-%s" % (string, strip_ansi, xterm256, mxp) + cachekey = f"{string}-{strip_ansi}-{xterm256}-{mxp}-{truecolor}" + if cachekey in _PARSE_CACHE: return _PARSE_CACHE[cachekey] # pre-convert bright colors to xterm256 color tags string = self.brightbg_sub.sub(self.sub_brightbg, string) + def do_truecolor(part: re.Match, truecolor=truecolor): + hex2truecolor = HexToTruecolor() + return hex2truecolor.sub_truecolor(part, truecolor) + def do_xterm256_fg(part): return self.sub_xterm256(part, xterm256, "fg") @@ -483,7 +491,8 @@ class ANSIParser(object): parsed_string = [] parts = self.ansi_escapes.split(in_string) + [" "] for part, sep in zip(parts[::2], parts[1::2]): - pstring = self.xterm256_fg_sub.sub(do_xterm256_fg, part) + pstring = hex_sub.sub(do_truecolor, part) + pstring = self.xterm256_fg_sub.sub(do_xterm256_fg, pstring) pstring = self.xterm256_bg_sub.sub(do_xterm256_bg, pstring) pstring = self.xterm256_gfg_sub.sub(do_xterm256_gfg, pstring) pstring = self.xterm256_gbg_sub.sub(do_xterm256_gbg, pstring) @@ -515,11 +524,12 @@ ANSI_PARSER = ANSIParser() # -def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False, mxp=False): +def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False, mxp=False, truecolor=False): """ Parses a string, subbing color codes as needed. Args: + truecolor: string (str): The string to parse. strip_ansi (bool, optional): Strip all ANSI sequences. parser (ansi.AnsiParser, optional): A parser instance to use. @@ -531,7 +541,7 @@ def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False, mxp """ string = string or "" - return parser.parse_ansi(string, strip_ansi=strip_ansi, xterm256=xterm256, mxp=mxp) + return parser.parse_ansi(string, strip_ansi=strip_ansi, xterm256=xterm256, mxp=mxp, truecolor=truecolor) def strip_ansi(string, parser=ANSI_PARSER): diff --git a/evennia/utils/hex_colors.py b/evennia/utils/hex_colors.py new file mode 100644 index 0000000000..4118c179c3 --- /dev/null +++ b/evennia/utils/hex_colors.py @@ -0,0 +1,145 @@ +import re + + +class HexToTruecolor: + """ + This houses a method for converting hex codes to xterm truecolor codes + or falls back to evennia xterm256 codes to be handled by sub_xterm256 + + Based on code from @InspectorCaracal + """ + + _RE_FG = '\|#' + _RE_BG = '\|\[#' + _RE_FG_OR_BG = '\|\[?#' + _RE_HEX_LONG = '[0-9a-fA-F]{6}' + _RE_HEX_SHORT = '[0-9a-fA-F]{3}' + _RE_BYTE = '[0-2][0-9][0-9]' + _RE_3_BYTES = f'({_RE_BYTE})({_RE_BYTE})({_RE_BYTE})' + + # Used in hex_sub + _RE_HEX_PATTERN = f'({_RE_FG_OR_BG})({_RE_HEX_LONG}|{_RE_HEX_SHORT})' + + # Used for truecolor_sub + _RE_24_BIT_RGB_FG = f'{_RE_FG}{_RE_3_BYTES}' + _RE_24_BIT_RGB_BG = f'{_RE_BG}{_RE_3_BYTES}' + + # Used for greyscale + _GREYS = "abcdefghijklmnopqrstuvwxyz" + + # Our matchers for use with ANSIParser and ANSIString + hex_sub = re.compile(rf'{_RE_HEX_PATTERN}', re.DOTALL) + + def sub_truecolor(self, match: re.Match, truecolor=False) -> str: + """ + Converts a hex string to xterm truecolor code, greyscale, or + falls back to evennia xterm256 to be handled by sub_xterm256 + + Args: + match (re.match): first group is the leading indicator, + second is the tag + truecolor (bool): return xterm truecolor or fallback + + Returns: + Newly formatted indicator and tag (str) + + """ + indicator, tag = match.groups() + + # Remove the # sign + indicator = indicator.replace('#', '') + + r, g, b = self._hex_to_rgb_24_bit(tag) + + # Is it greyscale? + if r == g and g == b: + return f"{indicator}=" + self._GREYS[self._grey_int(r)] + + else: + if not truecolor: + # Fallback to xterm256 syntax + r, g, b = self._rgb_24_bit_to_256(r, g, b) + return f"{indicator}{r}{g}{b}" + + else: + xtag = f"\033[" + if '[' in indicator: + # Background Color + xtag += '4' + + else: + xtag += '3' + + xtag += f"8;2;{r};{g};{b}m" + return xtag + + def _split_hex_to_bytes(self, tag: str) -> tuple[str, str, str]: + """ + Splits hex string into separate bytes: + #00FF00 -> ('00', 'FF', '00') + #CF3 -> ('CC', 'FF', '33') + + Args: + tag (str): the tag to convert + + Returns: + str: the text with converted tags + """ + strip_leading = re.compile(rf'{self._RE_FG_OR_BG}') + tag = strip_leading.sub("", tag) + + if len(tag) == 6: + # 6 digits + r, g, b = (tag[i:i + 2] for i in range(0, 6, 2)) + + else: + # 3 digits + r, g, b = (tag[i:i + 1] * 2 for i in range(0, 3, 1)) + + return r, g, b + + def _grey_int(self, num: int) -> int: + """ + Returns a grey greyscale integer + + Returns: + + """ + return round(max((int(num) - 8), 0) / 10) + + def _hue_int(self, num: int) -> int: + return round(max((int(num) - 45), 0) / 40) + + def _hex_to_rgb_24_bit(self, hex_code: str) -> tuple[int, int, int]: + """ + Converts a hex color code (#000 or #000000) into + a 3-int tuple (0, 255, 90) + + Args: + hex_code (str): HTML hex color code + + Returns: + 24-bit rgb tuple: (int, int, int) + """ + # Strip the leading indicator if present + hex_code = re.sub(rf'{self._RE_FG_OR_BG}', '', hex_code) + + r, g, b = self._split_hex_to_bytes(hex_code) + + return int(r, 16), int(g, 16), int(b, 16) + + def _rgb_24_bit_to_256(self, r: int, g: int, b: int) -> tuple[int, int, int]: + """ + converts 0-255 hex color codes to 0-5 + + Args: + r (int): red + g (int): green + b (int): blue + + Returns: + 256 color rgb tuple: (int, int, int) + + """ + + return self._hue_int(r), self._hue_int(g), self._hue_int(b) diff --git a/evennia/utils/tests/test_ansi.py b/evennia/utils/tests/test_ansi.py index 4ff9d468c6..0ecc8d1b04 100644 --- a/evennia/utils/tests/test_ansi.py +++ b/evennia/utils/tests/test_ansi.py @@ -8,7 +8,9 @@ Test of the ANSI parsing and ANSIStrings. from django.test import TestCase -from evennia.utils.ansi import ANSIString as AN +from evennia.utils.ansi import ANSIString as AN, ANSIParser + +parser = ANSIParser().parse_ansi class TestANSIString(TestCase): @@ -52,3 +54,124 @@ class TestANSIString(TestCase): self.assertEqual(split2, split3, "Split 2 and 3 differ") self.assertEqual(split1, split2, "Split 1 and 2 differ") self.assertEqual(split1, split3, "Split 1 and 3 differ") + +# TODO: Better greyscale testing + +class TestANSIStringHex(TestCase): + """ + Tests the conversion of html hex colors + to xterm-style colors + """ + def setUp(self): + self.str = 'test ' + self.output1 = '\x1b[38;5;16mtest \x1b[0m' + self.output2 = '\x1b[48;5;16mtest \x1b[0m' + self.output3 = '\x1b[38;5;46mtest \x1b[0m' + self.output4 = '\x1b[48;5;46mtest \x1b[0m' + + def test_long_grayscale_fg(self): + raw = f'|#000000{self.str}|n' + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output1, "Output") + + def test_long_grayscale_bg(self): + raw = f'|[#000000{self.str}|n' + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output2, "Output") + + def test_short_grayscale_fg(self): + raw = f'|#000{self.str}|n' + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output1, "Output") + + def test_short_grayscale_bg(self): + raw = f'|[#000{self.str}|n' + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output2, "Output") + + def test_short_color_fg(self): + raw = f'|#0F0{self.str}|n' + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output3, "Output") + + def test_short_color_bg(self): + raw = f'|[#0f0{self.str}|n' + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output4, "Output") + + def test_long_color_fg(self): + raw = f'|#00ff00{self.str}|n' + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output3, "Output") + + def test_long_color_bg(self): + raw = f'|[#00FF00{self.str}|n' + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output4, "Output") + + +class TestANSIParser(TestCase): + """ + Tests the ansi fallback of the hex color conversion and + truecolor conversion + """ + def setUp(self): + self.parser = ANSIParser().parse_ansi + self.str = 'test ' + + # ANSI FALLBACK + # Red + self.output1 = '\x1b[1m\x1b[31mtest \x1b[0m' + # White + self.output2 = '\x1b[1m\x1b[37mtest \x1b[0m' + # Red BG + self.output3 = '\x1b[41mtest \x1b[0m' + # Blue FG, Red BG + self.output4 = '\x1b[41m\x1b[1m\x1b[34mtest \x1b[0m' + + def test_hex_color(self): + raw = f'|#F00{self.str}|n' + ansi = parser(raw) + # self.assertEqual(ansi, self.str, "Cleaned") + self.assertEqual(ansi, self.output1, "Output") + + def test_hex_greyscale(self): + raw = f'|#FFF{self.str}|n' + ansi = parser(raw) + self.assertEqual(ansi, self.output2, "Output") + + def test_hex_color_bg(self): + raw = f'|[#Ff0000{self.str}|n' + ansi = parser(raw) + self.assertEqual(ansi, self.output3, "Output") + + def test_hex_color_fg_bg(self): + raw = f'|[#Ff0000|#00f{self.str}|n' + ansi = parser(raw) + self.assertEqual(ansi, self.output4, "Output") + + def test_truecolor_fg(self): + raw = f'|#00c700{self.str}|n' + ansi = parser(raw, truecolor=True) + output = f'\x1b[38;2;0;199;0m{self.str}\x1b[0m' + self.assertEqual(ansi, output, "Output") + + def test_truecolor_bg(self): + raw = f'|[#00c700{self.str}|n' + ansi = parser(raw, truecolor=True) + output = f'\x1b[48;2;0;199;0m{self.str}\x1b[0m' + self.assertEqual(ansi, output, "Output") + + def test_truecolor_fg_bg(self): + raw = f'|[#00c700|#880000{self.str}|n' + ansi = parser(raw, truecolor=True) + output = f'\x1b[48;2;0;199;0m\x1b[38;2;136;0;0m{self.str}\x1b[0m' + self.assertEqual(ansi, output, "Output") From de09f7a71caa5bd648554f838a55e73ded76cab9 Mon Sep 17 00:00:00 2001 From: mike Date: Sat, 6 Apr 2024 17:45:37 -0700 Subject: [PATCH 011/112] Added basic terminal detection for truecolor support. --- evennia/server/portal/telnet.py | 5 +++++ evennia/server/portal/ttype.py | 26 ++++++++++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index cb577f3835..1b1e27db62 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -437,6 +437,9 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS): xterm256 = options.get( "xterm256", flags.get("XTERM256", False) if flags.get("TTYPE", False) else True ) + truecolor = options.get( + "truecolor", flags.get("TRUECOLOR", False) if flags.get("TTYPE", False) else True + ) useansi = options.get( "ansi", flags.get("ANSI", False) if flags.get("TTYPE", False) else True ) @@ -460,6 +463,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS): _RE_N.sub("", prompt) + ("||n" if prompt.endswith("|") else "|n"), strip_ansi=nocolor, xterm256=xterm256, + truecolor=truecolor ) if mxp: prompt = mxp_parse(prompt) @@ -496,6 +500,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS): strip_ansi=nocolor, xterm256=xterm256, mxp=mxp, + truecolor=truecolor ) if mxp: linetosend = mxp_parse(linetosend) diff --git a/evennia/server/portal/ttype.py b/evennia/server/portal/ttype.py index 532d43f8a4..3a6be64c72 100644 --- a/evennia/server/portal/ttype.py +++ b/evennia/server/portal/ttype.py @@ -130,10 +130,10 @@ class Ttype: self.protocol.protocol_flags["NOPROMPTGOAHEAD"] = False if ( - clientname.startswith("XTERM") - or clientname.endswith("-256COLOR") - or clientname - in ( + clientname.startswith("XTERM") + or clientname.endswith("-256COLOR") + or clientname + in ( "ATLANTIS", # > 0.9.9.0 (aug 2009) "CMUD", # > 3.04 (mar 2009) "KILDCLIENT", # > 2.2.0 (sep 2005) @@ -143,13 +143,23 @@ class Ttype: "BEIP", # > 2.00.206 (late 2009) (BeipMu) "POTATO", # > 2.00 (maybe earlier) "TINYFUGUE", # > 4.x (maybe earlier) - ) + ) ): xterm256 = True + # use name to identify support for xterm truecolor + truecolor = False + if (clientname.endswith("-TRUECOLOR") or + clientname in ( + "AXMUD", + "TINTIN" + )): + truecolor = True + # all clients supporting TTYPE at all seem to support ANSI self.protocol.protocol_flags["ANSI"] = True self.protocol.protocol_flags["XTERM256"] = xterm256 + self.protocol.protocol_flags["TRUECOLOR"] = truecolor self.protocol.protocol_flags["CLIENTNAME"] = clientname self.protocol.requestNegotiation(TTYPE, SEND) @@ -159,9 +169,9 @@ class Ttype: tupper = term.upper() # identify xterm256 based on flag xterm256 = ( - tupper.endswith("-256COLOR") - or tupper.endswith("XTERM") # Apple Terminal, old Tintin - and not tupper.endswith("-COLOR") # old Tintin, Putty + tupper.endswith("-256COLOR") + or tupper.endswith("XTERM") # Apple Terminal, old Tintin + and not tupper.endswith("-COLOR") # old Tintin, Putty ) if xterm256: self.protocol.protocol_flags["ANSI"] = True From 32ede019da209476f3b244815ed7ba476d804134 Mon Sep 17 00:00:00 2001 From: t3albytes Date: Sat, 6 Apr 2024 21:26:40 -0400 Subject: [PATCH 012/112] Prevents admin from walking out non-existent exits in the wilderness --- evennia/contrib/grid/wilderness/wilderness.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/grid/wilderness/wilderness.py b/evennia/contrib/grid/wilderness/wilderness.py index bbda032f96..38d836d47f 100644 --- a/evennia/contrib/grid/wilderness/wilderness.py +++ b/evennia/contrib/grid/wilderness/wilderness.py @@ -682,7 +682,7 @@ class WildernessExit(DefaultExit): Returns: bool: True if traversing_object is allowed to traverse """ - return True + return self.wilderness.is_valid_coordinates(new_coordinates) def at_traverse(self, traversing_object, target_location): """ From 3684edb17218390667ad5bbd94ad7f165fde4c00 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Sat, 6 Apr 2024 19:38:35 -0600 Subject: [PATCH 013/112] fix int2str docstring --- evennia/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 377d31958d..cc92baee47 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -2885,7 +2885,7 @@ def int2str(number, adjective=False): Args: number (int): The number to convert. Floats will be converted to ints. - adjective (int): If set, map 1->1st, 2->2nd etc. If unset, map 1->one, 2->two etc. + adjective (bool): If True, map 1->1st, 2->2nd etc. If unset or False, map 1->one, 2->two etc. up to twelve. Return: str: The number expressed as a string. From 256c517ece55c07c2bcd52c011b3af95b5ecf242 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sun, 7 Apr 2024 13:48:13 +1000 Subject: [PATCH 014/112] Fix traceback if invalid regex is passed into editor :s command --- evennia/utils/eveditor.py | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 04578c8be8..7b1e775d1f 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -647,29 +647,34 @@ class CmdEditorGroup(CmdEditorBase): if not self.linerange: lstart = 0 lend = self.cline + 1 - caller.msg( - _("Search-replaced {arg1} -> {arg2} for lines {l1}-{l2}.").format( - arg1=self.arg1, arg2=self.arg2, l1=lstart + 1, l2=lend - ) - ) - else: - caller.msg( - _("Search-replaced {arg1} -> {arg2} for {line}.").format( - arg1=self.arg1, arg2=self.arg2, line=self.lstr - ) - ) sarea = "\n".join(linebuffer[lstart:lend]) regex = r"%s|^%s(?=\s)|(?<=\s)%s(?=\s)|^%s$|(?<=\s)%s$" regarg = self.arg1.strip("'").strip('"') if " " in regarg: regarg = regarg.replace(" ", " +") - sarea = re.sub( - regex % (regarg, regarg, regarg, regarg, regarg), - self.arg2.strip("'").strip('"'), - sarea, - re.MULTILINE, - ) + try: + sarea = re.sub( + regex % (regarg, regarg, regarg, regarg, regarg), + self.arg2.strip("'").strip('"'), + sarea, + re.MULTILINE, + ) + except re.error as e: + caller.msg(_("Invalid regular expression.")) + else: + if not self.linerange: + caller.msg( + _("Search-replaced {arg1} -> {arg2} for lines {l1}-{l2}.").format( + arg1=self.arg1, arg2=self.arg2, l1=lstart + 1, l2=lend + ) + ) + else: + caller.msg( + _("Search-replaced {arg1} -> {arg2} for {line}.").format( + arg1=self.arg1, arg2=self.arg2, line=self.lstr + ) + ) buf = linebuffer[:lstart] + sarea.split("\n") + linebuffer[lend:] editor.update_buffer(buf) elif cmd == ":f": From 0cac9bf8720ad0f7f601750b2e477f8efc09a171 Mon Sep 17 00:00:00 2001 From: mike Date: Sun, 7 Apr 2024 00:16:59 -0700 Subject: [PATCH 015/112] Added truecolor support to web portal. --- evennia/utils/ansi.py | 6 +-- evennia/utils/hex_colors.py | 42 ++++++++++++++++---- evennia/utils/tests/test_text2html.py | 8 ++++ evennia/utils/text2html.py | 55 ++++++++++++++++++++++----- 4 files changed, 92 insertions(+), 19 deletions(-) diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index 5d95c402ed..2c07ccd7f1 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -69,9 +69,9 @@ from django.conf import settings from evennia.utils import logger, utils from evennia.utils.utils import to_str -from evennia.utils.hex_colors import HexToTruecolor +from evennia.utils.hex_colors import HexColors -hex_sub = HexToTruecolor.hex_sub +hex_sub = HexColors.hex_sub MXP_ENABLED = settings.MXP_ENABLED @@ -470,7 +470,7 @@ class ANSIParser(object): string = self.brightbg_sub.sub(self.sub_brightbg, string) def do_truecolor(part: re.Match, truecolor=truecolor): - hex2truecolor = HexToTruecolor() + hex2truecolor = HexColors() return hex2truecolor.sub_truecolor(part, truecolor) def do_xterm256_fg(part): diff --git a/evennia/utils/hex_colors.py b/evennia/utils/hex_colors.py index 4118c179c3..dcea3531a7 100644 --- a/evennia/utils/hex_colors.py +++ b/evennia/utils/hex_colors.py @@ -1,7 +1,7 @@ import re -class HexToTruecolor: +class HexColors: """ This houses a method for converting hex codes to xterm truecolor codes or falls back to evennia xterm256 codes to be handled by sub_xterm256 @@ -14,19 +14,18 @@ class HexToTruecolor: _RE_FG_OR_BG = '\|\[?#' _RE_HEX_LONG = '[0-9a-fA-F]{6}' _RE_HEX_SHORT = '[0-9a-fA-F]{3}' - _RE_BYTE = '[0-2][0-9][0-9]' - _RE_3_BYTES = f'({_RE_BYTE})({_RE_BYTE})({_RE_BYTE})' + _RE_BYTE = '[0-2]?[0-9]?[0-9]' + _RE_XTERM_TRUECOLOR = rf'\[([34])8;2;({_RE_BYTE});({_RE_BYTE});({_RE_BYTE})m' # Used in hex_sub _RE_HEX_PATTERN = f'({_RE_FG_OR_BG})({_RE_HEX_LONG}|{_RE_HEX_SHORT})' - # Used for truecolor_sub - _RE_24_BIT_RGB_FG = f'{_RE_FG}{_RE_3_BYTES}' - _RE_24_BIT_RGB_BG = f'{_RE_BG}{_RE_3_BYTES}' - # Used for greyscale _GREYS = "abcdefghijklmnopqrstuvwxyz" + TRUECOLOR_FG = f'\x1b\[38;2;{_RE_BYTE};{_RE_BYTE};{_RE_BYTE}m' + TRUECOLOR_BG = f'\x1b\[48;2;{_RE_BYTE};{_RE_BYTE};{_RE_BYTE}m' + # Our matchers for use with ANSIParser and ANSIString hex_sub = re.compile(rf'{_RE_HEX_PATTERN}', re.DOTALL) @@ -73,6 +72,35 @@ class HexToTruecolor: xtag += f"8;2;{r};{g};{b}m" return xtag + def xterm_truecolor_to_html_style(self, fg="", bg="") -> str: + """ + Converts xterm truecolor to an html style property + + Args: + fg: xterm truecolor + bg: xterm truecolor + + Returns: style='color and or background-color' + + """ + prop = 'style="' + if fg != '': + res = re.search(self._RE_XTERM_TRUECOLOR, fg, re.DOTALL) + fg_bg, r, g, b = res.groups() + r = hex(int(r))[2:].zfill(2) + g = hex(int(g))[2:].zfill(2) + b = hex(int(b))[2:].zfill(2) + prop += f"color: #{r}{g}{b};" + if bg != '': + res = re.search(self._RE_XTERM_TRUECOLOR, bg, re.DOTALL) + fg_bg, r, g, b = res.groups() + r = hex(int(r))[2:].zfill(2) + g = hex(int(g))[2:].zfill(2) + b = hex(int(b))[2:].zfill(2) + prop += f"background-color: #{r}{g}{b};" + prop += f'"' + return prop + def _split_hex_to_bytes(self, tag: str) -> tuple[str, str, str]: """ Splits hex string into separate bytes: diff --git a/evennia/utils/tests/test_text2html.py b/evennia/utils/tests/test_text2html.py index aea0cb87e7..74bd61bf18 100644 --- a/evennia/utils/tests/test_text2html.py +++ b/evennia/utils/tests/test_text2html.py @@ -46,6 +46,14 @@ class TestText2Html(TestCase): parser.format_styles("a " + ansi.ANSI_INVERSE + "red" + ansi.ANSI_NORMAL + "foo"), ) + # True Color + self.assertEqual( + 'redfoo', + parser.format_styles( + f'\x1b[38;2;255;0;0m' + "red" + ansi.ANSI_NORMAL + "foo" + ), + ) + def test_remove_bells(self): parser = text2html.HTML_PARSER self.assertEqual("foo", parser.remove_bells("foo")) diff --git a/evennia/utils/text2html.py b/evennia/utils/text2html.py index 1effdd3266..e8000a7f81 100644 --- a/evennia/utils/text2html.py +++ b/evennia/utils/text2html.py @@ -13,11 +13,15 @@ from html import escape as html_escape from .ansi import * +from .hex_colors import HexColors + # All xterm256 RGB equivalents XTERM256_FG = "\033[38;5;{}m" XTERM256_BG = "\033[48;5;{}m" +hex_colors = HexColors() + class TextToHTMLparser(object): """ @@ -67,13 +71,11 @@ class TextToHTMLparser(object): ] xterm_bg_codes = [XTERM256_BG.format(i + 16) for i in range(240)] - re_style = re.compile( - r"({})".format( + r"({}|{})".format( "|".join( style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes - ).replace("[", r"\[") - ) + ).replace("[", r"\["), "|".join([HexColors.TRUECOLOR_FG, HexColors.TRUECOLOR_BG])) ) colorlist = ( @@ -244,6 +246,7 @@ class TextToHTMLparser(object): # split out the ANSI codes and clean out any empty items str_list = [substr for substr in self.re_style.split(text) if substr] + # initialize all the flags and classes classes = [] clean = True @@ -253,6 +256,8 @@ class TextToHTMLparser(object): fg = ANSI_WHITE # default bg is black bg = ANSI_BACK_BLACK + truecolor_fg = '' + truecolor_bg = '' for i, substr in enumerate(str_list): # reset all current styling @@ -266,6 +271,8 @@ class TextToHTMLparser(object): hilight = ANSI_UNHILITE fg = ANSI_WHITE bg = ANSI_BACK_BLACK + truecolor_fg = '' + truecolor_bg = '' # change color elif substr in self.ansi_color_codes + self.xterm_fg_codes: @@ -281,6 +288,14 @@ class TextToHTMLparser(object): # set new bg bg = substr + elif re.match(hex_colors.TRUECOLOR_FG, substr): + str_list[i] = '' + truecolor_fg = substr + + elif re.match(hex_colors.TRUECOLOR_BG, substr): + str_list[i] = "" + truecolor_bg = substr + # non-color codes elif substr in self.style_codes: # erase ANSI code from output @@ -319,9 +334,23 @@ class TextToHTMLparser(object): color_index = self.colorlist.index(fg) if inverse: - # inverse means swap fg and bg indices - bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0")) - color_class = "color-{}".format(str(bg_index).rjust(3, "0")) + if truecolor_fg != '' and truecolor_bg != '': + # True startcolor only + truecolor_fg, truecolor_bg = truecolor_bg, truecolor_fg + elif truecolor_fg != '' and truecolor_bg == '': + # Truecolor fg, class based bg + truecolor_bg = truecolor_fg + truecolor_fg = '' + color_class = "color-{}".format(str(bg_index).rjust(3, "0")) + elif truecolor_fg == '' and truecolor_bg != '': + # Truecolor bg, class based fg + truecolor_fg = truecolor_bg + truecolor_bg = '' + bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0")) + else: + # inverse means swap fg and bg indices + bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0")) + color_class = "color-{}".format(str(bg_index).rjust(3, "0")) else: # use fg and bg indices for classes bg_class = "bgcolor-{}".format(str(bg_index).rjust(3, "0")) @@ -333,8 +362,16 @@ class TextToHTMLparser(object): # light grey text is the default, don't explicitly style if color_class != "color-007": classes.append(color_class) + # define the new style span - prefix = ''.format(" ".join(classes)) + if truecolor_fg == '' and truecolor_bg == '': + prefix = f'' + else: + # Classes can't be used for true color + prefix = (f'') + # close any prior span if not clean: prefix = "" + prefix @@ -366,7 +403,7 @@ class TextToHTMLparser(object): """ # parse everything to ansi first - text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True) + text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True, truecolor=True) # convert all ansi to html result = re.sub(self.re_string, self.sub_text, text) result = re.sub(self.re_mxplink, self.sub_mxp_links, result) From 2b1524b5d986314cf89a99b06e75f9ca6071d0e6 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 7 Apr 2024 10:16:11 +0200 Subject: [PATCH 016/112] Test unittest with isNot assert, to see if it matters --- evennia/contrib/base_systems/components/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/base_systems/components/tests.py b/evennia/contrib/base_systems/components/tests.py index 75e169b825..57d072b8f7 100644 --- a/evennia/contrib/base_systems/components/tests.py +++ b/evennia/contrib/base_systems/components/tests.py @@ -268,7 +268,7 @@ class TestComponents(EvenniaTest): def test_mutables_are_not_shared_when_autocreate(self): self.char1.test_a.my_list.append(1) - self.assertNotEqual(id(self.char1.test_a.my_list), id(self.char2.test_a.my_list)) + self.assertIsNot(self.char1.test_a.my_list, self.char2.test_a.my_list) def test_replacing_class_component_slot_with_runtime_component(self): self.char1.components.add_default("replacement_inherited_test_a") From c2277f7188b0b05ba6c970c9f720ccd1eb3cd876 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 7 Apr 2024 02:41:23 -0600 Subject: [PATCH 017/112] CounterTrait validate_input method now checks for a last_update before defaulting it to current time. Resolves #3317 --- evennia/contrib/rpg/traits/traits.py | 76 +++++++++++++++++++++------- 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/evennia/contrib/rpg/traits/traits.py b/evennia/contrib/rpg/traits/traits.py index fbcbeda34a..911da85b02 100644 --- a/evennia/contrib/rpg/traits/traits.py +++ b/evennia/contrib/rpg/traits/traits.py @@ -571,12 +571,16 @@ class TraitHandler: # initialize any # Note that .trait_data retains the connection to the database, meaning every # update we do to .trait_data automatically syncs with database. - self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category) + self.trait_data = obj.attributes.get( + db_attribute_key, category=db_attribute_category + ) if self.trait_data is None: # no existing storage; initialize it, we then have to fetch it again # to retain the db connection obj.attributes.add(db_attribute_key, {}, category=db_attribute_category) - self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category) + self.trait_data = obj.attributes.get( + db_attribute_key, category=db_attribute_category + ) self._cache = {} def __len__(self): @@ -595,7 +599,9 @@ class TraitHandler: _SA(self, trait_key, value) else: trait_cls = self._get_trait_class(trait_key=trait_key) - valid_keys = list_to_string(list(trait_cls.default_keys.keys()), endsep="or") + valid_keys = list_to_string( + list(trait_cls.default_keys.keys()), endsep="or" + ) raise TraitException( f"Trait object not settable directly. Assign to {trait_key}.{valid_keys}." ) @@ -627,7 +633,9 @@ class TraitHandler: try: trait_type = self.trait_data[trait_key]["trait_type"] except KeyError: - raise TraitException(f"Trait class for Trait {trait_key} could not be found.") + raise TraitException( + f"Trait class for Trait {trait_key} could not be found." + ) try: return _TRAIT_CLASSES[trait_type] except KeyError: @@ -657,11 +665,18 @@ class TraitHandler: if trait is None and trait_key in self.trait_data: trait_type = self.trait_data[trait_key]["trait_type"] trait_cls = self._get_trait_class(trait_type) - trait = self._cache[trait_key] = trait_cls(_GA(self, "trait_data")[trait_key]) + trait = self._cache[trait_key] = trait_cls( + _GA(self, "trait_data")[trait_key] + ) return trait def add( - self, trait_key, name=None, trait_type=DEFAULT_TRAIT_TYPE, force=True, **trait_properties + self, + trait_key, + name=None, + trait_type=DEFAULT_TRAIT_TYPE, + force=True, + **trait_properties, ): """ Create a new Trait and add it to the handler. @@ -748,7 +763,9 @@ class TraitProperty: """ - def __init__(self, name=None, trait_type=DEFAULT_TRAIT_TYPE, force=True, **trait_properties): + def __init__( + self, name=None, trait_type=DEFAULT_TRAIT_TYPE, force=True, **trait_properties + ): """ Initialize a TraitField. Mimics TraitHandler.add input except no `trait_key`. @@ -767,7 +784,9 @@ class TraitProperty: """ self._traithandler_name = trait_properties.pop("traithandler_name", "traits") - trait_properties.update({"name": name, "trait_type": trait_type, "force": force}) + trait_properties.update( + {"name": name, "trait_type": trait_type, "force": force} + ) self._trait_properties = trait_properties self._cache = {} @@ -807,7 +826,9 @@ class TraitProperty: if trait is None: # initialize the trait traithandler.add(self._trait_key, **self._trait_properties) - trait = traithandler.get(self._trait_key) # caches it in the traithandler + trait = traithandler.get( + self._trait_key + ) # caches it in the traithandler self._cache[instance] = trait return self._cache[instance] @@ -915,13 +936,21 @@ class Trait: if MandatoryTraitKey in unset_defaults.values(): # we have one or more unset keys that was mandatory - _raise_err([key for key, value in unset_defaults.items() if value == MandatoryTraitKey]) + _raise_err( + [ + key + for key, value in unset_defaults.items() + if value == MandatoryTraitKey + ] + ) # apply the default values trait_data.update(unset_defaults) if not cls.allow_extra_properties: # don't allow any extra properties - remove the extra data - for key in (key for key in inp.difference(req) if key not in ("name", "trait_type")): + for key in ( + key for key in inp.difference(req) if key not in ("name", "trait_type") + ): del trait_data[key] return trait_data @@ -945,7 +974,12 @@ class Trait: def __getattr__(self, key): """Access extra parameters as attributes.""" - if key in ("default_keys", "data_default", "trait_type", "allow_extra_properties"): + if key in ( + "default_keys", + "data_default", + "trait_type", + "allow_extra_properties", + ): return _GA(self, key) try: return self._data[key] @@ -1276,7 +1310,7 @@ class CounterTrait(Trait): ) # set up rate if trait_data["rate"] != 0: - trait_data["last_update"] = time() + trait_data["last_update"] = trait_data.get("last_update", time()) else: trait_data["last_update"] = None return trait_data @@ -1310,7 +1344,8 @@ class CounterTrait(Trait): """Check if we passed the ratetarget in either direction.""" ratetarget = self._data["ratetarget"] return ratetarget is not None and ( - (self.rate < 0 and value <= ratetarget) or (self.rate > 0 and value >= ratetarget) + (self.rate < 0 and value <= ratetarget) + or (self.rate > 0 and value >= ratetarget) ) def _stop_timer(self): @@ -1435,7 +1470,9 @@ class CounterTrait(Trait): @current.setter def current(self, value): if type(value) in (int, float): - self._data["current"] = self._check_and_start_timer(self._enforce_boundaries(value)) + self._data["current"] = self._check_and_start_timer( + self._enforce_boundaries(value) + ) @current.deleter def current(self): @@ -1552,6 +1589,7 @@ class GaugeTrait(CounterTrait): rate = self.rate if rate != 0 and self._data["last_update"] is not None: now = time() + tdiff = now - self._data["last_update"] current += rate * tdiff value = current @@ -1657,13 +1695,17 @@ class GaugeTrait(CounterTrait): def current(self): """The `current` value of the gauge.""" return self._update_current( - self._enforce_boundaries(self._data.get("current", (self.base + self.mod) * self.mult)) + self._enforce_boundaries( + self._data.get("current", (self.base + self.mod) * self.mult) + ) ) @current.setter def current(self, value): if type(value) in (int, float): - self._data["current"] = self._check_and_start_timer(self._enforce_boundaries(value)) + self._data["current"] = self._check_and_start_timer( + self._enforce_boundaries(value) + ) @current.deleter def current(self): From 2c0888619388c28be848e6d4708fd7c21310d29c Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 7 Apr 2024 21:24:43 +0200 Subject: [PATCH 018/112] Update Changelog. Correct line lengths --- CHANGELOG.md | 3 + .../base_systems/components/dbfield.py | 3 +- evennia/contrib/rpg/traits/traits.py | 59 +++++-------------- evennia/objects/objects.py | 3 +- evennia/objects/tests.py | 13 ++-- evennia/typeclasses/attributes.py | 1 + 6 files changed, 28 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f92cdab5ee..59c1ca5d91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,11 @@ - [Feature][pull3470]: New `exit_order` kwarg to `DefaultObject.get_display_exits` to easier customize the order in which standard exits are displayed in a room (chiizujin) +- [Fix][pull3495]: Fix rate in Trait contribs not updating after reload + (jaborsh) [pull3470]: https://github.com/evennia/evennia/pull/3470 +[pull3495]: https://github.com/evennia/evennia/pull/3495 ## Evennia 4.1.1 diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index 67f812b484..4b9c6d4fa8 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -6,8 +6,7 @@ This file contains the Descriptors used to set Fields in Components import typing -from evennia.typeclasses.attributes import (AttributeProperty, - NAttributeProperty) +from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty if typing.TYPE_CHECKING: from .components import Component diff --git a/evennia/contrib/rpg/traits/traits.py b/evennia/contrib/rpg/traits/traits.py index 911da85b02..59daf7d70f 100644 --- a/evennia/contrib/rpg/traits/traits.py +++ b/evennia/contrib/rpg/traits/traits.py @@ -571,16 +571,12 @@ class TraitHandler: # initialize any # Note that .trait_data retains the connection to the database, meaning every # update we do to .trait_data automatically syncs with database. - self.trait_data = obj.attributes.get( - db_attribute_key, category=db_attribute_category - ) + self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category) if self.trait_data is None: # no existing storage; initialize it, we then have to fetch it again # to retain the db connection obj.attributes.add(db_attribute_key, {}, category=db_attribute_category) - self.trait_data = obj.attributes.get( - db_attribute_key, category=db_attribute_category - ) + self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category) self._cache = {} def __len__(self): @@ -599,9 +595,7 @@ class TraitHandler: _SA(self, trait_key, value) else: trait_cls = self._get_trait_class(trait_key=trait_key) - valid_keys = list_to_string( - list(trait_cls.default_keys.keys()), endsep="or" - ) + valid_keys = list_to_string(list(trait_cls.default_keys.keys()), endsep="or") raise TraitException( f"Trait object not settable directly. Assign to {trait_key}.{valid_keys}." ) @@ -633,9 +627,7 @@ class TraitHandler: try: trait_type = self.trait_data[trait_key]["trait_type"] except KeyError: - raise TraitException( - f"Trait class for Trait {trait_key} could not be found." - ) + raise TraitException(f"Trait class for Trait {trait_key} could not be found.") try: return _TRAIT_CLASSES[trait_type] except KeyError: @@ -665,9 +657,7 @@ class TraitHandler: if trait is None and trait_key in self.trait_data: trait_type = self.trait_data[trait_key]["trait_type"] trait_cls = self._get_trait_class(trait_type) - trait = self._cache[trait_key] = trait_cls( - _GA(self, "trait_data")[trait_key] - ) + trait = self._cache[trait_key] = trait_cls(_GA(self, "trait_data")[trait_key]) return trait def add( @@ -763,9 +753,7 @@ class TraitProperty: """ - def __init__( - self, name=None, trait_type=DEFAULT_TRAIT_TYPE, force=True, **trait_properties - ): + def __init__(self, name=None, trait_type=DEFAULT_TRAIT_TYPE, force=True, **trait_properties): """ Initialize a TraitField. Mimics TraitHandler.add input except no `trait_key`. @@ -784,9 +772,7 @@ class TraitProperty: """ self._traithandler_name = trait_properties.pop("traithandler_name", "traits") - trait_properties.update( - {"name": name, "trait_type": trait_type, "force": force} - ) + trait_properties.update({"name": name, "trait_type": trait_type, "force": force}) self._trait_properties = trait_properties self._cache = {} @@ -826,9 +812,7 @@ class TraitProperty: if trait is None: # initialize the trait traithandler.add(self._trait_key, **self._trait_properties) - trait = traithandler.get( - self._trait_key - ) # caches it in the traithandler + trait = traithandler.get(self._trait_key) # caches it in the traithandler self._cache[instance] = trait return self._cache[instance] @@ -936,21 +920,13 @@ class Trait: if MandatoryTraitKey in unset_defaults.values(): # we have one or more unset keys that was mandatory - _raise_err( - [ - key - for key, value in unset_defaults.items() - if value == MandatoryTraitKey - ] - ) + _raise_err([key for key, value in unset_defaults.items() if value == MandatoryTraitKey]) # apply the default values trait_data.update(unset_defaults) if not cls.allow_extra_properties: # don't allow any extra properties - remove the extra data - for key in ( - key for key in inp.difference(req) if key not in ("name", "trait_type") - ): + for key in (key for key in inp.difference(req) if key not in ("name", "trait_type")): del trait_data[key] return trait_data @@ -1344,8 +1320,7 @@ class CounterTrait(Trait): """Check if we passed the ratetarget in either direction.""" ratetarget = self._data["ratetarget"] return ratetarget is not None and ( - (self.rate < 0 and value <= ratetarget) - or (self.rate > 0 and value >= ratetarget) + (self.rate < 0 and value <= ratetarget) or (self.rate > 0 and value >= ratetarget) ) def _stop_timer(self): @@ -1470,9 +1445,7 @@ class CounterTrait(Trait): @current.setter def current(self, value): if type(value) in (int, float): - self._data["current"] = self._check_and_start_timer( - self._enforce_boundaries(value) - ) + self._data["current"] = self._check_and_start_timer(self._enforce_boundaries(value)) @current.deleter def current(self): @@ -1695,17 +1668,13 @@ class GaugeTrait(CounterTrait): def current(self): """The `current` value of the gauge.""" return self._update_current( - self._enforce_boundaries( - self._data.get("current", (self.base + self.mod) * self.mult) - ) + self._enforce_boundaries(self._data.get("current", (self.base + self.mod) * self.mult)) ) @current.setter def current(self, value): if type(value) in (int, float): - self._data["current"] = self._check_and_start_timer( - self._enforce_boundaries(value) - ) + self._data["current"] = self._check_and_start_timer(self._enforce_boundaries(value)) @current.deleter def current(self): diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 150c95f5d1..dcf9b49f90 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1588,6 +1588,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): obj.get_display_name(looker, exit_order=('north', 'south')) -> "Exits: north, south, out, and portal." (markup not shown here) """ + def _sort_exit_names(names): exit_order = kwargs.get("exit_order") if not exit_order: @@ -1595,7 +1596,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): sort_index = {name: key for key, name in enumerate(exit_order)} names = sorted(names) end_pos = len(names) + 1 - names.sort(key=lambda name:sort_index.get(name, end_pos)) + names.sort(key=lambda name: sort_index.get(name, end_pos)) return names exits = self.filter_visible(self.contents_get(content_type="exit"), looker, **kwargs) diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index 5c300865dc..df00189d50 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -3,8 +3,12 @@ from unittest import skip from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom from evennia.objects.models import ObjectDB from evennia.typeclasses.attributes import AttributeProperty -from evennia.typeclasses.tags import (AliasProperty, PermissionProperty, - TagCategoryProperty, TagProperty) +from evennia.typeclasses.tags import ( + AliasProperty, + PermissionProperty, + TagCategoryProperty, + TagProperty, +) from evennia.utils import create, search from evennia.utils.ansi import strip_ansi from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase @@ -102,7 +106,7 @@ class DefaultObjectTest(BaseEvenniaTest): self.assertEqual(exits, "Exits: out, south, portal, north, and aperture") # in specified order with unspecified exits alpbabetically on the end - exit_order = ('north', 'south', 'out') + exit_order = ("north", "south", "out") exits = strip_ansi(self.room1.get_display_exits(self.char1, exit_order=exit_order)) self.assertEqual(exits, "Exits: north, south, out, aperture, and portal") @@ -589,7 +593,6 @@ class TestProperties(EvenniaTestCase): # check cross-instance sharing self.assertEqual(obj2.attr5, [], "cross-instance sharing detected") - def test_mutable_defaults__autocreate_false(self): """ Test https://github.com/evennia/evennia/issues/3488, where a mutable default value (like a @@ -631,7 +634,6 @@ class TestProperties(EvenniaTestCase): # check cross-instance sharing self.assertEqual(obj2.attr7, []) - def test_mutable_defaults__autocreate_true(self): """ Test mutable defaults with autocreate=True. @@ -652,4 +654,3 @@ class TestProperties(EvenniaTestCase): obj1.delete() obj2.delete() - diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index bdef3bdc33..03929dac43 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -17,6 +17,7 @@ from copy import copy from django.conf import settings from django.db import models from django.utils.encoding import smart_str + from evennia.locks.lockhandler import LockHandler from evennia.utils.dbserialize import from_pickle, to_pickle from evennia.utils.idmapper.models import SharedMemoryModel From 2c570f5208c0284344f64329478d1647bdc6e228 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 7 Apr 2024 21:29:00 +0200 Subject: [PATCH 019/112] Update Changelog --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c1ca5d91..27c717504c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,13 @@ - [Feature][pull3470]: New `exit_order` kwarg to `DefaultObject.get_display_exits` to easier customize the order in which standard exits are displayed in a room (chiizujin) -- [Fix][pull3495]: Fix rate in Trait contribs not updating after reload - (jaborsh) +- [Fix][pull3495]: Fix rate in Trait contribs not updating after reload (jaborsh) +- [Fix][pull3491]: Fix traceback in EvEditor when searching with malformed regex (chiizujin) [pull3470]: https://github.com/evennia/evennia/pull/3470 [pull3495]: https://github.com/evennia/evennia/pull/3495 +[pull3491]: https://github.com/evennia/evennia/pull/3491 + ## Evennia 4.1.1 From 7f41e5a649629a886b3fcacdfed0697d5dfe98a3 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 7 Apr 2024 21:51:52 +0200 Subject: [PATCH 020/112] Fix typo in code structure and utils tutorial. Resolve #3474 --- CHANGELOG.md | 1 + .../Beginner-Tutorial/Part3/Beginner-Tutorial-Utilities.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27c717504c..25b114905a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ standard exits are displayed in a room (chiizujin) - [Fix][pull3495]: Fix rate in Trait contribs not updating after reload (jaborsh) - [Fix][pull3491]: Fix traceback in EvEditor when searching with malformed regex (chiizujin) +- [Docs]: Doc fixes (Griatch, chiizujin) [pull3470]: https://github.com/evennia/evennia/pull/3470 [pull3495]: https://github.com/evennia/evennia/pull/3495 diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Utilities.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Utilities.md index effd4bce3d..b715cfb559 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Utilities.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Utilities.md @@ -135,7 +135,7 @@ ABILITY_REVERSE_MAP = { Above, the `Ability` class holds some basic properties of a character sheet. -The `ABILITY_REVERSE_MAP` is a convenient map to go the other way &mdas; if in some command we were to enter the string 'cha', we could use this mapping to directly convert your input to the correct `Ability`. For example: +The `ABILITY_REVERSE_MAP` is a convenient map to go the other way — if in some command we were to enter the string 'cha', we could use this mapping to directly convert your input to the correct `Ability`. For example: ability = ABILITY_REVERSE_MAP.get(your_input) From 686e8195802e4a58491cc10dc3a4efa078253881 Mon Sep 17 00:00:00 2001 From: mike Date: Sun, 7 Apr 2024 17:56:36 -0700 Subject: [PATCH 021/112] Added validation for the TRUECOLOR option to support manual toggling. --- evennia/commands/default/account.py | 1 + 1 file changed, 1 insertion(+) diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index f51b079e17..dd3878b080 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -665,6 +665,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS): "INPUTDEBUG": validate_bool, "FORCEDENDLINE": validate_bool, "LOCALECHO": validate_bool, + "TRUECOLOR": validate_bool } name = self.lhs.upper() From 68dd8e8174ae3a1ffc248e8c8bb24aa6503c9081 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Mon, 8 Apr 2024 12:44:45 +1000 Subject: [PATCH 022/112] Fix editor search/replace feedback when replacing markup --- evennia/utils/eveditor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index c7a7a04c92..0245c938c7 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -673,13 +673,13 @@ class CmdEditorGroup(CmdEditorBase): if not self.linerange: caller.msg( _("Search-replaced {arg1} -> {arg2} for lines {l1}-{l2}.").format( - arg1=self.arg1, arg2=self.arg2, l1=lstart + 1, l2=lend + arg1=raw(self.arg1), arg2=raw(self.arg2), l1=lstart + 1, l2=lend ) ) else: caller.msg( _("Search-replaced {arg1} -> {arg2} for {line}.").format( - arg1=self.arg1, arg2=self.arg2, line=self.lstr + arg1=raw(self.arg1), arg2=raw(self.arg2), line=self.lstr ) ) buf = linebuffer[:lstart] + sarea.split("\n") + linebuffer[lend:] From 55f848469721afe9c766c8c306ff6811fe12d7e4 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Mon, 8 Apr 2024 14:14:06 +1000 Subject: [PATCH 023/112] Add width argument to editor's :j and :f commands --- evennia/utils/eveditor.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index c7a7a04c92..7557c40a0e 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -67,7 +67,7 @@ _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH # ------------------------------------------------------------- _HELP_TEXT = _( - """ + f""" - any non-command is appended to the end of the buffer. : - view buffer or only line(s) :: - raw-view buffer or only line(s) @@ -97,8 +97,10 @@ _HELP_TEXT = _( :s - search/replace word or regex in buffer or on line - :j - justify buffer or line . is f, c, l or r. Default f (full) - :f - flood-fill entire buffer or line . Equivalent to :j l + :j = - justify buffer or line . is f, c, l or r. is + width. Default for is l (left). Default for is {_DEFAULT_WIDTH} + :f = - flood-fill entire buffer or line to width . + Equivalent to :j l :fi - indent entire buffer or line :fd - de-indent entire buffer or line @@ -686,7 +688,14 @@ class CmdEditorGroup(CmdEditorBase): editor.update_buffer(buf) elif cmd == ":f": # :f flood-fill buffer or lines of buffer. + # :f = flood-fill buffer or lines of buffer to width . width = _DEFAULT_WIDTH + if self.arg1: + value = self.arg1.lstrip("=") + if not value.isdigit(): + self.caller.msg("Width must be a number.") + return + width = int(value) if not self.linerange: lstart = 0 lend = self.cline + 1 @@ -698,7 +707,7 @@ class CmdEditorGroup(CmdEditorBase): buf = linebuffer[:lstart] + fbuf.split("\n") + linebuffer[lend:] editor.update_buffer(buf) elif cmd == ":j": - # :f justify buffer of with as align (one of + # :j = justify buffer of to width with as align (one of # f(ull), c(enter), r(ight) or l(left). Default is full. align_map = { "full": "f", @@ -711,7 +720,10 @@ class CmdEditorGroup(CmdEditorBase): "l": "l", } align_name = {"f": "Full", "c": "Center", "l": "Left", "r": "Right"} - width = _DEFAULT_WIDTH + # shift width arg right if no alignment specified + if self.arg1.startswith('='): + self.arg2 = self.arg1 + self.arg1 = None if self.arg1 and self.arg1.lower() not in align_map: self.caller.msg( _("Valid justifications are") @@ -719,6 +731,13 @@ class CmdEditorGroup(CmdEditorBase): ) return align = align_map[self.arg1.lower()] if self.arg1 else "f" + width = _DEFAULT_WIDTH + if self.arg2: + value = self.arg2.lstrip("=") + if not value.isdigit(): + self.caller.msg("Width must be a number.") + return + width = int(value) if not self.linerange: lstart = 0 lend = self.cline + 1 From 5554946721b4a96d3c1dfd6b81e71a63415cf3a6 Mon Sep 17 00:00:00 2001 From: mike Date: Mon, 8 Apr 2024 16:28:41 -0700 Subject: [PATCH 024/112] Fixed typo in ttype and added truecolor to help text in telnet. --- evennia/server/portal/telnet.py | 1 + evennia/server/portal/ttype.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 1b1e27db62..d5e9118837 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -419,6 +419,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS): - xterm256: Enforce xterm256 colors, regardless of TTYPE. - noxterm256: Enforce no xterm256 color support, regardless of TTYPE. - nocolor: Strip all Color, regardless of ansi/xterm256 setting. + - truecolor: Enforce truecolor, regardless of TTYPE. - raw: Pass string through without any ansi processing (i.e. include Evennia ansi markers but do not convert them into ansi tokens) diff --git a/evennia/server/portal/ttype.py b/evennia/server/portal/ttype.py index 3a6be64c72..58705eb12d 100644 --- a/evennia/server/portal/ttype.py +++ b/evennia/server/portal/ttype.py @@ -44,7 +44,7 @@ class Ttype: def __init__(self, protocol): """ Initialize ttype by storing protocol on ourselves and calling - the client to see if it supporst ttype. + the client to see if it supports ttype. Args: protocol (Protocol): The protocol instance. From 68a3feb2fe4f5a4be0d64b02095e0c2a96d98fd4 Mon Sep 17 00:00:00 2001 From: mike Date: Tue, 9 Apr 2024 00:12:42 -0700 Subject: [PATCH 025/112] A few typo fixes and NAWS update detection and handling. --- evennia/server/portal/portalsessionhandler.py | 2 +- evennia/server/portal/telnet.py | 8 ++++++-- evennia/server/serversession.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/evennia/server/portal/portalsessionhandler.py b/evennia/server/portal/portalsessionhandler.py index 8a5ed53ce2..bd248cc8a7 100644 --- a/evennia/server/portal/portalsessionhandler.py +++ b/evennia/server/portal/portalsessionhandler.py @@ -468,7 +468,7 @@ class PortalSessionHandler(SessionHandler): kwargs (any): Each key is a command instruction to the protocol on the form key = [[args],{kwargs}]. This will call a method send_ on the protocol. If no such - method exixts, it sends the data to a method send_default. + method exits, it sends the data to a method send_default. """ # from evennia.server.profiling.timetrace import timetrace # DEBUG diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index cb577f3835..eeb00ba053 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -31,6 +31,7 @@ from twisted.internet.task import LoopingCall from evennia.server.portal import mssp, naws, suppress_ga, telnet_oob, ttype from evennia.server.portal.mccp import MCCP, Mccp, mccp_compress from evennia.server.portal.mxp import Mxp, mxp_parse +from evennia.server.portal.naws import NAWS from evennia.utils import ansi from evennia.utils.utils import class_from_module, to_bytes @@ -91,9 +92,12 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS): of incoming data. """ - # print(f"telnet dataReceived: {data}") try: - super().dataReceived(data) + # Do we have a NAWS update? + if NAWS in data and len([data[i:i+1] for i in range(0, len(data))]) == 9: + self.sessionhandler.sync(self.sessionhandler.get(self.sessid)) + else: + super().dataReceived(data) except ValueError as err: from evennia.utils import logger diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index 5177f30f5d..c4048e19ca 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -268,7 +268,7 @@ class ServerSession(_BASE_SESSION_CLASS): Notes: Since protocols can vary, no checking is done - as to the existene of the flag or not. The input + as to the existence of the flag or not. The input data should have been validated before this call. """ From 80117509319154ccb8f78d97686edf34a025fa8a Mon Sep 17 00:00:00 2001 From: mike Date: Tue, 9 Apr 2024 00:39:55 -0700 Subject: [PATCH 026/112] Update didn't work as an if/else. There appears to be a bug when used fullscreen. --- evennia/server/portal/telnet.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index eeb00ba053..31bf403f85 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -96,8 +96,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS): # Do we have a NAWS update? if NAWS in data and len([data[i:i+1] for i in range(0, len(data))]) == 9: self.sessionhandler.sync(self.sessionhandler.get(self.sessid)) - else: - super().dataReceived(data) + super().dataReceived(data) except ValueError as err: from evennia.utils import logger From c94b7f47c1012444bc673ddd64ee9191ca4d78cf Mon Sep 17 00:00:00 2001 From: mike Date: Tue, 9 Apr 2024 12:20:52 -0700 Subject: [PATCH 027/112] Added a new flag in the options/protocol_flags to ignore or follow NAWS updates. --- evennia/commands/default/account.py | 6 ++++++ evennia/server/inputfuncs.py | 4 ++++ evennia/server/portal/naws.py | 2 ++ evennia/server/portal/telnet.py | 6 +++++- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index 7b966eb7cf..e437fe8ca3 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -635,6 +635,11 @@ class CmdOption(COMMAND_DEFAULT_CLASS): self.msg(f"Option |w{new_name}|n was kept as '|w{old_val}|n'.") else: flags[new_name] = new_val + + # If we're manually assign a display size, turn off auto-resizing + if new_name in ['SCREENWIDTH', 'SCREENHEIGHT']: + flags['AUTORESIZE'] = False + self.msg( f"Option |w{new_name}|n was changed from '|w{old_val}|n' to" f" '|w{new_val}|n'." @@ -657,6 +662,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS): "RAW": validate_bool, "SCREENHEIGHT": validate_size, "SCREENWIDTH": validate_size, + "AUTORESIZE": validate_bool, "SCREENREADER": validate_bool, "TERM": utils.to_str, "UTF-8": validate_bool, diff --git a/evennia/server/inputfuncs.py b/evennia/server/inputfuncs.py index ee60643059..d30b06d4a5 100644 --- a/evennia/server/inputfuncs.py +++ b/evennia/server/inputfuncs.py @@ -175,6 +175,7 @@ _CLIENT_OPTIONS = ( "MCCP", "SCREENHEIGHT", "SCREENWIDTH", + "AUTORESIZE", "INPUTDEBUG", "RAW", "NOCOLOR", @@ -201,6 +202,7 @@ def client_options(session, *args, **kwargs): mccp (bool): MCCP compression on/off screenheight (int): Screen height in lines screenwidth (int): Screen width in characters + autoresize (bool): Use NAWS updates to dynamically adjust format inputdebug (bool): Debug input functions nocolor (bool): Strip color raw (bool): Turn off parsing @@ -256,6 +258,8 @@ def client_options(session, *args, **kwargs): flags["SCREENHEIGHT"] = validate_size(value) elif key == "screenwidth": flags["SCREENWIDTH"] = validate_size(value) + elif key == "autoresize": + flags["AUTORESIZE"] = validate_size(value) elif key == "inputdebug": flags["INPUTDEBUG"] = validate_bool(value) elif key == "nocolor": diff --git a/evennia/server/portal/naws.py b/evennia/server/portal/naws.py index ab7e892712..5e52688f2b 100644 --- a/evennia/server/portal/naws.py +++ b/evennia/server/portal/naws.py @@ -57,6 +57,7 @@ class Naws: option (Option): Not used. """ + self.protocol.protocol_flags["AUTORESIZE"] = False self.protocol.handshake_done() def do_naws(self, option): @@ -67,6 +68,7 @@ class Naws: option (Option): Not used. """ + self.protocol.protocol_flags["AUTORESIZE"] = True self.protocol.handshake_done() def negotiate_sizes(self, options): diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 31bf403f85..b545e67f7e 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -94,8 +94,12 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS): """ try: # Do we have a NAWS update? - if NAWS in data and len([data[i:i+1] for i in range(0, len(data))]) == 9: + if (NAWS in data and + len([data[i:i+1] for i in range(0, len(data))]) == 9 and + # Is auto resizing on? + self.protocol_flags.get('AUTORESIZE')): self.sessionhandler.sync(self.sessionhandler.get(self.sessid)) + super().dataReceived(data) except ValueError as err: from evennia.utils import logger From 289df257e5c5df44e1291ae0d999f4153b443f80 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Wed, 10 Apr 2024 13:07:54 +1000 Subject: [PATCH 028/112] Fix room creation showing room name instead of typeclass path --- evennia/commands/default/building.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 558d96b3b5..a457cac7a7 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1028,7 +1028,7 @@ class CmdDig(ObjManipCommand): if new_room.aliases.all(): alias_string = " (%s)" % ", ".join(new_room.aliases.all()) - room_string = f"Created room {new_room}({new_room.dbref}){alias_string} of type {new_room}." + room_string = f"Created room {new_room}({new_room.dbref}){alias_string} of type {new_room.typeclass_path}." # create exit to room From a38fdc90dc3c758568297cf90f0878add702dc61 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Fri, 12 Apr 2024 13:56:43 +1000 Subject: [PATCH 029/112] Fix inability to edit, delete (etc.) topics whose names overlap non-db topic names. --- evennia/commands/default/help.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index a1ec71caf0..c97a790375 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -932,7 +932,17 @@ class CmdSetHelp(CmdHelp): # types of entries. self.msg(f"|rWarning:\n|r{warning}|n") repl = yield ("|wDo you still want to continue? Y/[N]?|n") - if repl.lower() not in ("y", "yes"): + if repl.lower() in ("y", "yes"): + # find a db-based help entry if one already exists + db_topics = {**db_help_topics} + db_categories = list( + set(HelpCategory(topic.help_category) for topic in db_topics.values()) + ) + entries = list(db_topics.values()) + db_categories + match, _ = self.do_search(querystr, entries) + if match: + old_entry = match + else: self.msg("Aborted.") return else: From 72f2b6940ac22bfc80f02db86f3d583daab87087 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Fri, 12 Apr 2024 19:15:29 +1000 Subject: [PATCH 030/112] Add sethelp/locks command --- evennia/commands/default/help.py | 37 ++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index a1ec71caf0..db3d182684 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -17,6 +17,7 @@ from django.conf import settings from evennia.help.filehelp import FILE_HELP_ENTRIES from evennia.help.models import HelpEntry from evennia.help.utils import help_search_with_index, parse_entry_for_subcategories +from evennia.locks.lockhandler import LockException from evennia.utils import create, evmore from evennia.utils.ansi import ANSIString from evennia.utils.eveditor import EvEditor @@ -781,13 +782,14 @@ class CmdSetHelp(CmdHelp): Usage: sethelp[/switches] [[;alias;alias][,category[,locks]] - [= ] + [= ] Switches: edit - open a line editor to edit the topic's help text. replace - overwrite existing help topic. append - add text to the end of existing topic with a newline between. extend - as append, but don't add a newline. category - change category of existing help topic. + locks - change locks of existing help topic. delete - remove help topic. Examples: @@ -795,6 +797,7 @@ class CmdSetHelp(CmdHelp): sethelp/append pickpocketing,Thievery = This steals ... sethelp/replace pickpocketing, ,attr(is_thief) = This steals ... sethelp/edit thievery + sethelp/locks thievery = read:all() sethelp/category thievery = classes If not assigning a category, the `settings.DEFAULT_HELP_CATEGORY` category @@ -842,7 +845,7 @@ class CmdSetHelp(CmdHelp): key = "sethelp" aliases = [] - switch_options = ("edit", "replace", "append", "extend", "category", "delete") + switch_options = ("edit", "replace", "append", "extend", "category", "locks", "delete") locks = "cmd:perm(Helper)" help_category = "Building" arg_regex = None @@ -856,6 +859,7 @@ class CmdSetHelp(CmdHelp): switches = self.switches lhslist = self.lhslist + rhslist = self.rhslist if not self.args: self.msg( @@ -1001,6 +1005,35 @@ class CmdSetHelp(CmdHelp): self.msg(f"Category for entry '{topicstr}'{aliastxt} changed to '{category}'.") return + if "locks" in switches: + # set the locks + if not old_entry: + self.msg(f"Could not find topic '{topicstr}'{aliastxt}.") + return + show_locks = not rhslist + clear_locks = rhslist and not rhslist[0] + if show_locks: + self.msg(f"Current locks for entry '{topicstr}'{aliastxt} are: {old_entry.locks}") + return + if clear_locks: + old_entry.locks.clear() + old_entry.locks.add("read:all()") + self.msg(f"Locks for entry '{topicstr}'{aliastxt} reset to: read:all()") + return + lockstring = ",".join(rhslist) + # locks.validate() does not throw an exception for things like "read:id(1),read:id(6)" + # but locks.add() does + existing_locks = old_entry.locks.all() + old_entry.locks.clear() + try: + old_entry.locks.add(lockstring) + except LockException as e: + old_entry.locks.add(existing_locks) + self.msg(str(e) + " Locks not changed.") + else: + self.msg(f"Locks for entry '{topicstr}'{aliastxt} changed to: {lockstring}") + return + if "delete" in switches or "del" in switches: # delete the help entry if not old_entry: From ac8075385fb08831178690842648722d1a7c137d Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sat, 13 Apr 2024 23:07:39 +1000 Subject: [PATCH 031/112] Fix traceback when setting prototype parent --- evennia/prototypes/menus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index d7644b3f9d..58772d2f29 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -835,7 +835,7 @@ def _prototype_parent_actions(caller, raw_inp, **kwargs): return "node_prototype_parent" -def _prototype_parent_select(caller, new_parent): +def _prototype_parent_select(caller, new_parent, **kwargs): ret = None prototype_parent = protlib.search_prototype(new_parent) try: From 3e2995a152e86bafae4c07b3244abb946602d284 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sun, 14 Apr 2024 01:40:47 +1000 Subject: [PATCH 032/112] Fix protoype object updates changing cached prototype. Resolves #3505 --- 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 b77651a6db..3dfad73b31 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -323,7 +323,7 @@ def prototype_from_object(obj): prot["prototype_locks"] = "spawn:all();edit:all()" prot["prototype_tags"] = [] else: - prot = prot[0] + prot = prot[0].copy() prot["key"] = obj.db_key or hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:6] prot["typeclass"] = obj.db_typeclass_path From f755f052d3daf7a536f0b1bccccd041b774d0769 Mon Sep 17 00:00:00 2001 From: Cal Date: Sun, 21 Apr 2024 15:18:09 -0600 Subject: [PATCH 033/112] add mapping support, pronoun conjugation to actor stance callables --- docs/source/Components/FuncParser.md | 10 +- evennia/utils/funcparser.py | 112 +++++++++++++++++--- evennia/utils/tests/test_funcparser.py | 10 ++ evennia/utils/verb_conjugation/conjugate.py | 11 +- 4 files changed, 119 insertions(+), 24 deletions(-) diff --git a/docs/source/Components/FuncParser.md b/docs/source/Components/FuncParser.md index 05aee2d509..458bbeb3dc 100644 --- a/docs/source/Components/FuncParser.md +++ b/docs/source/Components/FuncParser.md @@ -337,13 +337,17 @@ Here the `caller` is the one sending the message and `receiver` the one to see i result of `you_obj.get_display_name(looker=receiver)`. This allows for a single string to echo differently depending on who sees it, and also to reference other people in the same way. - `$You([key])` - same as `$you` but always capitalized. -- `$conj(verb)` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) - conjugates a verb - between 2nd person presens to 3rd person presence depending on who +- `$conj(verb [,key])` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) - conjugates a verb + between 2nd person presence to 3rd person presence depending on who sees the string. For example `"$You() $conj(smiles)".` will show as "You smile." and "Tom smiles." depending on who sees it. This makes use of the tools in [evennia.utils.verb_conjugation](evennia.utils.verb_conjugation) to do this, and only works for English verbs. -- `$pron(pronoun [,options])` ([code](evennia.utils.funcparser.funcparser_callable_pronoun)) - Dynamically +- `$pron(pronoun [,options] [,key])` ([code](evennia.utils.funcparser.funcparser_callable_pronoun)) - Dynamically map pronouns (like his, herself, you, its etc) between 1st/2nd person to 3rd person. +- `$pconj(verb, [,key])` ([code](evennia.utils.funcparser.funcparser_callable_conjugate_for_pronouns)) - conjugates + a verb between 2nd and 3rd person, like `$conj`, but for pronouns instead of nouns to account for plural + gendering. For example `"$Pron(you) $pconj(smiles)"` will show to others as "He smiles" for a gender of "male", or + "They smile" for a gender of "plural". ### `evennia.prototypes.protfuncs` diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index 2c434badcf..bca394530f 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -1320,15 +1320,18 @@ def funcparser_callable_your_capitalize( ) -def funcparser_callable_conjugate(*args, caller=None, receiver=None, **kwargs): +def funcparser_callable_conjugate(*args, caller=None, receiver=None, mapping=None, **kwargs): """ - Usage: $conj(word, [options]) + Usage: $conj(word, [key]) Conjugate a verb according to if it should be 2nd or third person. Keyword Args: caller (Object): The object who represents 'you' in the string. receiver (Object): The recipient of the string. + mapping (dict, optional): This is a mapping `{key:Object, ...}` and is + used to find which object the optional `key` argument refers to. If not given, + the `caller` kwarg is used. Returns: str: The parsed string. @@ -1337,13 +1340,10 @@ def funcparser_callable_conjugate(*args, caller=None, receiver=None, **kwargs): ParsingError: If `you` and `recipient` were not both supplied. Notes: - Note that the verb will not be capitalized. It also - assumes that the active party (You) is the one performing the verb. - This automatic conjugation will fail if the active part is another person - than 'you'. The caller/receiver must be passed to the parser directly. - + Note that the verb will not be capitalized. + Examples: - This is often used in combination with the $you/You( callables. + This is often used in combination with the $you/You callables. - `With a grin, $you() $conj(jump)` @@ -1356,14 +1356,76 @@ def funcparser_callable_conjugate(*args, caller=None, receiver=None, **kwargs): if not (caller and receiver): raise ParsingError("No caller/receiver supplied to $conj callable") - second_person_str, third_person_str = verb_actor_stance_components(args[0]) - return second_person_str if caller == receiver else third_person_str + verb, *options = args + obj = caller + if mapping and options: + # get the correct referenced object from the mapping, or fall back to caller + obj = mapping.get(options[0], caller) + + second_person_str, third_person_str = verb_actor_stance_components(verb) + return second_person_str if obj == receiver else third_person_str -def funcparser_callable_pronoun(*args, caller=None, receiver=None, capitalize=False, **kwargs): +def funcparser_callable_conjugate_for_pronouns(*args, caller=None, receiver=None, mapping=None, **kwargs): + """ + Usage: $pconj(word, [key]) + + Conjugate a verb according to if it should be 2nd or third person, respecting the + singular/plural gendering for third person. + + Keyword Args: + caller (Object): The object who represents 'you' in the string. + receiver (Object): The recipient of the string. + mapping (dict, optional): This is a mapping `{key:Object, ...}` and is + used to find which object the optional `key` argument refers to. If not given, + the `caller` kwarg is used. + + Returns: + str: The parsed string. + + Raises: + ParsingError: If `you` and `recipient` were not both supplied. + + Notes: + Note that the verb will not be capitalized. + + Examples: + This is often used in combination with the $pron/Pron callables. + + - `With a grin, $pron(you) $pconj(jump)` + + You will see "With a grin, you jump." + With your gender as "male", others will see "With a grin, he jumps." + With your gender as "plural", others will see "With a grin, they jump." + + """ + if not args: + return "" + if not (caller and receiver): + raise ParsingError("No caller/receiver supplied to $conj callable") + + verb, *options = args + obj = caller + if mapping and options: + # get the correct referenced object from the mapping, or fall back to caller + obj = mapping.get(options[0], caller) + + # identify whether the 3rd person form should be singular or plural + plural = False + if hasattr(obj, "gender"): + if callable(obj.gender): + plural = (obj.gender() == "plural") + else: + plural = (obj.gender == "plural") + + second_person_str, third_person_str = verb_actor_stance_components(verb, plural=plural) + return second_person_str if obj == receiver else third_person_str + + +def funcparser_callable_pronoun(*args, caller=None, receiver=None, mapping=None, capitalize=False, **kwargs): """ - Usage: $pron(word, [options]) + Usage: $pron(word, [options], [key]) Adjust pronouns to the expected form. Pronouns are words you use instead of a proper name, such as 'him', 'herself', 'theirs' etc. These look different @@ -1424,6 +1486,9 @@ def funcparser_callable_pronoun(*args, caller=None, receiver=None, capitalize=Fa - `1st person`/`1st`/`1` - `2nd person`/`2nd`/`2` - `3rd person`/`3rd`/`3` + key (str, optional): If a mapping is provided, a string defining which object to + reference when finding the correct pronoun. If not provided, it defaults + to `caller` Keyword Args: @@ -1435,6 +1500,9 @@ def funcparser_callable_pronoun(*args, caller=None, receiver=None, capitalize=Fa receiver (Object): The recipient of the string. This being the same as `caller` or not helps determine 2nd vs 3rd-person forms. This is provided automatically by the funcparser. + mapping (dict, optional): This is a mapping `{key:Object, ...}` and is + used to find which object the optional `key` argument refers to. If not given, + the `caller` kwarg is used. capitalize (bool): The input retains its capitalization. If this is set the output is always capitalized. @@ -1457,8 +1525,17 @@ def funcparser_callable_pronoun(*args, caller=None, receiver=None, capitalize=Fa """ if not args: return "" + # by default, we use the caller as the object being referred to + obj = caller pronoun, *options = args + if options and mapping: + # check if the last argument is a valid mapping key + if options[-1] in mapping: + # get the object and remove the key from options + obj = mapping[options[-1]] + options = options[:-1] + # options is either multiple args or a space-separated string if len(options) == 1: options = options[0] @@ -1468,11 +1545,11 @@ def funcparser_callable_pronoun(*args, caller=None, receiver=None, capitalize=Fa default_gender = "neutral" default_viewpoint = "2nd person" - if hasattr(caller, "gender"): - if callable(caller.gender): - default_gender = caller.gender() + if hasattr(obj, "gender"): + if callable(obj.gender): + default_gender = obj.gender() else: - default_gender = caller.gender + default_gender = obj.gender if "viewpoint" in kwargs: # passed into FuncParser initialization @@ -1490,7 +1567,7 @@ def funcparser_callable_pronoun(*args, caller=None, receiver=None, capitalize=Fa pronoun_1st_or_2nd_person = pronoun_1st_or_2nd_person.capitalize() pronoun_3rd_person = pronoun_3rd_person.capitalize() - return pronoun_1st_or_2nd_person if caller == receiver else pronoun_3rd_person + return pronoun_1st_or_2nd_person if obj == receiver else pronoun_3rd_person def funcparser_callable_pronoun_capitalize( @@ -1557,6 +1634,7 @@ ACTOR_STANCE_CALLABLES = { "obj": funcparser_callable_you, "Obj": funcparser_callable_you_capitalize, "conj": funcparser_callable_conjugate, + "pconj": funcparser_callable_conjugate_for_pronouns, "pron": funcparser_callable_pronoun, "Pron": funcparser_callable_pronoun_capitalize, **FUNCPARSER_CALLABLES, diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index 7b13926ede..1b2fd41da1 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -435,6 +435,7 @@ class TestDefaultCallables(TestCase): ("$You() $conj(smile) at him.", "You smile at him.", "Char1 smiles at him."), ("$You() $conj(smile) at $You(char1).", "You smile at You.", "Char1 smiles at Char1."), ("$You() $conj(smile) at $You(char2).", "You smile at Char2.", "Char1 smiles at You."), + ("$You() $conj(smile) while $You(char2) $conj(waves, char2).", "You smile while Char2 waves.", "Char1 smiles while You wave."), ( "$You(char2) $conj(smile) at $you(char1).", "Char2 smile at you.", @@ -512,6 +513,15 @@ class TestDefaultCallables(TestCase): ret = self.parser.parse(string, caller=self.obj1, raise_errors=True) self.assertEqual(expected, ret) + def test_pronoun_mapping(self): + self.obj1.gender = "female" + self.obj2.gender = "male" + + string = "Char1 raises $pron(your, char1) fist as Char2 raises $pron(yours, char2)" + expected = "Char1 raises her fist as Char2 raises his" + ret = self.parser.parse(string, caller=self.obj1, mapping={'char1': self.obj1, 'char2': self.obj2}, raise_errors=True) + self.assertEqual(expected, ret) + def test_pronoun_viewpoint(self): string = "Char1 smiles at $pron(I)" diff --git a/evennia/utils/verb_conjugation/conjugate.py b/evennia/utils/verb_conjugation/conjugate.py index 20043b4e09..21ec134ced 100644 --- a/evennia/utils/verb_conjugation/conjugate.py +++ b/evennia/utils/verb_conjugation/conjugate.py @@ -365,25 +365,28 @@ def verb_is_past_participle(verb): return tense == "past participle" -def verb_actor_stance_components(verb): +def verb_actor_stance_components(verb, plural=False): """ Figure out actor stance components of a verb. Args: verb (str): The verb to analyze + plural (bool): Whether to force 3rd person to plural form Returns: tuple: The 2nd person (you) and 3rd person forms of the verb, in the same tense as the ingoing verb. - """ tense = verb_tense(verb) + them = "*" if plural else "3" + them_suff = "" if plural else "s" + if "participle" in tense or "plural" in tense: return (verb, verb) if tense == "infinitive" or "present" in tense: you_str = verb_present(verb, person="2") or verb - them_str = verb_present(verb, person="3") or verb + "s" + them_str = verb_present(verb, person=them) or verb + them_suff else: you_str = verb_past(verb, person="2") or verb - them_str = verb_past(verb, person="3") or verb + "s" + them_str = verb_past(verb, person=them) or verb + them_suff return (you_str, them_str) From 7589aaf760c2090f52012944df2607a2e3ff5eea Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:08:08 -0600 Subject: [PATCH 034/112] auto-look when quitting mid chargen --- evennia/contrib/rpg/character_creator/character_creator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/rpg/character_creator/character_creator.py b/evennia/contrib/rpg/character_creator/character_creator.py index 994093b427..7fae70332d 100644 --- a/evennia/contrib/rpg/character_creator/character_creator.py +++ b/evennia/contrib/rpg/character_creator/character_creator.py @@ -85,7 +85,10 @@ class ContribCmdCharCreate(MuxAccountCommand): # this gets called every time the player exits the chargen menu def finish_char_callback(session, menu): char = session.new_char - if not char.db.chargen_step: + if char.db.chargen_step: + # this means the character creation process was exited in the middle + account.execute_cmd("look") + else: # this means character creation was completed - start playing! # execute the ic command to start puppeting the character account.execute_cmd("ic {}".format(char.key)) From 9189b01d3416ba7214c211fda7bd2393abe54c58 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:28:15 -0600 Subject: [PATCH 035/112] don't ignore empty search candidates --- evennia/objects/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index dcf9b49f90..34d93f2869 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -397,7 +397,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # if candidates were already given, use them candidates = kwargs.get("candidates") - if candidates: + if candidates is not None: return candidates # find candidates based on location From 328a87b82cef806cd98d4fcff5efe500841f1f63 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Tue, 23 Apr 2024 20:26:28 -0600 Subject: [PATCH 036/112] correct GLOBAL_SCRIPTS.all() return --- evennia/utils/containers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/utils/containers.py b/evennia/utils/containers.py index 26e4f0ca69..4eec3f042c 100644 --- a/evennia/utils/containers.py +++ b/evennia/utils/containers.py @@ -244,7 +244,7 @@ class GlobalScriptContainer(Container): """ if not self.loaded: self.load_data() - return self.scripts.values() + return list(self.loaded_data.values()) # Create all singletons From c0d7de79b79cd15edf77fceb2900c6ec5978017b Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Thu, 25 Apr 2024 20:31:51 +1000 Subject: [PATCH 037/112] Fix exit order sorting sometimes being incorrect for exits not in the sort order --- evennia/objects/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index dcf9b49f90..b5644bb840 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1595,7 +1595,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): return names sort_index = {name: key for key, name in enumerate(exit_order)} names = sorted(names) - end_pos = len(names) + 1 + end_pos = len(sort_index) names.sort(key=lambda name: sort_index.get(name, end_pos)) return names From b0fff26a4f7e405337fd98d10e28af962ad18fc9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 27 Apr 2024 19:16:43 +0200 Subject: [PATCH 038/112] Fix ObjManipCommand docstring. Update Changelog --- CHANGELOG.md | 11 ++++++ evennia/commands/default/building.py | 52 +++++++++++++--------------- evennia/commands/default/comms.py | 1 - 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25b114905a..b18dde0403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,24 @@ - [Feature][pull3470]: New `exit_order` kwarg to `DefaultObject.get_display_exits` to easier customize the order in which standard exits are displayed in a room (chiizujin) +- [Feature][pull3498]: Properly update Evennia's screen width when client + changes width (assuming client supports NAWS properly) (michaelfaith84) - [Fix][pull3495]: Fix rate in Trait contribs not updating after reload (jaborsh) - [Fix][pull3491]: Fix traceback in EvEditor when searching with malformed regex (chiizujin) +- [Fix][pull3489]: Superuser could break wilderness contrib exits (t34lbytes) +- [Fix][pull3496]: EvEditor would not correctly show search&replace feedback + when replacing colors (Chiizujin) +- [Fix][pull3499]: Dig/tunnel commands didn't echo the typeclass of the newly + created room properly (chiizujin) - [Docs]: Doc fixes (Griatch, chiizujin) [pull3470]: https://github.com/evennia/evennia/pull/3470 [pull3495]: https://github.com/evennia/evennia/pull/3495 [pull3491]: https://github.com/evennia/evennia/pull/3491 +[pull3489]: https://github.com/evennia/evennia/pull/3489 +[pull3496]: https://github.com/evennia/evennia/pull/3496 +[pull3498]: https://github.com/evennia/evennia/pull/3498 +[pull3499]: https://github.com/evennia/evennia/pull/3499 ## Evennia 4.1.1 diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index a457cac7a7..7e72ad23e9 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -5,13 +5,13 @@ Building and world design commands import re import typing +import evennia from django.conf import settings from django.core.paginator import Paginator from django.db.models import Max, Min, Q - -import evennia from evennia import InterruptCommand -from evennia.commands.cmdhandler import generate_cmdset_providers, get_and_merge_cmdsets +from evennia.commands.cmdhandler import (generate_cmdset_providers, + get_and_merge_cmdsets) from evennia.locks.lockhandler import LockException from evennia.objects.models import ObjectDB from evennia.prototypes import menus as olc_menus @@ -24,18 +24,10 @@ from evennia.utils.dbserialize import deserialize from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.utils.evtable import EvTable -from evennia.utils.utils import ( - class_from_module, - crop, - dbref, - display_len, - format_grid, - get_all_typeclasses, - inherits_from, - interactive, - list_to_string, - variable_from_module, -) +from evennia.utils.utils import (class_from_module, crop, dbref, display_len, + format_grid, get_all_typeclasses, + inherits_from, interactive, list_to_string, + variable_from_module) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -178,26 +170,30 @@ class ObjManipCommand(COMMAND_DEFAULT_CLASS): def get_object_typeclass( self, obj_type: str = "object", typeclass: str = None, method: str = "cmd_create", **kwargs - ) -> tuple[typing.Optional["Builder"], list[str]]: + ) -> tuple[typing.Optional['Typeclass'], list[str]]: """ - This hook is called by build commands to determine which typeclass to use for a specific purpose. For instance, - when using dig, the system can use this to autodetect which kind of Room typeclass to use based on where the - builder is currently located. - - Note: Although intended to be used with typeclasses, as long as this hook returns a class with a create method, - which accepts the same API as DefaultObject.create(), build commands and other places should take it. + This hook is called by build commands to determine which typeclass to use for a specific + purpose. Args: - obj_type (str, optional): The type of object that is being created. Defaults to "object". Evennia provides - "room", "exit", and "character" by default, but this can be extended. - typeclass (str, optional): The typeclass that was requested by the player. Defaults to None. - Can also be an actual class. + obj_type (str, optional): The type of object that is being created. Defaults to + "object". Evennia provides "room", "exit", and "character" by default, but this can be + extended. + typeclass (str, optional): The typeclass that was requested by the player. Defaults to + None. Can also be an actual class. method (str, optional): The method that is calling this hook. Defaults to "cmd_create". Others are "cmd_dig", "cmd_open", "cmd_tunnel", etc. Returns: - results_tuple (tuple[Optional[Builder], list[str]]): A tuple containing the typeclass to use and a list of - errors. (which might be empty.) + tuple: A tuple containing the typeclass to use and a list of errors. (which might be + empty.) + + Notes: + Although intended to be used with typeclasses, as long as this hook returns a class with + a create method, which accepts the same API as DefaultObject.create(), build commands + and other places should take it. While not used by default, one could picture using this + for things like autodetecting which room to build next based on the current location. + """ found_typeclass = typeclass or self.default_typeclasses.get(obj_type, None) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index cac9905a5a..521eef7291 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -9,7 +9,6 @@ Communication commands: from django.conf import settings from django.db.models import Q - from evennia.accounts import bots from evennia.accounts.models import AccountDB from evennia.comms.comms import DefaultChannel From 22a476d017aca106e695f9185cc2e505c0c91fc3 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Sat, 27 Apr 2024 12:36:41 -0600 Subject: [PATCH 039/112] support full capitalization in GMCP commands --- evennia/server/portal/telnet_oob.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/evennia/server/portal/telnet_oob.py b/evennia/server/portal/telnet_oob.py index 6fe68ff7a1..37c1f8067c 100644 --- a/evennia/server/portal/telnet_oob.py +++ b/evennia/server/portal/telnet_oob.py @@ -257,11 +257,8 @@ class TelnetOOB: if cmdname in EVENNIA_TO_GMCP: gmcp_cmdname = EVENNIA_TO_GMCP[cmdname] elif "_" in cmdname: - if cmdname.istitle(): - # leave without capitalization - gmcp_cmdname = ".".join(word for word in cmdname.split("_")) - else: - gmcp_cmdname = ".".join(word.capitalize() for word in cmdname.split("_")) + # enforce initial capitalization of each command part, leaving fully-capitalized sections intact + gmcp_cmdname = ".".join(word.capitalize() if not word.isupper() else word for word in cmdname.split("_")) else: gmcp_cmdname = "Core.%s" % (cmdname if cmdname.istitle() else cmdname.capitalize()) From 49330826f3e5d0ea511b7f44f031593c32f631c1 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 27 Apr 2024 20:56:38 +0200 Subject: [PATCH 040/112] Updated changelog --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b18dde0403..77d13d193c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ standard exits are displayed in a room (chiizujin) - [Feature][pull3498]: Properly update Evennia's screen width when client changes width (assuming client supports NAWS properly) (michaelfaith84) +- [Feature][pull3502]: New `sethelp/locks` allows for editing help entry + locks after they were first created (chiizujin) +- [Feature][pull3503]: `page` now shows timestamps in local time instead of in UTC. +- [Feature][pull3514]: Support `$pron(pronoun, key)` and new `$pconj(verb, key)` + (pronoun conjugation) for actor stance (InspectorCaracal) +- [Feature][pull3521]: Allow `WORD` (fully capitalized) in GMCP command names + instead of only `Word` (titled) to support specific clients better (InspectorCaracal) - [Fix][pull3495]: Fix rate in Trait contribs not updating after reload (jaborsh) - [Fix][pull3491]: Fix traceback in EvEditor when searching with malformed regex (chiizujin) - [Fix][pull3489]: Superuser could break wilderness contrib exits (t34lbytes) @@ -14,6 +21,16 @@ when replacing colors (Chiizujin) - [Fix][pull3499]: Dig/tunnel commands didn't echo the typeclass of the newly created room properly (chiizujin) +- [Fix][pull3501]: Using `sethelp` to create a help entry colliding with a + command-name made the entry impossible to edit/delete later (chiizujin) +- [Fix][pull3506]: Fix Traceback when setting prototype parent in the in-game OLC wizard (chiizujin) +- [Fix][pull3507]: Prototype wizard would not save changes if aborting the + updating of existing spawned instances (chiizujun) +- [Fix][pull3516]: Quitting the chargen contrib menu will now trigger auto-look (InspectorCaracal) +- [Fix][pull3517]: Supply `Object.search` with an empty `candidates` list caused + defaults to be used instead of finding nothing (InspectorCaracal) +- [Fix][pull3518]: `GlobalScriptsContainer.all()` raised a traceback (InspectorCaracal) +- [Fix][pull3520]: Exits not included in exit sort order were not listed correctly (chiizujin) - [Docs]: Doc fixes (Griatch, chiizujin) [pull3470]: https://github.com/evennia/evennia/pull/3470 @@ -23,6 +40,17 @@ [pull3496]: https://github.com/evennia/evennia/pull/3496 [pull3498]: https://github.com/evennia/evennia/pull/3498 [pull3499]: https://github.com/evennia/evennia/pull/3499 +[pull3501]: https://github.com/evennia/evennia/pull/3501 +[pull3502]: https://github.com/evennia/evennia/pull/3502 +[pull3503]: https://github.com/evennia/evennia/pull/3503 +[pull3506]: https://github.com/evennia/evennia/pull/3506 +[pull3507]: https://github.com/evennia/evennia/pull/3507 +[pull3514]: https://github.com/evennia/evennia/pull/3514 +[pull3516]: https://github.com/evennia/evennia/pull/3516 +[pull3517]: https://github.com/evennia/evennia/pull/3517 +[pull3518]: https://github.com/evennia/evennia/pull/3518 +[pull3520]: https://github.com/evennia/evennia/pull/3520 +[pull3521]: https://github.com/evennia/evennia/pull/3521 ## Evennia 4.1.1 From fa17412687cef71e5b37bff8ef2700bb296dd97d Mon Sep 17 00:00:00 2001 From: Cal Date: Sat, 27 Apr 2024 13:46:10 -0600 Subject: [PATCH 041/112] revise achievements docs --- .../game_systems/achievements/README.md | 175 ++++++++++++------ 1 file changed, 123 insertions(+), 52 deletions(-) diff --git a/evennia/contrib/game_systems/achievements/README.md b/evennia/contrib/game_systems/achievements/README.md index a406dcd6f0..55bae77aef 100644 --- a/evennia/contrib/game_systems/achievements/README.md +++ b/evennia/contrib/game_systems/achievements/README.md @@ -4,24 +4,70 @@ A simple, but reasonably comprehensive, system for tracking achievements. Achiev The contrib provides several functions for tracking and accessing achievements, as well as a basic in-game command for viewing achievement status. +## Installation + +This contrib requires creation one or more module files containing your achievement data, which you then add to your settings file to make them available. + +> See the section below on "Creating Achievements" for what to put in this module. + +```python +# in server/conf/settings.py + +ACHIEVEMENT_CONTRIB_MODULES = ["world.achievements"] +``` + +To allow players to check their achievements, you'll also want to add the `achievements` command to your default Character and/or Account command sets. + +```python +# in commands/default_cmdsets.py + +from evennia.contrib.game_systems.achievements.achievements import CmdAchieve + +class CharacterCmdSet(default_cmds.CharacterCmdSet): + key = "DefaultCharacter" + + def at_cmdset_creation(self): + # ... + self.add(CmdAchieve) +``` + +**Optional** - The achievements contrib stores individual progress data on the `achievements` attribute by default, visible via `obj.db.attributes`. You can change this by assigning an attribute (key, category) tuple to the setting `ACHIEVEMENT_CONTRIB_ATTRIBUTE` + +Example: +```py +# in settings.py + +ACHIEVEMENT_CONTRIB_ATTRIBUTE = ("progress_data", "achievements") +``` + + ## Creating achievements -This achievement system is designed to use ordinary dicts for the achievement data - however, there are certain keys which, if present in the dict, define how the achievement is progressed or completed. +An achievement is represented by a simple python dictionary defined at the module level in your achievements module(s). + +Each achievement requires certain specific keys to be defined to work properly, along with several optional keys that you can use to override defaults. + +> Note: Any additional keys not described here are included in the achievement data when you access those acheivements through the contrib, so you can easily add your own extended features. + +#### Required keys + +- **name** (str): The searchable name for the achievement. Doesn't need to be unique. +- **category** (str): The category, or general type, of condition which can progress this achievement. Usually this will be a player action or result. e.g. you would use a category of "defeat" on an achievement for killing 10 rats. +- **tracking** (str or list): The specific subset of condition which can progress this achievement. e.g. you would use a tracking value of "rat" on an achievement for killing 10 rats. An achievement can also track multiple things, for example killing 10 rats or snakes. For that situation, assign a list of all the values to check against, e.g. `["rat", "snake"]` + +#### Optional keys - **key** (str): *Default value if unset: the variable name.* The unique, case-insensitive key identifying this achievement. -- **name** (str): The searchable name for the achievement. Doesn't need to be unique. +> Note: If any achievements have the same unique key, only *one* will be loaded. It is case-insensitive, but punctuation is respected - "ten_rats", "Ten_Rats" and "TEN_RATS" will conflict, but "ten_rats" and "ten rats" will not. - **desc** (str): A longer description of the achievement. Common uses for this would be flavor text or hints on how to complete it. -- **category** (str): The category of conditions which this achievement tracks. It will most likely be an action and you will most likely specify it based on where you're checking from. e.g. killing 10 rats might have a category of "defeat", which you'd then check from your code that runs when a player defeats something. -- **tracking** (str or list): The *specific* condition this achievement tracks. e.g. the previous example of killing rats might have a `"tracking"` value of `"rat"`. This value will most likely be taken from a specific object in your code, like a tag on the defeated object, or the ID of a visited location. An achievement can also track multiple things: instead of only tracking buying apples, you might want to track apples and pears. For that situation, you can assign it to a list of values to check against: e.g. `["apple", "pear"]` -- **tracking_type** (str): *Default value if unset: `"sum"`* There are two valid tracking types: "sum" (which is the default) and "separate". `"sum"` will increment a single counter every time any of the tracked items match. `"separate"` will have a counter for each individual item in the tracked items. (This is really only useful when `"tracking"` is a list.) -- **count** (int): *Default value if unset: 1* The number of tallies this achievement's requirements need to build up in order to complete the achievement. e.g. the previous example of killing rats would have a `"count"` value of `10`. For achievements using the "separate" tracking type, *each* item being tracked must tally up to this number to be completed -- **prereqs** (str or list): The *keys* of any achievements which must be completed before this achievement can start tracking progress. An achievement's key is the variable name it's assigned to in your achievement module. +- **count** (int): *Default value if unset: 1* The number of tallies this achievement's requirements need to build up in order to complete the achievement. e.g. killing 10 rats would have a `"count"` value of `10`. For achievements using the "separate" tracking type, *each* item being tracked must tally up to this number to be completed. +- **tracking_type** (str): *Default value if unset: `"sum"`* There are two valid tracking types: "sum" (which is the default) and "separate". `"sum"` will increment a single counter every time any of the tracked items match. `"separate"` will have a counter for each individual item in the tracked items. ("See the Example Achievements" section for a demonstration of the difference.) +- **prereqs** (str or list): The *keys* of any achievements which must be completed before this achievement can start tracking progress. -You can add any additional keys to your achievement dicts that you want, and they'll be included with all the rest of the achievement data when using the contrib's functions. This could be useful if you want to add extra metadata to your achievements for your own features. ### Example achievements -A simple achievement which you can get just for logging in the first time. This achievement has no prerequisites and it only needs to be fulfilled once to complete, so it doesn't need to define most of the fields. +A simple achievement which you can get just for logging in the first time. This achievement has no prerequisites and it only needs to be fulfilled once to complete. ```python # This achievement has the unique key of "first_login_achieve" FIRST_LOGIN_ACHIEVE = { @@ -32,7 +78,7 @@ FIRST_LOGIN_ACHIEVE = { } ``` -An achievement for killing 10 rats, and another for killing 10 *dire* rats which requires the "kill 10 rats" achievement to be completed first. +An achievement for killing a total of 10 rats, and another for killing 10 *dire* rats which requires the "kill 10 rats" achievement to be completed first. The dire rats achievement won't begin tracking *any* progress until the first achievement is completed. ```python # This achievement has the unique key of "ten_rats" instead of "achieve_ten_rats" ACHIEVE_TEN_RATS = { @@ -54,10 +100,23 @@ ACHIEVE_DIRE_RATS = { } ``` -An achievement for buying 5 each of apples, oranges, and pears. +An achievement for buying a total of 5 of apples, oranges, *or* pears. The "sum" tracking types means that all items are tallied together - so it can be completed by buying 5 apples, or 5 pears, or 3 apples, 1 orange and 1 pear, or any other combination of those three fruits that totals to 5. + +```python +FRUIT_FAN_ACHIEVEMENT = { + "name": "A Fan of Fruit", # note, there is no desc here - that's allowed! + "category": "buy", + "tracking": ("apple", "orange", "pear"), + "count": 5, + "tracking_type": "sum", # this is the default, but it's included here for clarity +} +``` + +An achievement for buying 5 *each* of apples, oranges, and pears. The "separate" tracking type means that each of the tracked items is tallied independently of the other items - so you will need 5 apples, 5 oranges, and 5 pears. ```python FRUIT_BASKET_ACHIEVEMENT = { - "name": "A Fan of Fruit", # note, there is no desc here - that's allowed! + "name": "Fruit Basket", + "desc": "One kind of fruit just isn't enough.", "category": "buy", "tracking": ("apple", "orange", "pear"), "count": 5, @@ -66,39 +125,23 @@ FRUIT_BASKET_ACHIEVEMENT = { ``` -## Installation - -Once you've defined your achievement dicts in one or more module, assign that module to the `ACHIEVEMENT_CONTRIB_MODULES` setting in your settings.py - -> Note: If any achievements have the same unique key, whichever conflicting achievement is processed *last* will be the only one loaded into the game. Case is ignored, so "ten_rats", "Ten_Rats" and "TEN_RATS" will conflict. "ten_rats" and "ten rats" will not. - -```python -# in server/conf/settings.py - -ACHIEVEMENT_CONTRIB_MODULES = ["world.achievements"] -``` - -There is also a command available to let players check their achievements - `from evennia.contrib.game_systems.achievements.achievements import CmdAchieve` and then add `CmdAchieve` to your default Character and/or Account cmdsets. - -**Optional** - The achievements contrib stores individual progress data on the `achievements` attribute by default, visible via `obj.db.attributes`. You can change this in your settings if necessary, e.g.: - -```py -# in settings.py - -ACHIEVEMENT_CONTRIB_ATTRIBUTE = ("achievement_data", "systems") -``` - ## Usage -### `track_achievements` +The two main things you'll need to do in order to use the achievements contrib in your game are **tracking achievements** and **getting achievement information**. The first is done with the function `track_achievements`; the second can be done with `search_achievement` or `get_achievement`. -The primary mechanism for using the achievements system is the `track_achievements` function. In any actions or functions in your game's mechanics which you might want to track in an achievement, add a call to `track_achievements` to update the achievement progress for that individual. +### Tracking achievements + +#### `track_achievements` + +In any actions or functions in your game's mechanics which you might want to track in an achievement, add a call to `track_achievements` to update that player's achievement progress. Using the "kill 10 rats" example achievement from earlier, you might have some code that triggers when a character is defeated: for the sake of example, we'll pretend we have an `at_defeated` method on the base Object class that gets called when the Object is defeated. Adding achievement tracking to it could then look something like this: ```python +# in typeclasses/objects.py + from contrib.game_systems.achievements import track_achievements class Object(ObjectParent, DefaultObject): @@ -106,31 +149,41 @@ class Object(ObjectParent, DefaultObject): def at_defeated(self, victor): """called when this object is defeated in combat""" - # we'll use the "mob_type" tag category as the tracked information for achievements + # we'll use the "mob_type" tag-category as the tracked info + # this way we can have rats named "black rat" and "brown rat" that are both rats mob_type = self.tags.get(category="mob_type") + # only one mob was defeated, so we include a count of 1 track_achievements(victor, category="defeated", tracking=mob_type, count=1) ``` If a player defeats something tagged `rat` with a tag category of `mob_type`, it'd now count towards the rat-killing achievement. -The `track_achievements` function does also return a value: an iterable of keys for any achievements which were newly completed by that update. You can ignore this value, or you can use it to e.g. send a message to the player with their latest achievements. +You can also have the tracking information hard-coded into your game, for special or unique situations. The achievement described earlier, `FIRST_LOGIN_ACHIEVE`, for example, would be tracked like this: -### `search_achievement` - -A utility function for searching achievements by name or description. It handles partial matching and returns a dictionary of matching achievements. The provided `achievement` command for in-game uses this function to find matching achievements from user inputs. - -#### Example: ```py ->>> from evennia.contrib.game_systems.achievements import search_achievement ->>> search_achievement("fruit") -{'fruit_basket_achievement': {'name': 'A Fan of Fruit', 'category': 'buy', 'tracking': ('apple', 'orange', 'pear'), 'count': 5, 'tracking_type': 'separate'}} ->>> search_achievement("rat") -{'ten_rats': {'key': 'ten_rats', 'name': 'The Usual', 'desc': 'Why do all these inns have rat problems?', 'category': 'defeat', 'tracking': 'rat', 'count': 10}, {'achieve_dire_rats': {'name': 'Once More, But Bigger', 'desc': 'Somehow, normal rats just aren't enough any more.', 'category': 'defeat', 'tracking': 'dire rat', 'count': 10, 'prereqs': "ACHIEVE_TEN_RATS"}} +# in typeclasses/accounts.py +from contrib.game_systems.achievements import track_achievements + +class Account(DefaultAccount): + # ... + + def at_first_login(self, **kwargs): + # this function is only called on the first time the account logs in + # so we already know and can just tell the tracker that this is the first + track_achievements(self, category="login", tracking="first") ``` -### `get_achievement` +The `track_achievements` function does also return a value: an iterable of keys for any achievements which were newly completed by that update. You can ignore this value, or you can use it to e.g. send a message to the player with their latest achievements. -A utility function for retrieving a specific achievement's data from the achievement's unique key. It cannot be used for searching, but if you already have an achievement's key - for example, from the results of `track_achievements` - you can retrieve the rest of its data this way. +### Getting achievements + +The main method for getting a specific achievement's information is `get_achievement`, which takes an already-known achievement key and returns the data for that one achievement. + +For handling more variable and player-friendly input, however, there is also `search_achievement`, which does partial matching on not just the keys, but also the display names and descriptions for the achievements. + +#### `get_achievement` + +A utility function for retrieving a specific achievement's data from the achievement's unique key. It cannot be used for searching, but if you already have an achievement's key - for example, from the results of `track_achievements` - you can retrieve its data this way. #### Example: @@ -139,17 +192,35 @@ from evennia.contrib.game_systems.achievements import get_achievement def toast(achiever, completed_list): if completed_list: - # we've completed some achievements! + # `completed_data` will be a list of dictionaries - unrecognized keys return empty dictionaries completed_data = [get_achievement(key) for key in args] names = [data.get('name') for data in completed] achiever.msg(f"|wAchievement Get!|n {iter_to_str(name for name in names if name)}")) ``` +#### `search_achievement` + +A utility function for searching achievements by name or description. It handles partial matching and returns a dictionary of matching achievements. The provided `achievement` command for in-game uses this function to find matching achievements from user inputs. + +#### Example: + +The first example does a search for "fruit", which returns the fruit medley achievement as it contains "fruit" in the key and name. + +The second example searches for "usual", which returns the ten rats achievement due to its display name. + +```py +>>> from evennia.contrib.game_systems.achievements import search_achievement +>>> search_achievement("fruit") +{'fruit_basket_achievement': {'name': 'Fruit Basket', 'desc': "One kind of fruit just isn't enough.", 'category': 'buy', 'tracking': ('apple', 'orange', 'pear'), 'count': 5, 'tracking_type': 'separate'}} +>>> search_achievement("usual") +{'ten_rats': {'key': 'ten_rats', 'name': 'The Usual', 'desc': 'Why do all these inns have rat problems?', 'category': 'defeat', 'tracking': 'rat', 'count': 10}} +``` + ### The `achievements` command The contrib's provided command, `CmdAchieve`, aims to be usable as-is, with multiple switches to filter achievements by various progress statuses and the ability to search by achievement names. -To make it easier to integrate into your own particular game (e.g. accommodating some of that extra achievement data you might have added), the code for formatting a particular achievement's data for display is in `CmdAchieve.format_achievement`, making it easy to overload for your custom display styling without reimplementing the whole command. +To make it easier to customize for your own game (e.g. displaying some of that extra achievement data you might have added), the format and style code is split out from the command logic into the `format_achievement` method and the `template` attribute, both on `CmdAchieve` #### Example output From 7a83a2951a40a9491e9445ad81fefec0be600c80 Mon Sep 17 00:00:00 2001 From: Cal Date: Sat, 27 Apr 2024 14:05:09 -0600 Subject: [PATCH 042/112] fix stray tabs --- evennia/contrib/game_systems/achievements/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/evennia/contrib/game_systems/achievements/README.md b/evennia/contrib/game_systems/achievements/README.md index 55bae77aef..217b9e54e0 100644 --- a/evennia/contrib/game_systems/achievements/README.md +++ b/evennia/contrib/game_systems/achievements/README.md @@ -24,9 +24,9 @@ To allow players to check their achievements, you'll also want to add the `achie from evennia.contrib.game_systems.achievements.achievements import CmdAchieve class CharacterCmdSet(default_cmds.CharacterCmdSet): - key = "DefaultCharacter" + key = "DefaultCharacter" - def at_cmdset_creation(self): + def at_cmdset_creation(self): # ... self.add(CmdAchieve) ``` @@ -191,11 +191,11 @@ A utility function for retrieving a specific achievement's data from the achieve from evennia.contrib.game_systems.achievements import get_achievement def toast(achiever, completed_list): - if completed_list: + if completed_list: # `completed_data` will be a list of dictionaries - unrecognized keys return empty dictionaries - completed_data = [get_achievement(key) for key in args] - names = [data.get('name') for data in completed] - achiever.msg(f"|wAchievement Get!|n {iter_to_str(name for name in names if name)}")) + completed_data = [get_achievement(key) for key in args] + names = [data.get('name') for data in completed] + achiever.msg(f"|wAchievement Get!|n {iter_to_str(name for name in names if name)}")) ``` #### `search_achievement` From fd39935d19c662e7e6bf761c0741dc7faa7c95e9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 27 Apr 2024 22:26:18 +0200 Subject: [PATCH 043/112] Ran black on sources --- evennia/commands/default/account.py | 4 ++-- evennia/commands/default/building.py | 24 ++++++++++++++------- evennia/commands/default/comms.py | 1 + evennia/commands/default/help.py | 2 +- evennia/server/portal/telnet.py | 11 ++++++---- evennia/server/portal/telnet_oob.py | 4 +++- evennia/utils/funcparser.py | 22 +++++++++++-------- evennia/utils/tests/test_funcparser.py | 13 +++++++++-- evennia/utils/verb_conjugation/conjugate.py | 4 ++-- 9 files changed, 56 insertions(+), 29 deletions(-) diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index 1185648d01..4fdb88f4ff 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -638,8 +638,8 @@ class CmdOption(COMMAND_DEFAULT_CLASS): flags[new_name] = new_val # If we're manually assign a display size, turn off auto-resizing - if new_name in ['SCREENWIDTH', 'SCREENHEIGHT']: - flags['AUTORESIZE'] = False + if new_name in ["SCREENWIDTH", "SCREENHEIGHT"]: + flags["AUTORESIZE"] = False self.msg( f"Option |w{new_name}|n was changed from '|w{old_val}|n' to" diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 7e72ad23e9..53b3c6ba94 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -5,13 +5,13 @@ Building and world design commands import re import typing -import evennia from django.conf import settings from django.core.paginator import Paginator from django.db.models import Max, Min, Q + +import evennia from evennia import InterruptCommand -from evennia.commands.cmdhandler import (generate_cmdset_providers, - get_and_merge_cmdsets) +from evennia.commands.cmdhandler import generate_cmdset_providers, get_and_merge_cmdsets from evennia.locks.lockhandler import LockException from evennia.objects.models import ObjectDB from evennia.prototypes import menus as olc_menus @@ -24,10 +24,18 @@ from evennia.utils.dbserialize import deserialize from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.utils.evtable import EvTable -from evennia.utils.utils import (class_from_module, crop, dbref, display_len, - format_grid, get_all_typeclasses, - inherits_from, interactive, list_to_string, - variable_from_module) +from evennia.utils.utils import ( + class_from_module, + crop, + dbref, + display_len, + format_grid, + get_all_typeclasses, + inherits_from, + interactive, + list_to_string, + variable_from_module, +) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -170,7 +178,7 @@ class ObjManipCommand(COMMAND_DEFAULT_CLASS): def get_object_typeclass( self, obj_type: str = "object", typeclass: str = None, method: str = "cmd_create", **kwargs - ) -> tuple[typing.Optional['Typeclass'], list[str]]: + ) -> tuple[typing.Optional["Typeclass"], list[str]]: """ This hook is called by build commands to determine which typeclass to use for a specific purpose. diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index 521eef7291..cac9905a5a 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -9,6 +9,7 @@ Communication commands: from django.conf import settings from django.db.models import Q + from evennia.accounts import bots from evennia.accounts.models import AccountDB from evennia.comms.comms import DefaultChannel diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index 4a7a1f9adf..2a8ef389cc 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -1039,7 +1039,7 @@ class CmdSetHelp(CmdHelp): old_entry.locks.add(lockstring) except LockException as e: old_entry.locks.add(existing_locks) - self.msg(str(e) + " Locks not changed.") + self.msg(str(e) + " Locks not changed.") else: self.msg(f"Locks for entry '{topicstr}'{aliastxt} changed to: {lockstring}") return diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index b545e67f7e..e961152b16 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -94,10 +94,13 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS): """ try: # Do we have a NAWS update? - if (NAWS in data and - len([data[i:i+1] for i in range(0, len(data))]) == 9 and - # Is auto resizing on? - self.protocol_flags.get('AUTORESIZE')): + if ( + NAWS in data + and len([data[i : i + 1] for i in range(0, len(data))]) == 9 + and + # Is auto resizing on? + self.protocol_flags.get("AUTORESIZE") + ): self.sessionhandler.sync(self.sessionhandler.get(self.sessid)) super().dataReceived(data) diff --git a/evennia/server/portal/telnet_oob.py b/evennia/server/portal/telnet_oob.py index 37c1f8067c..ffc2283067 100644 --- a/evennia/server/portal/telnet_oob.py +++ b/evennia/server/portal/telnet_oob.py @@ -258,7 +258,9 @@ class TelnetOOB: gmcp_cmdname = EVENNIA_TO_GMCP[cmdname] elif "_" in cmdname: # enforce initial capitalization of each command part, leaving fully-capitalized sections intact - gmcp_cmdname = ".".join(word.capitalize() if not word.isupper() else word for word in cmdname.split("_")) + gmcp_cmdname = ".".join( + word.capitalize() if not word.isupper() else word for word in cmdname.split("_") + ) else: gmcp_cmdname = "Core.%s" % (cmdname if cmdname.istitle() else cmdname.capitalize()) diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index bca394530f..c0de83838a 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -1330,7 +1330,7 @@ def funcparser_callable_conjugate(*args, caller=None, receiver=None, mapping=Non caller (Object): The object who represents 'you' in the string. receiver (Object): The recipient of the string. mapping (dict, optional): This is a mapping `{key:Object, ...}` and is - used to find which object the optional `key` argument refers to. If not given, + used to find which object the optional `key` argument refers to. If not given, the `caller` kwarg is used. Returns: @@ -1341,7 +1341,7 @@ def funcparser_callable_conjugate(*args, caller=None, receiver=None, mapping=Non Notes: Note that the verb will not be capitalized. - + Examples: This is often used in combination with the $you/You callables. @@ -1366,7 +1366,9 @@ def funcparser_callable_conjugate(*args, caller=None, receiver=None, mapping=Non return second_person_str if obj == receiver else third_person_str -def funcparser_callable_conjugate_for_pronouns(*args, caller=None, receiver=None, mapping=None, **kwargs): +def funcparser_callable_conjugate_for_pronouns( + *args, caller=None, receiver=None, mapping=None, **kwargs +): """ Usage: $pconj(word, [key]) @@ -1377,7 +1379,7 @@ def funcparser_callable_conjugate_for_pronouns(*args, caller=None, receiver=None caller (Object): The object who represents 'you' in the string. receiver (Object): The recipient of the string. mapping (dict, optional): This is a mapping `{key:Object, ...}` and is - used to find which object the optional `key` argument refers to. If not given, + used to find which object the optional `key` argument refers to. If not given, the `caller` kwarg is used. Returns: @@ -1388,7 +1390,7 @@ def funcparser_callable_conjugate_for_pronouns(*args, caller=None, receiver=None Notes: Note that the verb will not be capitalized. - + Examples: This is often used in combination with the $pron/Pron callables. @@ -1414,15 +1416,17 @@ def funcparser_callable_conjugate_for_pronouns(*args, caller=None, receiver=None plural = False if hasattr(obj, "gender"): if callable(obj.gender): - plural = (obj.gender() == "plural") + plural = obj.gender() == "plural" else: - plural = (obj.gender == "plural") + plural = obj.gender == "plural" second_person_str, third_person_str = verb_actor_stance_components(verb, plural=plural) return second_person_str if obj == receiver else third_person_str -def funcparser_callable_pronoun(*args, caller=None, receiver=None, mapping=None, capitalize=False, **kwargs): +def funcparser_callable_pronoun( + *args, caller=None, receiver=None, mapping=None, capitalize=False, **kwargs +): """ Usage: $pron(word, [options], [key]) @@ -1501,7 +1505,7 @@ def funcparser_callable_pronoun(*args, caller=None, receiver=None, mapping=None, `caller` or not helps determine 2nd vs 3rd-person forms. This is provided automatically by the funcparser. mapping (dict, optional): This is a mapping `{key:Object, ...}` and is - used to find which object the optional `key` argument refers to. If not given, + used to find which object the optional `key` argument refers to. If not given, the `caller` kwarg is used. capitalize (bool): The input retains its capitalization. If this is set the output is always capitalized. diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index 1b2fd41da1..5ca8824186 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -435,7 +435,11 @@ class TestDefaultCallables(TestCase): ("$You() $conj(smile) at him.", "You smile at him.", "Char1 smiles at him."), ("$You() $conj(smile) at $You(char1).", "You smile at You.", "Char1 smiles at Char1."), ("$You() $conj(smile) at $You(char2).", "You smile at Char2.", "Char1 smiles at You."), - ("$You() $conj(smile) while $You(char2) $conj(waves, char2).", "You smile while Char2 waves.", "Char1 smiles while You wave."), + ( + "$You() $conj(smile) while $You(char2) $conj(waves, char2).", + "You smile while Char2 waves.", + "Char1 smiles while You wave.", + ), ( "$You(char2) $conj(smile) at $you(char1).", "Char2 smile at you.", @@ -519,7 +523,12 @@ class TestDefaultCallables(TestCase): string = "Char1 raises $pron(your, char1) fist as Char2 raises $pron(yours, char2)" expected = "Char1 raises her fist as Char2 raises his" - ret = self.parser.parse(string, caller=self.obj1, mapping={'char1': self.obj1, 'char2': self.obj2}, raise_errors=True) + ret = self.parser.parse( + string, + caller=self.obj1, + mapping={"char1": self.obj1, "char2": self.obj2}, + raise_errors=True, + ) self.assertEqual(expected, ret) def test_pronoun_viewpoint(self): diff --git a/evennia/utils/verb_conjugation/conjugate.py b/evennia/utils/verb_conjugation/conjugate.py index 21ec134ced..e29530b9b9 100644 --- a/evennia/utils/verb_conjugation/conjugate.py +++ b/evennia/utils/verb_conjugation/conjugate.py @@ -371,7 +371,7 @@ def verb_actor_stance_components(verb, plural=False): Args: verb (str): The verb to analyze - plural (bool): Whether to force 3rd person to plural form + plural (bool): Whether to force 3rd person to plural form Returns: tuple: The 2nd person (you) and 3rd person forms of the verb, @@ -380,7 +380,7 @@ def verb_actor_stance_components(verb, plural=False): tense = verb_tense(verb) them = "*" if plural else "3" them_suff = "" if plural else "s" - + if "participle" in tense or "plural" in tense: return (verb, verb) if tense == "infinitive" or "present" in tense: From 5bec1a29d6e51b83488221b82ebd219d85c8fb47 Mon Sep 17 00:00:00 2001 From: mike Date: Sat, 27 Apr 2024 14:01:44 -0700 Subject: [PATCH 044/112] =?UTF-8?q?Changes=20per=20Griatch:=20-Reordered?= =?UTF-8?q?=20methods=20in=20HexColors=20-Separated=20truecolor=20tests=20?= =?UTF-8?q?-Clarified=20comment=20re:=20classes=20and=20styles=20in=20text?= =?UTF-8?q?2html.py=20-Changed=20ansi.py=20to=20only=20instatiate=20HexCol?= =?UTF-8?q?ors=20once=20=F0=9F=A4=A6=E2=80=8D=E2=99=82=EF=B8=8F=20-Fixed?= =?UTF-8?q?=20missing=20docsctring=20in=20parse=5Fansi=20re:=20truecolor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- evennia/utils/ansi.py | 12 +- evennia/utils/hex_colors.py | 174 +++++++++++++------------- evennia/utils/tests/test_ansi.py | 125 +----------------- evennia/utils/tests/test_truecolor.py | 127 +++++++++++++++++++ evennia/utils/text2html.py | 36 +++--- 5 files changed, 243 insertions(+), 231 deletions(-) create mode 100644 evennia/utils/tests/test_truecolor.py diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index 7bb6643e5f..68dbc0c103 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -72,6 +72,7 @@ from evennia.utils import logger, utils from evennia.utils.utils import to_str from evennia.utils.hex_colors import HexColors +hex2truecolor = HexColors() hex_sub = HexColors.hex_sub MXP_ENABLED = settings.MXP_ENABLED @@ -471,7 +472,6 @@ class ANSIParser(object): string = self.brightbg_sub.sub(self.sub_brightbg, string) def do_truecolor(part: re.Match, truecolor=truecolor): - hex2truecolor = HexColors() return hex2truecolor.sub_truecolor(part, truecolor) def do_xterm256_fg(part): @@ -525,24 +525,28 @@ ANSI_PARSER = ANSIParser() # -def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False, mxp=False, truecolor=False): +def parse_ansi( + string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False, mxp=False, truecolor=False +): """ Parses a string, subbing color codes as needed. Args: - truecolor: string (str): The string to parse. strip_ansi (bool, optional): Strip all ANSI sequences. parser (ansi.AnsiParser, optional): A parser instance to use. xterm256 (bool, optional): Support xterm256 or not. mxp (bool, optional): Support MXP markup or not. + truecolor (bool, optional): Support for truecolor or not. Returns: string (str): The parsed string. """ string = string or "" - return parser.parse_ansi(string, strip_ansi=strip_ansi, xterm256=xterm256, mxp=mxp, truecolor=truecolor) + return parser.parse_ansi( + string, strip_ansi=strip_ansi, xterm256=xterm256, mxp=mxp, truecolor=truecolor + ) def strip_ansi(string, parser=ANSI_PARSER): diff --git a/evennia/utils/hex_colors.py b/evennia/utils/hex_colors.py index dcea3531a7..781496ac71 100644 --- a/evennia/utils/hex_colors.py +++ b/evennia/utils/hex_colors.py @@ -9,97 +9,25 @@ class HexColors: Based on code from @InspectorCaracal """ - _RE_FG = '\|#' - _RE_BG = '\|\[#' - _RE_FG_OR_BG = '\|\[?#' - _RE_HEX_LONG = '[0-9a-fA-F]{6}' - _RE_HEX_SHORT = '[0-9a-fA-F]{3}' - _RE_BYTE = '[0-2]?[0-9]?[0-9]' - _RE_XTERM_TRUECOLOR = rf'\[([34])8;2;({_RE_BYTE});({_RE_BYTE});({_RE_BYTE})m' + _RE_FG = "\|#" + _RE_BG = "\|\[#" + _RE_FG_OR_BG = "\|\[?#" + _RE_HEX_LONG = "[0-9a-fA-F]{6}" + _RE_HEX_SHORT = "[0-9a-fA-F]{3}" + _RE_BYTE = "[0-2]?[0-9]?[0-9]" + _RE_XTERM_TRUECOLOR = rf"\[([34])8;2;({_RE_BYTE});({_RE_BYTE});({_RE_BYTE})m" # Used in hex_sub - _RE_HEX_PATTERN = f'({_RE_FG_OR_BG})({_RE_HEX_LONG}|{_RE_HEX_SHORT})' + _RE_HEX_PATTERN = f"({_RE_FG_OR_BG})({_RE_HEX_LONG}|{_RE_HEX_SHORT})" # Used for greyscale _GREYS = "abcdefghijklmnopqrstuvwxyz" - TRUECOLOR_FG = f'\x1b\[38;2;{_RE_BYTE};{_RE_BYTE};{_RE_BYTE}m' - TRUECOLOR_BG = f'\x1b\[48;2;{_RE_BYTE};{_RE_BYTE};{_RE_BYTE}m' + TRUECOLOR_FG = f"\x1b\[38;2;{_RE_BYTE};{_RE_BYTE};{_RE_BYTE}m" + TRUECOLOR_BG = f"\x1b\[48;2;{_RE_BYTE};{_RE_BYTE};{_RE_BYTE}m" # Our matchers for use with ANSIParser and ANSIString - hex_sub = re.compile(rf'{_RE_HEX_PATTERN}', re.DOTALL) - - def sub_truecolor(self, match: re.Match, truecolor=False) -> str: - """ - Converts a hex string to xterm truecolor code, greyscale, or - falls back to evennia xterm256 to be handled by sub_xterm256 - - Args: - match (re.match): first group is the leading indicator, - second is the tag - truecolor (bool): return xterm truecolor or fallback - - Returns: - Newly formatted indicator and tag (str) - - """ - indicator, tag = match.groups() - - # Remove the # sign - indicator = indicator.replace('#', '') - - r, g, b = self._hex_to_rgb_24_bit(tag) - - # Is it greyscale? - if r == g and g == b: - return f"{indicator}=" + self._GREYS[self._grey_int(r)] - - else: - if not truecolor: - # Fallback to xterm256 syntax - r, g, b = self._rgb_24_bit_to_256(r, g, b) - return f"{indicator}{r}{g}{b}" - - else: - xtag = f"\033[" - if '[' in indicator: - # Background Color - xtag += '4' - - else: - xtag += '3' - - xtag += f"8;2;{r};{g};{b}m" - return xtag - - def xterm_truecolor_to_html_style(self, fg="", bg="") -> str: - """ - Converts xterm truecolor to an html style property - - Args: - fg: xterm truecolor - bg: xterm truecolor - - Returns: style='color and or background-color' - - """ - prop = 'style="' - if fg != '': - res = re.search(self._RE_XTERM_TRUECOLOR, fg, re.DOTALL) - fg_bg, r, g, b = res.groups() - r = hex(int(r))[2:].zfill(2) - g = hex(int(g))[2:].zfill(2) - b = hex(int(b))[2:].zfill(2) - prop += f"color: #{r}{g}{b};" - if bg != '': - res = re.search(self._RE_XTERM_TRUECOLOR, bg, re.DOTALL) - fg_bg, r, g, b = res.groups() - r = hex(int(r))[2:].zfill(2) - g = hex(int(g))[2:].zfill(2) - b = hex(int(b))[2:].zfill(2) - prop += f"background-color: #{r}{g}{b};" - prop += f'"' - return prop + hex_sub = re.compile(rf"{_RE_HEX_PATTERN}", re.DOTALL) def _split_hex_to_bytes(self, tag: str) -> tuple[str, str, str]: """ @@ -113,16 +41,16 @@ class HexColors: Returns: str: the text with converted tags """ - strip_leading = re.compile(rf'{self._RE_FG_OR_BG}') + strip_leading = re.compile(rf"{self._RE_FG_OR_BG}") tag = strip_leading.sub("", tag) if len(tag) == 6: # 6 digits - r, g, b = (tag[i:i + 2] for i in range(0, 6, 2)) + r, g, b = (tag[i : i + 2] for i in range(0, 6, 2)) else: # 3 digits - r, g, b = (tag[i:i + 1] * 2 for i in range(0, 3, 1)) + r, g, b = (tag[i : i + 1] * 2 for i in range(0, 3, 1)) return r, g, b @@ -150,7 +78,7 @@ class HexColors: 24-bit rgb tuple: (int, int, int) """ # Strip the leading indicator if present - hex_code = re.sub(rf'{self._RE_FG_OR_BG}', '', hex_code) + hex_code = re.sub(rf"{self._RE_FG_OR_BG}", "", hex_code) r, g, b = self._split_hex_to_bytes(hex_code) @@ -171,3 +99,75 @@ class HexColors: """ return self._hue_int(r), self._hue_int(g), self._hue_int(b) + + def sub_truecolor(self, match: re.Match, truecolor=False) -> str: + """ + Converts a hex string to xterm truecolor code, greyscale, or + falls back to evennia xterm256 to be handled by sub_xterm256 + + Args: + match (re.match): first group is the leading indicator, + second is the tag + truecolor (bool): return xterm truecolor or fallback + + Returns: + Newly formatted indicator and tag (str) + + """ + indicator, tag = match.groups() + + # Remove the # sign + indicator = indicator.replace("#", "") + + r, g, b = self._hex_to_rgb_24_bit(tag) + + # Is it greyscale? + if r == g and g == b: + return f"{indicator}=" + self._GREYS[self._grey_int(r)] + + else: + if not truecolor: + # Fallback to xterm256 syntax + r, g, b = self._rgb_24_bit_to_256(r, g, b) + return f"{indicator}{r}{g}{b}" + + else: + xtag = f"\033[" + if "[" in indicator: + # Background Color + xtag += "4" + + else: + xtag += "3" + + xtag += f"8;2;{r};{g};{b}m" + return xtag + + def xterm_truecolor_to_html_style(self, fg="", bg="") -> str: + """ + Converts xterm truecolor to an html style property + + Args: + fg: xterm truecolor + bg: xterm truecolor + + Returns: style='color and or background-color' + + """ + prop = 'style="' + if fg != "": + res = re.search(self._RE_XTERM_TRUECOLOR, fg, re.DOTALL) + fg_bg, r, g, b = res.groups() + r = hex(int(r))[2:].zfill(2) + g = hex(int(g))[2:].zfill(2) + b = hex(int(b))[2:].zfill(2) + prop += f"color: #{r}{g}{b};" + if bg != "": + res = re.search(self._RE_XTERM_TRUECOLOR, bg, re.DOTALL) + fg_bg, r, g, b = res.groups() + r = hex(int(r))[2:].zfill(2) + g = hex(int(g))[2:].zfill(2) + b = hex(int(b))[2:].zfill(2) + prop += f"background-color: #{r}{g}{b};" + prop += f'"' + return prop diff --git a/evennia/utils/tests/test_ansi.py b/evennia/utils/tests/test_ansi.py index 0ecc8d1b04..4ff9d468c6 100644 --- a/evennia/utils/tests/test_ansi.py +++ b/evennia/utils/tests/test_ansi.py @@ -8,9 +8,7 @@ Test of the ANSI parsing and ANSIStrings. from django.test import TestCase -from evennia.utils.ansi import ANSIString as AN, ANSIParser - -parser = ANSIParser().parse_ansi +from evennia.utils.ansi import ANSIString as AN class TestANSIString(TestCase): @@ -54,124 +52,3 @@ class TestANSIString(TestCase): self.assertEqual(split2, split3, "Split 2 and 3 differ") self.assertEqual(split1, split2, "Split 1 and 2 differ") self.assertEqual(split1, split3, "Split 1 and 3 differ") - -# TODO: Better greyscale testing - -class TestANSIStringHex(TestCase): - """ - Tests the conversion of html hex colors - to xterm-style colors - """ - def setUp(self): - self.str = 'test ' - self.output1 = '\x1b[38;5;16mtest \x1b[0m' - self.output2 = '\x1b[48;5;16mtest \x1b[0m' - self.output3 = '\x1b[38;5;46mtest \x1b[0m' - self.output4 = '\x1b[48;5;46mtest \x1b[0m' - - def test_long_grayscale_fg(self): - raw = f'|#000000{self.str}|n' - ansi = AN(raw) - self.assertEqual(ansi.clean(), self.str, "Cleaned") - self.assertEqual(ansi.raw(), self.output1, "Output") - - def test_long_grayscale_bg(self): - raw = f'|[#000000{self.str}|n' - ansi = AN(raw) - self.assertEqual(ansi.clean(), self.str, "Cleaned") - self.assertEqual(ansi.raw(), self.output2, "Output") - - def test_short_grayscale_fg(self): - raw = f'|#000{self.str}|n' - ansi = AN(raw) - self.assertEqual(ansi.clean(), self.str, "Cleaned") - self.assertEqual(ansi.raw(), self.output1, "Output") - - def test_short_grayscale_bg(self): - raw = f'|[#000{self.str}|n' - ansi = AN(raw) - self.assertEqual(ansi.clean(), self.str, "Cleaned") - self.assertEqual(ansi.raw(), self.output2, "Output") - - def test_short_color_fg(self): - raw = f'|#0F0{self.str}|n' - ansi = AN(raw) - self.assertEqual(ansi.clean(), self.str, "Cleaned") - self.assertEqual(ansi.raw(), self.output3, "Output") - - def test_short_color_bg(self): - raw = f'|[#0f0{self.str}|n' - ansi = AN(raw) - self.assertEqual(ansi.clean(), self.str, "Cleaned") - self.assertEqual(ansi.raw(), self.output4, "Output") - - def test_long_color_fg(self): - raw = f'|#00ff00{self.str}|n' - ansi = AN(raw) - self.assertEqual(ansi.clean(), self.str, "Cleaned") - self.assertEqual(ansi.raw(), self.output3, "Output") - - def test_long_color_bg(self): - raw = f'|[#00FF00{self.str}|n' - ansi = AN(raw) - self.assertEqual(ansi.clean(), self.str, "Cleaned") - self.assertEqual(ansi.raw(), self.output4, "Output") - - -class TestANSIParser(TestCase): - """ - Tests the ansi fallback of the hex color conversion and - truecolor conversion - """ - def setUp(self): - self.parser = ANSIParser().parse_ansi - self.str = 'test ' - - # ANSI FALLBACK - # Red - self.output1 = '\x1b[1m\x1b[31mtest \x1b[0m' - # White - self.output2 = '\x1b[1m\x1b[37mtest \x1b[0m' - # Red BG - self.output3 = '\x1b[41mtest \x1b[0m' - # Blue FG, Red BG - self.output4 = '\x1b[41m\x1b[1m\x1b[34mtest \x1b[0m' - - def test_hex_color(self): - raw = f'|#F00{self.str}|n' - ansi = parser(raw) - # self.assertEqual(ansi, self.str, "Cleaned") - self.assertEqual(ansi, self.output1, "Output") - - def test_hex_greyscale(self): - raw = f'|#FFF{self.str}|n' - ansi = parser(raw) - self.assertEqual(ansi, self.output2, "Output") - - def test_hex_color_bg(self): - raw = f'|[#Ff0000{self.str}|n' - ansi = parser(raw) - self.assertEqual(ansi, self.output3, "Output") - - def test_hex_color_fg_bg(self): - raw = f'|[#Ff0000|#00f{self.str}|n' - ansi = parser(raw) - self.assertEqual(ansi, self.output4, "Output") - - def test_truecolor_fg(self): - raw = f'|#00c700{self.str}|n' - ansi = parser(raw, truecolor=True) - output = f'\x1b[38;2;0;199;0m{self.str}\x1b[0m' - self.assertEqual(ansi, output, "Output") - - def test_truecolor_bg(self): - raw = f'|[#00c700{self.str}|n' - ansi = parser(raw, truecolor=True) - output = f'\x1b[48;2;0;199;0m{self.str}\x1b[0m' - self.assertEqual(ansi, output, "Output") - - def test_truecolor_fg_bg(self): - raw = f'|[#00c700|#880000{self.str}|n' - ansi = parser(raw, truecolor=True) - output = f'\x1b[48;2;0;199;0m\x1b[38;2;136;0;0m{self.str}\x1b[0m' - self.assertEqual(ansi, output, "Output") diff --git a/evennia/utils/tests/test_truecolor.py b/evennia/utils/tests/test_truecolor.py new file mode 100644 index 0000000000..33fddeca16 --- /dev/null +++ b/evennia/utils/tests/test_truecolor.py @@ -0,0 +1,127 @@ +from django.test import TestCase + +from evennia.utils.ansi import ANSIString as AN, ANSIParser + +parser = ANSIParser().parse_ansi + + +class TestANSIStringHex(TestCase): + """ + Tests the conversion of html hex colors + to xterm-style colors + """ + + def setUp(self): + self.str = "test " + self.output1 = "\x1b[38;5;16mtest \x1b[0m" + self.output2 = "\x1b[48;5;16mtest \x1b[0m" + self.output3 = "\x1b[38;5;46mtest \x1b[0m" + self.output4 = "\x1b[48;5;46mtest \x1b[0m" + + def test_long_grayscale_fg(self): + raw = f"|#000000{self.str}|n" + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output1, "Output") + + def test_long_grayscale_bg(self): + raw = f"|[#000000{self.str}|n" + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output2, "Output") + + def test_short_grayscale_fg(self): + raw = f"|#000{self.str}|n" + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output1, "Output") + + def test_short_grayscale_bg(self): + raw = f"|[#000{self.str}|n" + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output2, "Output") + + def test_short_color_fg(self): + raw = f"|#0F0{self.str}|n" + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output3, "Output") + + def test_short_color_bg(self): + raw = f"|[#0f0{self.str}|n" + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output4, "Output") + + def test_long_color_fg(self): + raw = f"|#00ff00{self.str}|n" + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output3, "Output") + + def test_long_color_bg(self): + raw = f"|[#00FF00{self.str}|n" + ansi = AN(raw) + self.assertEqual(ansi.clean(), self.str, "Cleaned") + self.assertEqual(ansi.raw(), self.output4, "Output") + + +class TestANSIParser(TestCase): + """ + Tests the ansi fallback of the hex color conversion and + truecolor conversion + """ + + def setUp(self): + self.parser = ANSIParser().parse_ansi + self.str = "test " + + # ANSI FALLBACK + # Red + self.output1 = "\x1b[1m\x1b[31mtest \x1b[0m" + # White + self.output2 = "\x1b[1m\x1b[37mtest \x1b[0m" + # Red BG + self.output3 = "\x1b[41mtest \x1b[0m" + # Blue FG, Red BG + self.output4 = "\x1b[41m\x1b[1m\x1b[34mtest \x1b[0m" + + def test_hex_color(self): + raw = f"|#F00{self.str}|n" + ansi = parser(raw) + # self.assertEqual(ansi, self.str, "Cleaned") + self.assertEqual(ansi, self.output1, "Output") + + def test_hex_greyscale(self): + raw = f"|#FFF{self.str}|n" + ansi = parser(raw) + self.assertEqual(ansi, self.output2, "Output") + + def test_hex_color_bg(self): + raw = f"|[#Ff0000{self.str}|n" + ansi = parser(raw) + self.assertEqual(ansi, self.output3, "Output") + + def test_hex_color_fg_bg(self): + raw = f"|[#Ff0000|#00f{self.str}|n" + ansi = parser(raw) + self.assertEqual(ansi, self.output4, "Output") + + def test_truecolor_fg(self): + raw = f"|#00c700{self.str}|n" + ansi = parser(raw, truecolor=True) + output = f"\x1b[38;2;0;199;0m{self.str}\x1b[0m" + self.assertEqual(ansi, output, "Output") + + def test_truecolor_bg(self): + raw = f"|[#00c700{self.str}|n" + ansi = parser(raw, truecolor=True) + output = f"\x1b[48;2;0;199;0m{self.str}\x1b[0m" + self.assertEqual(ansi, output, "Output") + + def test_truecolor_fg_bg(self): + raw = f"|[#00c700|#880000{self.str}|n" + ansi = parser(raw, truecolor=True) + output = f"\x1b[48;2;0;199;0m\x1b[38;2;136;0;0m{self.str}\x1b[0m" + self.assertEqual(ansi, output, "Output") diff --git a/evennia/utils/text2html.py b/evennia/utils/text2html.py index e8000a7f81..bb9d642915 100644 --- a/evennia/utils/text2html.py +++ b/evennia/utils/text2html.py @@ -75,7 +75,9 @@ class TextToHTMLparser(object): r"({}|{})".format( "|".join( style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes - ).replace("[", r"\["), "|".join([HexColors.TRUECOLOR_FG, HexColors.TRUECOLOR_BG])) + ).replace("[", r"\["), + "|".join([HexColors.TRUECOLOR_FG, HexColors.TRUECOLOR_BG]), + ) ) colorlist = ( @@ -256,8 +258,8 @@ class TextToHTMLparser(object): fg = ANSI_WHITE # default bg is black bg = ANSI_BACK_BLACK - truecolor_fg = '' - truecolor_bg = '' + truecolor_fg = "" + truecolor_bg = "" for i, substr in enumerate(str_list): # reset all current styling @@ -271,8 +273,8 @@ class TextToHTMLparser(object): hilight = ANSI_UNHILITE fg = ANSI_WHITE bg = ANSI_BACK_BLACK - truecolor_fg = '' - truecolor_bg = '' + truecolor_fg = "" + truecolor_bg = "" # change color elif substr in self.ansi_color_codes + self.xterm_fg_codes: @@ -289,7 +291,7 @@ class TextToHTMLparser(object): bg = substr elif re.match(hex_colors.TRUECOLOR_FG, substr): - str_list[i] = '' + str_list[i] = "" truecolor_fg = substr elif re.match(hex_colors.TRUECOLOR_BG, substr): @@ -334,18 +336,18 @@ class TextToHTMLparser(object): color_index = self.colorlist.index(fg) if inverse: - if truecolor_fg != '' and truecolor_bg != '': + if truecolor_fg != "" and truecolor_bg != "": # True startcolor only truecolor_fg, truecolor_bg = truecolor_bg, truecolor_fg - elif truecolor_fg != '' and truecolor_bg == '': + elif truecolor_fg != "" and truecolor_bg == "": # Truecolor fg, class based bg truecolor_bg = truecolor_fg - truecolor_fg = '' + truecolor_fg = "" color_class = "color-{}".format(str(bg_index).rjust(3, "0")) - elif truecolor_fg == '' and truecolor_bg != '': + elif truecolor_fg == "" and truecolor_bg != "": # Truecolor bg, class based fg truecolor_fg = truecolor_bg - truecolor_bg = '' + truecolor_bg = "" bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0")) else: # inverse means swap fg and bg indices @@ -364,13 +366,15 @@ class TextToHTMLparser(object): classes.append(color_class) # define the new style span - if truecolor_fg == '' and truecolor_bg == '': + if truecolor_fg == "" and truecolor_bg == "": prefix = f'' else: - # Classes can't be used for true color - prefix = (f'') + # Classes can't be used for truecolor--but they can be extras such as 'blink' + prefix = ( + f"" + ) # close any prior span if not clean: From 09884c44af52e6a8621bb69daf5fb425d04d6e4e Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Sun, 28 Apr 2024 12:14:42 +1000 Subject: [PATCH 045/112] Update editor help to clarify optional nature of '= ' for :j and :f --- evennia/utils/eveditor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 7557c40a0e..ed371bec6f 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -97,10 +97,11 @@ _HELP_TEXT = _( :s - search/replace word or regex in buffer or on line - :j = - justify buffer or line . is f, c, l or r. is - width. Default for is l (left). Default for is {_DEFAULT_WIDTH} - :f = - flood-fill entire buffer or line to width . - Equivalent to :j l + :j = - justify buffer or line . is f, c, l or r. is + width. and are optional and default to l (left) + and {_DEFAULT_WIDTH} respectively + :f = - flood-fill entire buffer or line to width . + Equivalent to :j l. is optional, as for :j :fi - indent entire buffer or line :fd - de-indent entire buffer or line From 737d98c640afb76336faa5a9f255d3e1cab010fe Mon Sep 17 00:00:00 2001 From: Moonchasered Date: Sun, 28 Apr 2024 08:10:55 +0200 Subject: [PATCH 046/112] initial --- evennia/locale/pl/LC_MESSAGES/django.mo | Bin 1631 -> 25353 bytes evennia/locale/pl/LC_MESSAGES/django.po | 475 ++++++++++++++---------- 2 files changed, 289 insertions(+), 186 deletions(-) diff --git a/evennia/locale/pl/LC_MESSAGES/django.mo b/evennia/locale/pl/LC_MESSAGES/django.mo index 685804a56ca037df30e4186eed3d589c5516ba2e..ac86aeba6f01bd6619c3993da4f32886163ac617 100644 GIT binary patch literal 25353 zcmbuHdyr&Tecvw@V7oC7feaWhx+5Ml12er32?QhUihU{zyV|vOSBn;$)t&A;Gk3cC z-d5l4?d_f!8w&{>+gN!Kv*u-t0|bgHTwCD}Pzaz|%2b>5%@ar36L(q=luO|f{-fsOYk=E|A0RY-b&~Dz-jQU;4;Y4gHM1n z;FrNW!9N4{f-eu-~3wdhcma^gIu0{!Oq2eh}3A zZh5Vn_d!tmdIo$W_#sfwH$koA|A89k6}$}A3ig4T_aP8c1P_7YkINuK2Ty{U=QWR; z;7j=ZN1*8R-$3>I0jPejgeh1;aF@pmpyoRS^}bJnd%$ZT{{`RWAHAnC?dDenAzg3+ z)Vv#@_VZJq`1C1I^!jU1{Bk>k9|i9LHQswb_5U;&g5L!9f!_lUg0Gr&`}HmmRtW~+ zJ>ZAHbKoC>;>TH-So?Jx)cS{@==7_g*6~~Z{ue;?{~D1pa$|{~n$H3;6LjBfAj( z7w<;r@%#C_tP4H~{sH*xx1syMKLn+3-gU^y+W>r+-+uvq9z1k72>$J}g5a+}$=6q5 z#zpWyg5t}A$5;#aUht2>KLWM?pNA-F{|!*|`;NzF-|ysh8q|KA1uI|+)H<*D`=0`z z!|$(xC&6!kKM%eH=7EKSN5R*D9|m6wehqvs_~+p3!M_Bj!P`!_-}i#r-x{d-ya#+f z_(@Ro{9RD>D#0&<8t<#%0r0!vVQ}gpr&sEr<~s!M1V0A$!G8yej&Hx<##shm#P5%L z`~vt=et!+T75on%EEaqZ)I9HcCo}=y1;V<)?}BRoH=x#iD?%x{+yg!rd>H&G@DWh! zXo2qnzXJXY_==0pzT5>K=XVB*o?isDp1%OA;2Y?q`wx5k6;SuT49`!c9`4?)rE6CkV{Y=W{oxBk4N(_Zj)exCz1UKiB9e-u=|e+B+J_&>m312_Jz zldH-lr-vQ@d))sF2q}VBKjQeg4{9I&At-+QBB*(O9fb9QzX4wco{Zf3>!A4gbKtLl z-}k@Q7ag6y4yyg1c>F&2EPkuckgNDS!55}zAJO$*uD{3i8M`z3M>Nn!b9t0Yd;Kad z$Y~$(4zDm9sT+s5w2wL0eOz^}k8|nsPq{v3-<&Tj{&*`_=I?1AL{miHbiMSlXrT|{ zZFJIi$&EfQ;QF9_gMQ$XT<_r$U9?B2G~=y1z<nhi8ap`lrgW!V4^WZ+N z+qhP_UdSb0&_}v8)R(c(U3`~J{Sz+n(qmkYaQy<;Pjl&WKNl<-z(1HE? zT>h1ewYiSi9sFHz&3~63{byVs^1maGKM6j}HN_=e^$IT7K6r@h!(95jnQLI*CjXI4 zN=|=~>u0$dT#}bdT%Y24oJ*gSOY->^u3zW6hwHOk`ZT#d$Mq7f3Rm%Y9bbN%>#)DE z0R9867yI9W4{*KF|Nb%XH@Qx7-O6=<>!VyXuIF=I;flHRc|X@DxqgG|ceqY*b-9+g z-o~ZRk8{0<>sR%I&nvm!Yu}vg`>(-A^raH=ITw!f2y{iM4R zKHAH=aUMpUte4iq?n>P9qjaxy59;k1FX@DNx5Hp_VK47R&E_CXvu>Eiah;ofh-!r; zEKF$?^Wm{8ajn;l!&+94!^Pgx5<}>9S+5)B(Petk_FmWaXk6>WQ6B5%VKcrQH^Xcx zjC$Ry6?KzZn56YM?eaBC!zEUsW@&t-YlO)eN6Y1%C(^&=I0xJ~XPIbiWtRzL>7Hz9 zNlU%g2*i7%ROCqKYgwxmLBj-Uw%crD3_UZOEz<$W49uO) z!o_hrT#Rase$=Vwa8{`MAq(whoq^h0hxu@(6PNAbIW^y*_4)H|v7I=NyCHJnXPetQ zwE0Pzbd#vbi=%F=3DxVim#$wuZpH{nGs+z^OGYPmzTUUy_af5sp>Hm6Ht|PSTnY7v zb+E@tr{FF<;;->AEsNPO+K zmyF_8Hy`Hga&2W#G2b28#!KiHeqp%Pa=7_$wTDr8CzDv zqw!LNsR@_DnWbK{DcN1J@f`mxHM6Wfza&C$W7}d$3*j-u{BqQ!g*c80HRC0zkfo#; zY92U~aC9?a zKKU&6;ZRs>)uk3%aR&oBA9p&Ua;?_upj)cp8H5m<2NlqN$kB4hMrCkmJ?=&cWLOrT zRHU&`+L@kqvU;zEioWsS=cKy%FGDEOu--#sXS^jJU`MZ1+*A<43=Z~Y$xYVYWE1vp z@g@x&TyG*?n&xN;CuGswi{uWJuW4pARGw4Za~1rJofm5EhlqOYa*QZMNO`u3Fwa5d zTC-=ag-FoFOvO^#SXwEOT?q1wQ}xtlnqp+M@zRo6xR@U}CCz$8jumQT`+n>;O{0(u z2=l|-*TXG4tZjOR$2Z9UyR7$t3`OYN4R{RsllL|i7W*EBwGM_y+n_D$uAp^lQ5vpb zyep*_Sc`gjoNFC794=3mVHdoziGizOcw(tiPx4xprtBO{i@F*8K!wIR-oYJVo;AJO zz$Py?`DRKy5q>C13gRBxGuz$IN?QC}o0<2ep&9AVw%1V(v|(T-9l z4;SJlxOwdZV+s+sE?CJOWRa{Q-UzH(3 zoDW7Y*R~?@o(ubt(?FVNCt1X+ohw8&$#Yx|DLtvka1~8q%3WL2!}1UCoDSiQz}d-V z1RXyH2U>1?pY4gm_5Nant6A)17niq#w{Qtn-K=dknXn^{y3mwG9CkHcY)tKks|ltl>kxbT7MQ8%itr<3FSoE*V& zfMq$C@Tni@Y$Iv6S-`elXVtFk)rqC5q3x)rpPtl`e9HQ^5zet_q8A7*GFhZjOd5yO z`WOw{aYu|ME7KyDsfI2(U{V$KMQ;C=7I?KQ3E_idRnfM1O$^IIq`vvfd9QOhK^Qe< z*sn`eRj_|2HnL&BGnDlfa8eJ~vbOg3QX?L0RD&5J z1hJPo%?5`SFwWx`AmJ-8Vj`IyQMPI3fxYN0{8lhef z4&f9m%a0Eu*GQqM?5+f-w3_1eXgT6;lfCQE)$`1lb@D1do^N4J#TrhnAmG{*uZBXu zdpSdtF`HaOHrG2@YjR0g_93LxyALiF^-M=N_zVxzYH-9N7$D0q_)Nr6Q))x&m=+zU zX(q26CSFVR!hFNLLX=$xt>oM^+IXEmU2f)RuP(F92-RA)$_X1=aoY3mJ2+=MR*bRp z(>aUG#@c9dTYi>pO8;N{61UpuJDL_^1f!zNyWxndplKBLS28>vm(DOY8fUJl9s1`o zEO7hu8oCWOkD4&34K?0#{LM=^c{C>+6fvW+#u-y9u9a>&A37S0YK0wPeJu)+vkB#md zEaOzt#}ecNun6bORD%5ed2XzH1kxiELpw+L7l$Ai_;kiL~8qJnEFE@iFoHbKk8JCY#eVB=i^Jez{$AA7`s$8i0;D zcmn~8-5gh^jv(SZT%WjWWAF4_4-hgW)botZING9`i#80Vbe`VkI&njJziM#W=H}Jp zVv~f|RB=z74W# zB!QeMOwOoDCnn*6)~3}&qp>Dy6Av~BO`Ml4~_qD_cO0Gl3 zDV#z1m?*fM9{e;?ls9N!EVpR3#)$hijPmBO>@?I^n^AYmpUKgY^4&paJcvhZ`e!_& z3ccmGIZvp}uM#h@D*4WG;;VrL28%J`6PqWa@aMkZtYyOHeI|@IyP3})v~T-&S;I~B zDpqxt7oq1Z$zJ!F|H5lKi=*KfuMg>o{V*+4p`t9gw)I|1*#n###S>o7ZFd{aW}S{1 zWt6L8NU1tG2~iW#trNwe#c(!vYo-)6!8IaxPRW2oVL6<(-M^o54V<2;VzhDW5sn9|ljojlKK32&E{2C-arvb zv1sn-T*WC>vK;)TC-SG{MHw}a_1xy)F<%X;f67dYr=i}+C`G)_S38M`3fEmUvlBBP zGRiOWeQ0~09SmQyZ*&9Ssi4~U%}$oJ#(uLhyd`cF-mLQ>v6#@Id#O9z9`BWoZ{ZP~ zD@eX3|*2JMIa+aUBJJJe?S?8k|ojRiJ2Ql0xcm4@RBygxnJ0(3319GDUPz z4IZ*E*X6nW+Te*!96ZF?TyP=kC{rI45EsZ`!*M-!(HIfqQ1T*}uYZw6~c0(=7dwi5OR8-`dGfKwcfH zUq{nBwKM;f!Q3IrGqrH9^DXe)jBYOVC7~pX0`Fuw=~9u=+TlqS(ig5NGeReEv=Z*9 zt4=nMwq^1(v||B08fFVal#CaiKX-ioE!-zmRVFh?c8V1OqBgufGO!!-o<(KUl_VH9 zx!ao*wrNe#S=`w$DiWz*X*ZJ^TQ@Bhm>y#fOaJ>*#?sXYw|E`Zz>PasctRZYZM&zl zLoMHvP?WL#X5vz;%EV4Q)Y(q?WKHD&SxTt2mM!W;U;|ksgUWi@%(XFNfaO+tWX-t& zH`Bq5xlvv7M;aC}m>p&N+#!d!9qGhAV(ENxig3-HGgiZMVt+dz#CQ@4swN^1AJJ5| z3|n>WW@aA|J9jbDKkVBT+fTeX;{5oAucgtJcaG9`Wom9qCT1c4kal=f8A-^!cZyxu zp?|TRri1t2HP0yu5jqJFyopA&fq)bubAFMy< z?v0}RxL&Zc9$&EIzqG6GHEX*5;J6p(v_CIjB*BS&Pz+mmA;EgbQaG9o1rC=6aegVy zNHMvP)mxLLq|>551q^nY$RaH7N%oljCsOC|xhR*UIQLFPskVAc?&9r6KLN-XWM%j=W6CASafxsfLH$!GJe!NKlb~rG}nwe%sMlb|8L3!_r&pfG3N4?ItGt2oC_4$W%RFI{dT>JCPGw^%9 zLU7Na6?tme-^+We`zsIH*36&Emy`PZ;ofpSe=b`HD`!s66ZvpD#i7DcT-Jqf|6T-Z z?_Klz?+*9vUAXhEH}2iPcW>n+9`*b=KWNrj2v22MIykoyA4PAr$ocXU(*(=M^|vjH zDmvLZNWXNs$Gqpy(QHev^5FvHymaPd<FbPc@4a{5{&0QW?(aRg@2*+8?4!&6*?TJQ zoWDO#<4(l!&O&%5Ggs%%>V9ZHUS=0f*4Yt`B5nPIZ5&1v6e$iL4jR`ViwJ{Mau+oh zvv3$SaCqm!)u_SU?NxVe&amr)v^IqEuRoUc;k(|VMnxTQm>_&!9n@A6=GY)*ziRWv>qw&7 z3jKW1-Nk;gdA0FF^z%h_7p_0H`9y8#h%@j%e9a$gOgEnxO=0t@)>l^Z7&YL5goSNh ztuxGk8R81oqgKNF)8zV7@#<_ikL6Z^sc_SJ|q*@%mSS1kMi%LC?_p<(ghxM#sI}zibySHo~#2CTY zZlKZ@4YRd`uRpl{;byXG`2|~nF&RrsqlR{<&eB?bF1C)chz~B#T3KzPhR>K_=;q@H zIgDC^Mzjho;fnD(zKN#1X|P(q{v~Eu6!N(n;Y>knIDsw$BejG@1Rl1MC`Dpotw9@* z@C^$Ge~y>+`Ju$y*3WV2991j$POZ@IBtwTs*vxQZ^c`ybpb)Njqr(RD8310xttS%0 zS4`ZLSjLumzwthBz|+@A8*J}Xl>H5rRO$9s-Dral=7VGUou{vDzF!Hc6w=p*n@@Bb z@WQs@KPqXjKR)6_+kZ-^q%3ykt%+Jd*V}6tQ%s$*91x`<*|%fE3_hN7n#*x&&$HOk z@rppH6*!D(JFSs%MuV&v!|qfXI3xd_a>@q9c&uMqVe{$?g?g%oL!O+x)eps?oMs_}1dU z85jG_ic6xtQgB}3dX0k}Xh}8JdVZitt0dCTq$7BTzdh17x}Q`3fyO}vg!QMdU4INp zY%L^|i$-1;stvK|n@_ZhfkZ@8a&WzAJvwhKDcmw^rxRteJjYE;mi}Ao3TIqQT&%k6 zkL^g}+W1Pf|J#cTQ9RNFdr%xm^W)>TZk!Q~gD{2O24Y#=HZbORg(0F(>H$s~sadya zj?_od*PB<{s#a^)R?)T8yyB7;MTd_4IULGbQj<~?yHp;|j7kq#dGS&hwi)+fC&&Cd z&0!%tU2kX1iT$<{tA;G&&P9oAjZ*Bqpau)nm<{_fDUsv!a2kKSOpzSDFEp6NelJN} zZf+Rx7*yR5R&gvX*g+r2l)%(@_NllK0H+ zWywlLE9vbd#E=sRF-ue}35j|SO&V>!U;fU`&fsF{H2LH$LdD6PtfgCIWmVNC zZWU-WxqFgn%vD`B6-UeJIIT|ZOtu>Jwdh^bjDrZJaX)kJKji?Dj zm{E)_8b(=gQW{*%2 zy>5$2uq-M>WF~Goa)T8nCCQe()Z|n7X^yxzc)AmR?MzVPHa1h#f&)Yt&0&%>dfNlc zpBXe`++c#lVrwz<`4q2%RbXbn^Nr4+D`$03!MVW0=MPp2J|D;x_1lk%{ENn#1FNLc@=-m8qY( ziYyGaUc2Az6=M~WGGG|CIv4Tq1XJ`+jXkNnUBSRRAW_D_N;pHfHITC=9pBUL@y7u1 zDzr=a69_3V%`Zc9@<=0+2HcZVDbho8H7W`wdaF_G=E^#&mHRohvmGHX&__++*p~aI zv9$J4L8nuN!$=M#56ag%-)_9Hvl6kP!)Upq0bp@vDfTq%H8WajhqO|5El|b zSELB5m`&uYrI*U^RYOUM2qHyRG!#Xk6sDqZLU5&OD`ZJ{`&jAdaT_EGt@Lv!ce#d2 zg$>=S(HRQQbO&ZM&vJ*l(PEIXNFm%dmSv+zW=c%_piEb!O$Xbi1${^<)dvI!97{t_8a|RFx2Hipkzxn<-88=RCk-~h{ zy%3IzyZo(Dh%;gpp($1X12K%ttx$6(wDWFG$hac#S_ofUCAcZdM0={5*Nn#X#i&>m zxtC3a&K>Cn$Pt7`$TSeCAt6|2HrAY}WTI~L0<;?zg`;A#kuW&L5sXUZI4;3b*vi_t z%26MsKAuT-g`|$HC)E!{rD?CJX;Gp<_H(?f6!V5|rkA*ltl4BaOyFeM@3oT1&JE|V z;b>NmR)Y(qQ}QkWs7ZYk-fx;9%&}HzN%Ek^Dj3$QR1<_}yxDVN$%J@m8xQ46v|6nO z+guRWgN=adNvgaz+v5sQl?ZayvWSB{yUS7!nU9Gj)|DNRL@_#>(>EKmn{!^A?~GTb z*5IX5e#jbFmW6!KWQpoztFcDnKvPEJhSxdRwQmyRJs?V~FN>(q(yOG}}Msp5( zJgQ&y2?k#M4ws$b$l04yj-1UIf{u0X+n3D-JD07xF?N3FD^klQRl%z2y0gO@+R9`x!^9y1~5PGX1gL{OaZWbz25RYxNm9AwDHGPfRsk>Cu6^=1S= zj-8cIrF7##$BJ4Rcf7lsQ&t~2BW0nPdo<>z;E`GABe#+7y~(;(O=El~P9M++dZiDs?^UeGR!CqG0K0RD&YBC_Ztui4lxNdKZ}*<@xY+ zIo@;86K8qnz3_6+9r1E+RQ9S}lX&pAw~$SGcq1U;j-p4-l$dFG$s6r5r6z~X#@w(7 zc06b(U3lY`#ZJ^Hd_1Q+`UCDMoywug=KIl!3LJ2aT*zz5QGO-OqIDl1BqCzQT4h}` zmosJc>@2x_XBHZchNuQ99hN9}#DbG>8Nb2i0<|WJXDtpc#v&xpkxXLR-L>z>eL@jD zFG#K#7S^1SHym~+#m)dqw<#hw9W2ku#mA+Hk7~xAp&f_QoKdrB7e^fIPSFSz#ChIX z@I`WR$1QK8tlN%-w^Ht<%0_jV+OFgNX8e)5McE4LoPivo@{hpZ$_6O}OS7ERkO_5% z6bXF8U-4u^@-Jj@$-GanXqo)pym|v3l~s}MbCEu0szv4mFKDc?ZnBw0*ghv!(C7k= zYd@-6xj93!8rIj!meDKbfJ~|*zgis&cmbT<$zBwG*eDrPzOFPbzIxYOH9vWIkD{56 zFWC*hk?0W_@`RHKCJR>%`4>CSA8;iUQ&L53tZizqUoI#M%Q9cc98ntM*vhdX3JaS< zdM&Dk4~}ohF15Xd-55q?U9X1oDW-vwE|j=4uhp7?dwNjqG5x ztg=^59ggiTP0YdMhzIf+5%%tC=}M-${uD_RMFNzV72cb(hRXV9P}x7XEQl10cd(Nv zI4lioX`yXa4(eKce$Sih?e803{f2wvI3uR~?<96HaW~nF2;T;gV~zDv8YJ@nw*WV( zQ3FfeDFKLgVU|hEH|BI;j!tvOJmk`pM^nKCrM6`$$8y@f=omsd9gNBIGHG7B4^8`n zPGn4H_IEY9{$aGp>V0Jeu_=|pFWdYxaDHtsS(f5)6P#G+k1{|(1GbEf(%CgGKL59nx-)JtorV8M-4UD^Dg2>? zSQd)h(o}fS#p?Mj zzv&*Gcu24;T}i)lZo(c=1e5nR!%p zZ>ZcTxhCnH$u<`k2P(ysVe$6ZkxlP#`3_i-jkLV7`RNkS9g^9Y0fhymX?1d_7n#>p zrnH|qYNZ&*8^9v*xKl7tC<#+$6Q+=Dii)#*rt409JO>zSLKu?w9fhZ^w82$M+x8mG zRq*SIq@{Hf-Z`3%S}3W+rWhHaE_~rO%S$5`pEWpFtYF9O@zSVt+B<XK+m;Z{{S?|4QzMlnYtX*P5uNd+|;B2Dmk zJ##9%=hDRH(C$7erBLRB@$wO87d;tt{2dZ{j-U{}Z2U02YN3=JGB`JILS&OYihig; zLQja%Ed9f&92pT}7av$86xnpM;9|xf4i1CU5Sc{RQ}A3IoU;@w%NZ7n=!Ii1fQ+M5 z%TOL=!Dg_y7aU3%6byS_e*k_d61ciW$+h`+=G(}<8%a$)B|boxF^`)o!X6*HO92)c zA{4uPM|*;DQ!<(Smr^Tau*sSsW2AfKkwGR4xh%I-%c!)0JsS3BU1O@kQBnfj_XYyN zBu}ZMC$ui3@pq?6yY%p3HXCz1q^(pek-Ugxt@ z$!p0gJvC=ctKMk{cZ$eqnx7o=bTtOg)Lct4&w|!&cr440(7_iWqdQmY9631sPW36| z>xZx!BE;t=ys%OY_r717*j;EKc5 z#;UIuN?P~LSVrRW2bkk!CE8r7F(lt(+twUq@yE w^W`*Bg#LDNUz`Wnf>hux`%5Ji42}2Va@%vZC(!$ny^QwmV{}BwRasU7T literal 1631 zcmaKs-EQ1O6vqcBUrV6~sUQJ@lUB`#TATGYNa-fqs!duHk+iEcrBE)=#6H=bU35&R1bJ}*8m#0Gd3`~W-z zL!2kzo8V92dGHqyxA+Z@-(mX{d>8LOpAlj%jDtbf-{3rW9>K1FYv9YE0++zA!Pmg= z!Q0>uV6guKd=EVL(ox*YV9>J$z5sp+B5Y%e5aLy|H_%=|3;PH$!u9$Qn&;V5{TBMc zPuRoh3tB5ln@&fG}OQ9Ewn`vq)?P-sd9#F

=l&#yBRa$&_9hz>>F9S_oDPvDnOVCa(%Ml_x$-nKRC=P^MgJwC2>qTP(Izv1i1V z84NfhrD@-qf;e|P8M5V=HvYZ*u7LoTTGfP%#eGw-9|m0$hQx_AdN4;{n2Mxj%BrGl z+YZ}OOXbpWwVjYu+s}cv~s?ghOY_aP*LSxmDpDyeL7?p zCY2-H4;*rNF^PrxuWbusMcE%AWGAeqLFbM|X@ZT$gm&yt>}e8aVl(et+Iy65|p}W>wUJCtYyuFrm=<(xb0*ew%-Jk?kdJYbrt-9W8 z&#aTBU~ug9e6cK^XL=SY^4tg?HkUNr%v3#*E%UH92KgM z>&i@|o}#oKmde%7YD?}-$@qz}^)#i@6ef+ur^v3M+MpV9LzSzvLY>9p;dD>2tx;s@ zh$*l3sH&$U8sp&WY*uaSwREV^I<2Q#8<#37ZOvnA(%R_h6|$zzWm-#kjm142-dt+& z2%!c+Ap)cLG!G0FibpP$G3}hTxMk%S5|pcAv2GkhyBad\n" +"Language-Team: MoonyTheDream\n" "Language: pl_PL\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" -"%100<12 || n%100>=14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" -"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" -"X-Generator: Poedit 2.2.1\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 " +"|| n%100>14) ? 1 : 2);\n" +"X-Generator: Poedit 3.2.2\n" #: accounts/accounts.py:341 #, python-brace-format msgid "|c{key}|R is already puppeted by another Account." -msgstr "" +msgstr "|c{key}|R jest obecnie wykorzystywane przez inne Konto." #: accounts/accounts.py:361 #, python-brace-format msgid "" "You cannot control any more puppets (max {_MAX_NR_SIMULTANEOUS_PUPPETS})" msgstr "" +"Nie możesz kontrolować więcej postaci jednocześnie (max " +"{_MAX_NR_SIMULTANEOUS_PUPPETS})" #: accounts/accounts.py:555 msgid "Too many login failures; please try again in a few minutes." -msgstr "" +msgstr "Zbyt wiele nieudanych prób logowania. Spróbuj ponownie za kilka minut." #: accounts/accounts.py:568 accounts/accounts.py:832 msgid "" "|rYou have been banned and cannot continue from here.\n" "If you feel this ban is in error, please email an admin.|x" msgstr "" +"|rZostałeś/aś zbanowany, więc nie masz tu dostępu.\n" +"Jeśli uważasz że zostałeś niesłusznie zbanowany skontaktuj się z adminem.|x" #: accounts/accounts.py:580 msgid "Username and/or password is incorrect." -msgstr "" +msgstr "Błędne nazwa użytkownika i/lub hasło." #: accounts/accounts.py:587 msgid "Too many authentication failures." -msgstr "" +msgstr "Zbyt wiele błędów uwierzytelniania." #: accounts/accounts.py:803 msgid "" "You are creating too many accounts. Please log into an existing account." msgstr "" +"Tworzysz zbyt wielu użytkowników. Proszę zaloguj się do istniejącego konta." #: accounts/accounts.py:849 msgid "" "There was an error creating the Account. If this problem persists, contact " "an admin." msgstr "" +"Pojawił się błąd podczas tworzenia Konta. Jeśli błąd będzie się powtarzał, " +"proszę skontaktuj się z adminem." #: accounts/accounts.py:885 accounts/accounts.py:1801 msgid "An error occurred. Please e-mail an admin if the problem persists." msgstr "" +"Wystąpił błąd. Proszę poinformuj e-mailowo admina jeśli błąd będzie się " +"powtarzał." #: accounts/accounts.py:918 msgid "Account being deleted." -msgstr "Konto zostalo usuniete." +msgstr "Konto zostało usunięte." #: accounts/accounts.py:1475 accounts/accounts.py:1819 #, python-brace-format msgid "|G{key} connected|n" -msgstr "" +msgstr "|G{key} połączył się|n" #: accounts/accounts.py:1481 -#, fuzzy -#| msgid "The destination doesn't exist." msgid "The Character does not exist." -msgstr "Punkt przeznaczenia nie istnieje." +msgstr "Postać nie istnieje." #: accounts/accounts.py:1520 #, python-brace-format msgid "|R{key} disconnected{reason}|n" -msgstr "" +msgstr "|R{key} rozłączył się{reason}|n" #: accounts/accounts.py:1754 msgid "Guest accounts are not enabled on this server." -msgstr "" +msgstr "Konta Gości nie są dozwolone na tym serwerze." #: accounts/accounts.py:1764 msgid "All guest accounts are in use. Please try again later." -msgstr "" +msgstr "Wszystkie Konta Gości są aktualnie zajęte. Prosimy spróbować później." #: commands/cmdhandler.py:84 msgid "" "\n" "An untrapped error occurred.\n" msgstr "" +"\n" +"Wystąpił nieznany błąd.\n" #: commands/cmdhandler.py:89 msgid "" @@ -104,6 +112,9 @@ msgid "" "An untrapped error occurred. Please file a bug report detailing the steps to " "reproduce.\n" msgstr "" +"\n" +"Wystąpił nieznany błąd. Jeśli to możliwe prosimy o zgłoszenie bug'a z opisem " +"kroków do reprodukcji.\n" #: commands/cmdhandler.py:97 msgid "" @@ -111,6 +122,9 @@ msgid "" "A cmdset merger-error occurred. This is often due to a syntax\n" "error in one of the cmdsets to merge.\n" msgstr "" +"\n" +"Wystąpił błąd: cmdset merger-error. Zazwyczaj problemem w tym wypadku\n" +"jest błąd składni w jednym z cmdsets.\n" #: commands/cmdhandler.py:103 msgid "" @@ -118,6 +132,9 @@ msgid "" "A cmdset merger-error occurred. Please file a bug report detailing the\n" "steps to reproduce.\n" msgstr "" +"\n" +"Wystąpił błąd: cmdset merger-error. Jeśli to możliwe zgłoś bug'a\n" +"opisując szczegółowe kroki żeby go zreprodukować.\n" #: commands/cmdhandler.py:112 msgid "" @@ -125,6 +142,9 @@ msgid "" "No command sets found! This is a critical bug that can have\n" "multiple causes.\n" msgstr "" +"\n" +"Nie znaleziono command sets! Jest to błąd krytyczny\n" +"z wieloma możliwymi przyczynami.\n" #: commands/cmdhandler.py:118 msgid "" @@ -133,6 +153,10 @@ msgid "" "disconnecting/reconnecting doesn't\" solve the problem, try to contact\n" "the server admin through\" some other means for assistance.\n" msgstr "" +"\n" +"Nie znaleziono command sets! Jest to błąd krytyczny.\n" +"Jeśli ponowne połączenie nie rozwiąże problemu spróbuj skontaktować\n" +"się z adminem serwera by uzyskać pomoc.\n" #: commands/cmdhandler.py:128 msgid "" @@ -141,6 +165,11 @@ msgid "" "please file a bug report with the Evennia project, including the\n" "traceback and steps to reproduce.\n" msgstr "" +"\n" +"Pojawił się błąd z command handler. Jeśli przyczyną nie jest lokalna " +"zmiana,\n" +"prosimy o zgłoszenie tego bug'a twórcom Evennii, dołączając opis zdarzenia\n" +"i niezbędne kroki do reprodukcji błędu.\n" #: commands/cmdhandler.py:135 msgid "" @@ -148,6 +177,9 @@ msgid "" "A command handler bug occurred. Please notify staff - they should\n" "likely file a bug report with the Evennia project.\n" msgstr "" +"\n" +"Pojawił się błąd z command handler. Prosimy o powiadomienie admina,\n" +"prawdopodobnie muszą oni zgłosić bug do twórców Evenii.\n" #: commands/cmdhandler.py:143 #, python-brace-format @@ -166,19 +198,17 @@ msgstr "" #: commands/cmdhandler.py:715 msgid "There were multiple matches." -msgstr "Znaleziono wiele dopasowan." +msgstr "Znaleziono wiele dopasowań." #: commands/cmdhandler.py:740 -#, fuzzy, python-brace-format -#| msgid "Command '%s' is not available." +#, python-brace-format msgid "Command '{command}' is not available." -msgstr "Komenda '%s' jests niedostepna." +msgstr "Komenda '{command}' jest niedostępna." #: commands/cmdhandler.py:750 -#, fuzzy, python-brace-format -#| msgid " Maybe you meant %s?" +#, python-brace-format msgid " Maybe you meant {command}?" -msgstr " Czy miales na mysli %s?" +msgstr " Czy miałeś na myśli {command}?" #: commands/cmdhandler.py:751 msgid "or" @@ -186,7 +216,7 @@ msgstr "lub" #: commands/cmdhandler.py:754 msgid " Type \"help\" for help." -msgstr " Wpisz \"help\" aby otworzyc pomoc." +msgstr " Wpisz \"help\" aby otworzyć pomoc." #: commands/cmdsethandler.py:89 #, python-brace-format @@ -233,16 +263,11 @@ msgid "Fallback path '{fallback_path}' failed to generate a cmdset." msgstr "" #: commands/cmdsethandler.py:188 commands/cmdsethandler.py:200 -#, fuzzy, python-brace-format -#| msgid "" -#| "\n" -#| "(Unsuccessfully tried '%s')." +#, python-brace-format msgid "" "\n" "(Unsuccessfully tried '{path}')." msgstr "" -"\n" -"(Bezskuteczna proba '%s')." #: commands/cmdsethandler.py:331 #, python-brace-format @@ -291,64 +316,60 @@ msgid "Lock: '{lockdef}' has no valid lock functions." msgstr "" #: objects/objects.py:856 -#, fuzzy, python-brace-format -#| msgid "Couldn't perform move ('%s'). Contact an admin." +#, python-brace-format msgid "Couldn't perform move ({err}). Contact an admin." -msgstr "Nie udalo sie wykonac ruchu ('%s'). Skontaktuj sie z adminem." +msgstr "Nie udało się wykonać ruchu ({err}}). Skontaktuj się z adminem." #: objects/objects.py:866 msgid "The destination doesn't exist." -msgstr "Punkt przeznaczenia nie istnieje." +msgstr "Miejsce przeznaczenia nie istnieje." #: objects/objects.py:978 -#, fuzzy, python-brace-format -#| msgid "Could not find default home '(#%d)'." +#, python-brace-format msgid "Could not find default home '(#{dbid})'." -msgstr "Nie znaleziono domyslnego domu '(#%d)'." +msgstr "Nie znaleziono domyślnego domu '(#{dbid})'." #: objects/objects.py:992 msgid "Something went wrong! You are dumped into nowhere. Contact an admin." -msgstr "Cos poszlo zle! Zostales wrzucony w nicosc. Skontaktuj sie z adminem." +msgstr "" +"Coś poszło nie tak! Zostałeś wrzucony w nicość. Skontaktuj się z adminem." #: objects/objects.py:1145 -#, fuzzy, python-brace-format -#| msgid "Your character %s has been destroyed." +#, python-brace-format msgid "Your character {key} has been destroyed." -msgstr "Twoja postac %s zostala zniszczona." +msgstr "Twoja postać {key} została zniszczona." #: objects/objects.py:1853 #, python-brace-format msgid "You now have {name} in your possession." -msgstr "" +msgstr "{name} znajduje się teraz w Twoim posiadaniu." #: objects/objects.py:1863 #, python-brace-format msgid "{object} arrives to {destination} from {origin}." -msgstr "" +msgstr "{object} dotarł do {destination} z {origin}." #: objects/objects.py:1865 #, python-brace-format msgid "{object} arrives to {destination}." -msgstr "" +msgstr "{object} dotarł do {destination}." #: objects/objects.py:2530 msgid "Invalid character name." -msgstr "" +msgstr "Nieprawidłowa nazwa postaci." #: objects/objects.py:2549 msgid "There are too many characters associated with this account." -msgstr "" +msgstr "Zbyt wiele postaci jest przypisanych do tego konta." #: objects/objects.py:2575 -#, fuzzy -#| msgid "This is User #1." msgid "This is a character." -msgstr "To jest User #1." +msgstr "To jest postać." #: objects/objects.py:2664 #, python-brace-format msgid "|r{obj} has no location and no home is set.|n" -msgstr "" +msgstr "|r{obj} nie posiada swojej lokalizacji ani nie ma ustawionego domu.|n" #: objects/objects.py:2682 #, python-brace-format @@ -356,44 +377,42 @@ msgid "" "\n" "You become |c{name}|n.\n" msgstr "" +"\n" +"Stajesz się |c{name}|n.\n" #: objects/objects.py:2687 #, python-brace-format msgid "{name} has entered the game." -msgstr "" +msgstr "{name}: postać weszła do gry." #: objects/objects.py:2716 #, python-brace-format msgid "{name} has left the game{reason}." -msgstr "" +msgstr "{name}: postać opuściła grę{reason}." #: objects/objects.py:2838 -#, fuzzy -#| msgid "This is User #1." msgid "This is a room." -msgstr "To jest User #1." +msgstr "To jest pomieszczenie." #: objects/objects.py:3045 -#, fuzzy -#| msgid "This is User #1." msgid "This is an exit." -msgstr "To jest User #1." +msgstr "To jest wyjście." #: objects/objects.py:3142 msgid "You cannot go there." -msgstr "" +msgstr "Nie możesz tam pójść." #: prototypes/prototypes.py:55 msgid "Error" -msgstr "" +msgstr "Błąd" #: prototypes/prototypes.py:56 msgid "Warning" -msgstr "" +msgstr "Ostrzeżenie" #: prototypes/prototypes.py:389 msgid "Prototype requires a prototype_key" -msgstr "" +msgstr "Prototyp wymaga prototype_key" #: prototypes/prototypes.py:397 prototypes/prototypes.py:466 #: prototypes/prototypes.py:1092 @@ -410,7 +429,7 @@ msgstr "" #: prototypes/prototypes.py:475 #, python-brace-format msgid "Prototype {prototype_key} was not found." -msgstr "" +msgstr "Prototyp {prototype_key} nie został znaleziony." #: prototypes/prototypes.py:483 #, python-brace-format @@ -418,19 +437,20 @@ msgid "" "{caller} needs explicit 'edit' permissions to delete prototype " "{prototype_key}." msgstr "" +"{caller} potrzebuje uprawnień 'edit' aby usunąć prototyp {prototype_key}." #: prototypes/prototypes.py:605 #, python-brace-format msgid "Found {num} matching prototypes among {module_prototypes}." -msgstr "" +msgstr "Znaleziono {num} dopasowanych prototypów pośród {module_prototypes}." #: prototypes/prototypes.py:765 msgid "No prototypes found." -msgstr "" +msgstr "Nie znaleziono prototypu." #: prototypes/prototypes.py:816 msgid "Prototype lacks a 'prototype_key'." -msgstr "" +msgstr "Prototyp nie posiada 'prototype_key'." #: prototypes/prototypes.py:825 #, python-brace-format @@ -495,15 +515,6 @@ msgid "Script {key}(#{dbid}) of type '{name}': at_repeat() error '{err}'." msgstr "" #: server/initial_setup.py:29 -#, fuzzy -#| msgid "" -#| "\n" -#| "Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com " -#| "if you need\n" -#| "help, want to contribute, report issues or just join the community.\n" -#| "As Account #1 you can create a demo/tutorial area with |w@batchcommand " -#| "tutorial_world.build|n.\n" -#| " " msgid "" "\n" "Welcome to your new |wEvennia|n-based game! Visit https://www.evennia.com if " @@ -516,13 +527,17 @@ msgid "" "play the demo game.\n" msgstr "" "\n" -"Witaj w swojej nowej grze, bazujacej na |wEvennia|n! Odwiedz http://www." +"Witaj w swojej nowej grze, bazującej na |wEvennia|n! Odwiedź http://www." "evennia.com\n" -"jesli potrzebujesz pomocy, chcesz pomoc badz zglosic blad, lub po prostu " -"chcesz dolaczyc do spolecznosci.\n" -"Jako Konto #1 mozesz otworzyc demo/samouczek wpisujac |w@batchcommand " -"tutorial_world.build|n.\n" -" " +"jeśli potrzebujesz pomocy, chcesz pomóc rozwijać ten projekt, bądź zgłosić " +"błąd,\n" +"lub po prostu chcesz dołączyć do społeczności.\n" +"\n" +"Jako Konto #1 (użytkownik uprzywilejowany) możesz otworzyć demo/samouczek\n" +"wpisujac |wbatchcommand tutorial_world.build|n.\n" +"Gdy się on wczyta spróbuj wpisać |wintro|n żeby zacząć samouczek albo |" +"wtutorial|n\n" +"aby zagrać w demo.\n" #: server/initial_setup.py:108 msgid "This is User #1." @@ -530,7 +545,7 @@ msgstr "To jest User #1." #: server/initial_setup.py:128 msgid "Limbo" -msgstr "Otchlan" +msgstr "Otchłań" #: server/portal/portalsessionhandler.py:41 #, python-brace-format @@ -538,18 +553,20 @@ msgid "" "{servername} DoS protection is active.You are queued to connect in {num} " "seconds ..." msgstr "" +"{servername} ma aktualnie włączony DoS protection.Połączymy Cię za {num} " +"sekund ..." #: server/server.py:156 msgid "idle timeout exceeded" -msgstr "czas bezczynnosci przekroczony" +msgstr "maksymalny czas bezczynności przekroczony" #: server/server.py:177 msgid " (connection lost)" -msgstr "" +msgstr " (utracono połączenie)" #: server/sessionhandler.py:41 msgid "Your client sent an incorrect UTF-8 sequence." -msgstr "" +msgstr "Twój klient wysłał nieprawidłową sekwencję UTF-8." #: server/sessionhandler.py:410 msgid " ... Server restarted." @@ -567,26 +584,25 @@ msgstr "Czas bezczynnosci przekroczony, rozlaczanie." msgid "" "Too many failed attempts; you must wait a few minutes before trying again." msgstr "" +"Zbyt wiele nieudanych prób. Musisz poczekać kilka minut zanim spróbujesz " +"ponownie." #: server/validators.py:31 msgid "Sorry, that username is reserved." -msgstr "" +msgstr "Przepraszamy, ta nazwa użytkownika jest zarezerwowana." #: server/validators.py:38 msgid "Sorry, that username is already taken." -msgstr "" +msgstr "Przepraszamy, ta nazwa użytkownika jest zajęta." #: server/validators.py:88 -#, fuzzy, python-brace-format -#| msgid "" -#| "%s From a terminal client, you can also use a phrase of multiple words if " -#| "you enclose the password in double quotes." +#, python-brace-format msgid "" "{policy} From a terminal client, you can also use a phrase of multiple words " "if you enclose the password in double quotes." msgstr "" -"%s Z poziomu terminala, mozesz rowniez uzyc frazy z wieloma slowami jesli " -"ujmiesz haslo w cudzyslowie." +"{policy} Z poziomu terminala, możesz również użyć frazy z wieloma słowami " +"jeśli ujmiesz hasło w cudzysłowie." #: utils/eveditor.py:68 msgid "" @@ -630,6 +646,49 @@ msgid "" "\n" " :echo - turn echoing of the input on/off (helpful for some clients)\n" msgstr "" +"\n" +" - tekst (nie będący komendami poniżej) zostanie zapamiętany jako " +"ostatni wpis do buforu.\n" +" : - przejrzyj cały bufor lub tylko wskazane wiersze \n" +" :: - surowy widok całego buforu lub tylko wskazanych wierszy \n" +" ::: - escape - wyślij ':' jako jedyne znaki w danym wierszu.\n" +" :h - ta pomoc.\n" +"\n" +" :w - zapisz bufor (nie zamykając go)\n" +" :wq - zapisz bufor i wyjdź\n" +" :q - wyjdź (zostaniesz zapytany o zapis zmian, jeśli jakieś były)\n" +" :q! - wyjdź bez zapisywania, system nie zapyta Cię o zapis zmian\n" +"\n" +" :u - (undo) jeden krok wstecz w historii zmian\n" +" :uu - (redo) jeden krok wprzód w historii zmian\n" +" :UU - zresetuj zmiany i przywróć stan początkowy\n" +"\n" +" :dd - usuń ostatni wiersz, lub wskazane wiersze \n" +" :dw - usuń słowo albo regex w całym buforze, albo wierszu \n" +" :DD - wyczyść cały obecny bufor\n" +"\n" +" :y - (yank) skopiuj linię(e) do pamięci\n" +" :x - wytnij linię(e) i zapisz ją w pamięci\n" +" :p - (paste) wklej linię(e) z pamięci bezpośrednio po wierszu " +"\n" +" :i - (insert) wstaw nowy tekst na miejscu wiersza . " +"Poprzedni wiersz zostanie przesunięty w dół\n" +" :r - (replace) zastąp wiersz tekstem \n" +" :I - (insert) wstaw tekst na początku wiersza \n" +" :A - (append) wstaw tekst na koniec wiersza \n" +"\n" +" :s - znajdź/zamień słowo lub regex w buforze lub wierszu " +"\n" +"\n" +" :j - wyjustuj bufor lub wiersz . Dostępne opcje to f, c, l " +"albo r. Domyślnie f (full)\n" +" :f - (flood-fill) wyjustuj do lewej cały bufor lub wiersz : " +"Odpowiednik funkcji :j left\n" +" :fi - (indent) wcięcie na całym buforze lub wierszu\n" +" :fd - (de-indent) cofnij wcięcie na całym buforze lub wierszu \n" +"\n" +" :echo - włącz lub wyłącz echoing twojego input'u (przydatne w wypadku " +"niektórych klientów)\n" #: utils/eveditor.py:108 msgid "" @@ -639,6 +698,11 @@ msgid "" " - a single word, or multiple words with quotes around them.\n" " - longer string, usually not needing quotes.\n" msgstr "" +"\n" +" Legenda:\n" +" - numer wiersza, na przykład: '5' albo zakres, jak: '3:7'.\n" +" - pojedyncze słowo lub wiele słów zapisanych w cudzysłowie.\n" +" - dłuższy tekst, zazwyczaj bez cudzysłowu.\n" #: utils/eveditor.py:117 msgid "" @@ -648,6 +712,11 @@ msgid "" " :> - Increase the level of automatic indentation for the next lines\n" " := - Switch automatic indentation on/off\n" msgstr "" +"\n" +" :! - Wykonaj cały code buffer bez zapisywania go\n" +" :< - Zmniejsz poziom indentacji w kolejnych liniach\n" +" :> - Zwiększ poziom indentacji w kolejnych liniach\n" +" := - Włącz lub wyłącz automatyczną indentację\n" #: utils/eveditor.py:128 #, python-brace-format @@ -657,6 +726,11 @@ msgid "" "\n" "|rBuffer load function error. Could not load initial data.|n\n" msgstr "" +"\n" +"{error}\n" +"\n" +"|rBłąd z załadowaniem bufora funkcji. Nie można załadować danych " +"początkowych.|n\n" #: utils/eveditor.py:136 #, python-brace-format @@ -666,18 +740,22 @@ msgid "" "\n" "|rSave function returned an error. Buffer not saved.|n\n" msgstr "" +"\n" +"{error}\n" +"\n" +"|rFunkcja zapisywania zwróciła błąd. Bufor nie został zapisany.|n\n" #: utils/eveditor.py:143 msgid "|rNo save function defined. Buffer cannot be saved.|n" -msgstr "" +msgstr "|rNie zdefiniowano save function. Bufor nie może zostać zapisany.|n" #: utils/eveditor.py:145 msgid "No changes need saving" -msgstr "" +msgstr "Nie było żadnych zmian do zapisania" #: utils/eveditor.py:146 msgid "Exited editor." -msgstr "" +msgstr "Opuszczono edytor." #: utils/eveditor.py:149 #, python-brace-format @@ -687,6 +765,10 @@ msgid "" "\n" "|rQuit function gave an error. Skipping.|n\n" msgstr "" +"\n" +"{error}\n" +"\n" +"|rFunkcja Quit zwróciła błąd i została pominięta.|n\n" #: utils/eveditor.py:157 #, python-brace-format @@ -698,6 +780,13 @@ msgid "" "to non-persistent mode (which means the editor session won't survive\n" "an eventual server reload - so save often!)|n\n" msgstr "" +"\n" +"{error}\n" +"\n" +"|rStan edytora nie mógł być zapisany pod persistent mode. Zmieniono na\n" +"non-persistent mode (co może oznaczać, że ta sesja nie przetrwa " +"ewentualnego\n" +"przeładowania serwera - zapisuj więc często!)|n\n" #: utils/eveditor.py:167 msgid "" @@ -705,203 +794,207 @@ msgid "" "EvEditor callbacks could not be pickled, for example because it's a class " "method or is defined inside another function." msgstr "" +"EvEditor persistent-mode error. Zazwyczaj przyczyną jest brak możliwości " +"wybrania choćby jednej lub większej ilości wywołań zwrotnych (callbacks) " +"Może tak się dziać na przykład gdy jest to class method albo callback " +"zdefiniowany jest wewnątrz innej funkcji." #: utils/eveditor.py:173 msgid "Nothing to undo." -msgstr "" +msgstr "Nie ma nic do cofnięcia." #: utils/eveditor.py:174 msgid "Nothing to redo." -msgstr "" +msgstr "Nie ma nic do ponowienia (redo)." #: utils/eveditor.py:175 msgid "Undid one step." -msgstr "" +msgstr "Cofnięto krok wstecz." #: utils/eveditor.py:176 msgid "Redid one step." -msgstr "" +msgstr "Ponowiono o jeden krok." #: utils/eveditor.py:494 msgid "Single ':' added to buffer." -msgstr "" +msgstr "Pojedynczy ':' dodano do buforu." #: utils/eveditor.py:509 msgid "Save before quitting?" -msgstr "" +msgstr "Czy zapisać przed wyjściem? (yes/no)" #: utils/eveditor.py:524 msgid "Reverted all changes to the buffer back to original state." -msgstr "" +msgstr "Przywrócono stan sprzed wszystkich zmian." #: utils/eveditor.py:529 #, python-brace-format msgid "Deleted {string}." -msgstr "" +msgstr "Usunięto {string}." #: utils/eveditor.py:534 msgid "You must give a search word to delete." -msgstr "" +msgstr "Musisz podać słowo do wyszukania i usunięcia." #: utils/eveditor.py:540 #, python-brace-format msgid "Removed {arg1} for lines {l1}-{l2}." -msgstr "" +msgstr "Usunięto {arg1} z wierszy {l1}-{l2}." #: utils/eveditor.py:546 #, python-brace-format msgid "Removed {arg1} for {line}." -msgstr "" +msgstr "Usunięto {arg1} z wiersza {line}." #: utils/eveditor.py:562 #, python-brace-format msgid "Cleared {nlines} lines from buffer." -msgstr "" +msgstr "Wyczyszczono wiersze {nlines} z bufora." #: utils/eveditor.py:567 #, python-brace-format msgid "{line}, {cbuf} yanked." -msgstr "" +msgstr "{line}, {cbuf} skopiowane (yanked)." #: utils/eveditor.py:574 #, python-brace-format msgid "{line}, {cbuf} cut." -msgstr "" +msgstr "{line}, {cbuf} wycięte (cut)." #: utils/eveditor.py:578 msgid "Copy buffer is empty." -msgstr "" +msgstr "Pamięć kopii jest pusta." #: utils/eveditor.py:583 #, python-brace-format msgid "Pasted buffer {cbuf} to {line}." -msgstr "" +msgstr "Wklejono z pamięci {cbuf} do wiersza {line}." #: utils/eveditor.py:591 msgid "You need to enter a new line and where to insert it." -msgstr "" +msgstr "Musisz podać tekst nowego wiersza oraz miejsce gdzie go wstawić." #: utils/eveditor.py:596 #, python-brace-format msgid "Inserted {num} new line(s) at {line}." -msgstr "" +msgstr "Wstawiono {num} nowych wierszy zaczynając od {line}." #: utils/eveditor.py:604 msgid "You need to enter a replacement string." -msgstr "" +msgstr "Musisz podać tekst którym zastąpić obecny." #: utils/eveditor.py:609 #, python-brace-format msgid "Replaced {num} line(s) at {line}." -msgstr "" +msgstr "Zastąpiono {num} wiersze/y zaczynając od {line}." #: utils/eveditor.py:616 msgid "You need to enter text to insert." -msgstr "" +msgstr "Musisz podać tekst który ma zostać wstawiony." #: utils/eveditor.py:624 #, python-brace-format msgid "Inserted text at beginning of {line}." -msgstr "" +msgstr "Wstawiono tekst na początku wiersza {line}." #: utils/eveditor.py:628 msgid "You need to enter text to append." -msgstr "" +msgstr "Musisz podać tekst który ma zostać wstawiony." #: utils/eveditor.py:636 #, python-brace-format msgid "Appended text to end of {line}." -msgstr "" +msgstr "Wstawiono tekst na koniec wiersza {line}." #: utils/eveditor.py:641 msgid "You must give a search word and something to replace it with." -msgstr "" +msgstr "Musisz podać hasło do wyszukania oraz coś czym ma być zastąpione." #: utils/eveditor.py:647 #, python-brace-format msgid "Search-replaced {arg1} -> {arg2} for lines {l1}-{l2}." -msgstr "" +msgstr "Wyszukano -> zamieniono {arg1} -> {arg2} dla wierszy {l1}-{l2}." #: utils/eveditor.py:653 #, python-brace-format msgid "Search-replaced {arg1} -> {arg2} for {line}." -msgstr "" +msgstr "Wyszukano -> zamieniono {arg1} -> {arg2} dla wiersza {line}" #: utils/eveditor.py:677 #, python-brace-format msgid "Flood filled lines {l1}-{l2}." -msgstr "" +msgstr "Wyjustowano do lewej wiersze {l1}-{l2}." #: utils/eveditor.py:679 #, python-brace-format msgid "Flood filled {line}." -msgstr "" +msgstr "Wyjustowano do lewej wiersz {line}." #: utils/eveditor.py:701 msgid "Valid justifications are" -msgstr "" +msgstr "Poprawne formy justowania, to" #: utils/eveditor.py:710 #, python-brace-format msgid "{align}-justified lines {l1}-{l2}." -msgstr "" +msgstr "{align}-wyjustowano wiersze {l1}-{l2}." #: utils/eveditor.py:716 #, python-brace-format msgid "{align}-justified {line}." -msgstr "" +msgstr "{align}-wyjustowano wiersz {line}." #: utils/eveditor.py:728 #, python-brace-format msgid "Indented lines {l1}-{l2}." -msgstr "" +msgstr "Dodano wcięcia w wierszach {l1}-{l2}." #: utils/eveditor.py:730 #, python-brace-format msgid "Indented {line}." -msgstr "" +msgstr "Dodano wcięcie w wierszu {line}." #: utils/eveditor.py:740 #, python-brace-format msgid "Removed left margin (dedented) lines {l1}-{l2}." -msgstr "" +msgstr "Usunięto wcięcie w wierszach {l1}-{l2}." #: utils/eveditor.py:745 #, python-brace-format msgid "Removed left margin (dedented) {line}." -msgstr "" +msgstr "Usunięto wcięcie w wierszu {line}." #: utils/eveditor.py:753 #, python-brace-format msgid "Echo mode set to {mode}" -msgstr "" +msgstr "Echo mode ustawiony na {mode}" #: utils/eveditor.py:758 utils/eveditor.py:773 utils/eveditor.py:788 #: utils/eveditor.py:799 msgid "This command is only available in code editor mode." -msgstr "" +msgstr "Ta komenda dostępna jest jedynie w trybie code editor." #: utils/eveditor.py:766 #, python-brace-format msgid "Decreased indentation: new indentation is {indent}." -msgstr "" +msgstr "Zmniejszono Indentacje: nowa indentacja to {indent}." #: utils/eveditor.py:771 utils/eveditor.py:786 msgid "|rManual indentation is OFF.|n Use := to turn it on." -msgstr "" +msgstr "|rManualna indentacja jest WYŁĄCZONA.|n Wpisz := aby ją włączyć." #: utils/eveditor.py:781 #, python-brace-format msgid "Increased indentation: new indentation is {indent}." -msgstr "" +msgstr "Zmniejszono indentację: nowa indentacja to {indent}." #: utils/eveditor.py:795 msgid "Auto-indentation turned on." -msgstr "" +msgstr "Auto-indentacja została włączona." #: utils/eveditor.py:797 msgid "Auto-indentation turned off." -msgstr "" +msgstr "Auto-indentacja została wyłączona." #: utils/eveditor.py:1093 #, python-brace-format @@ -910,7 +1003,7 @@ msgstr "" #: utils/eveditor.py:1101 msgid "(:h for help)" -msgstr "" +msgstr "(:h żeby uzyskać pomoc)" #: utils/evmenu.py:302 #, python-brace-format @@ -918,6 +1011,8 @@ msgid "" "Menu node '{nodename}' is either not implemented or caused an error. Make " "another choice or try 'q' to abort." msgstr "" +"Menu node '{nodename}' jest niezaimplementowane lub wywołało błąd. Wybierz " +"inną opcję alby spróbuj wpisać 'q' żeby anulować." #: utils/evmenu.py:305 #, python-brace-format @@ -930,19 +1025,19 @@ msgstr "Brak opisu." #: utils/evmenu.py:307 msgid "Commands:

, help, quit" -msgstr "" +msgstr "Komendy: , help, quit" #: utils/evmenu.py:308 msgid "Commands: , help" -msgstr "" +msgstr "Komendy: , help" #: utils/evmenu.py:309 msgid "Commands: help, quit" -msgstr "" +msgstr "Komendy: help, quit" #: utils/evmenu.py:310 msgid "Commands: help" -msgstr "" +msgstr "Komendy: help" #: utils/evmenu.py:311 utils/evmenu.py:1850 msgid "Choose an option or try 'help'." @@ -950,49 +1045,48 @@ msgstr "Wybierz opcje lub uzyj komendy 'help'." #: utils/evmenu.py:1375 msgid "|rInvalid choice.|n" -msgstr "" +msgstr "|rNiepoprawny wybór.|n" #: utils/evmenu.py:1439 msgid "|Wcurrent|n" -msgstr "" +msgstr "|Wobecnie|n" #: utils/evmenu.py:1447 msgid "|wp|Wrevious page|n" -msgstr "" +msgstr "|wp|Woprzednia strona|n" #: utils/evmenu.py:1454 msgid "|wn|Wext page|n" -msgstr "" +msgstr "|wn|Wastępna strona|n" #: utils/evmenu.py:1689 msgid "Aborted." -msgstr "" +msgstr "Anulowano." #: utils/evmenu.py:1712 msgid "|rError in ask_yes_no. Choice not confirmed (report to admin)|n" msgstr "" +"|rBłąd przy ask_yes_no. Wybór nie został potwierdzony (zgłoś to adminowi)|n" #: utils/evmore.py:235 msgid "|xExited pager.|n" -msgstr "" +msgstr "|xOpuszczono stronę.|n" #: utils/optionhandler.py:138 utils/optionhandler.py:162 msgid "Option not found!" -msgstr "" +msgstr "Opcja nieznaleziona!" #: utils/optionhandler.py:159 msgid "Option field blank!" -msgstr "" +msgstr "Pole opcji jest puste!" #: utils/optionhandler.py:165 -#, fuzzy -#| msgid "There were multiple matches." msgid "Multiple matches:" -msgstr "Znaleziono wiele dopasowan." +msgstr "Znaleziono wiele dopasowań:" #: utils/optionhandler.py:165 msgid "Please be more specific." -msgstr "" +msgstr "Czy możesz wyrazić się jaśniej?" #: utils/utils.py:2127 #, python-brace-format @@ -1000,6 +1094,8 @@ msgid "" "{obj}.{handlername} is a handler and can't be set directly. To add values, " "use `{obj}.{handlername}.add()` instead." msgstr "" +"{obj}.{handlername} ma typ 'handler' i nie może zostać ustawiony " +"bezpośrednio. Aby dodawać wartość użyj: `{obj}.{handlername}.add()`." #: utils/utils.py:2137 #, python-brace-format @@ -1007,136 +1103,143 @@ msgid "" "{obj}.{handlername} is a handler and can't be deleted directly. To remove " "values, use `{obj}.{handlername}.remove()` instead." msgstr "" +"{obj}.{handlername} ma typ 'handler' i nie może zostać usunięty " +"bezpośrednio. Aby usuwać wartości użyj: `{obj}.{handlername}.remove()`." #: utils/utils.py:2278 -#, fuzzy, python-brace-format +#, python-brace-format #| msgid "Could not find '%s'." msgid "Could not find '{query}'." -msgstr "Nie odnaleziono '%s'." +msgstr "Nie odnaleziono '{query}'." #: utils/utils.py:2285 -#, fuzzy, python-brace-format -#| msgid "More than one match for '%s' (please narrow target):\n" +#, python-brace-format msgid "More than one match for '{query}' (please narrow target):\n" -msgstr "Wiecej niz jedno dopasowanie dla '%s' (prosze zawezyc cel):\n" +msgstr "Więcej niż jedno dopasowanie dla '{query}' (proszę zawęzić cel):\n" #: utils/validatorfuncs.py:25 #, python-brace-format msgid "Input could not be converted to text ({err})" -msgstr "" +msgstr "Wprowadzone dane nie mogą być przekonwertowane na tekst ({err})" #: utils/validatorfuncs.py:34 #, python-brace-format msgid "Nothing entered for a {option_key}!" -msgstr "" +msgstr "Nie wprowadzono nic dla: {option_key}!" #: utils/validatorfuncs.py:38 #, python-brace-format msgid "'{entry}' is not a valid {option_key}." -msgstr "" +msgstr "'{entry}' posiada niepoprawny {option_key}." #: utils/validatorfuncs.py:63 utils/validatorfuncs.py:236 #, python-brace-format msgid "No {option_key} entered!" -msgstr "" +msgstr "Nie wprowadzono nic dla: {option_key}!" #: utils/validatorfuncs.py:72 #, python-brace-format msgid "Timezone string '{acct_tz}' is not a valid timezone ({err})" msgstr "" +"Wprowadzona nazwa strefy czasowej '{acct_tz}' jest nieprawidłowa ({err})" #: utils/validatorfuncs.py:89 utils/validatorfuncs.py:97 #, python-brace-format msgid "{option_key} must be entered in a 24-hour format such as: {timeformat}" msgstr "" +"{option_key} musi zostać wprowadzona jako format 24-godzinny, na przykład: " +"{timeformat}" #: utils/validatorfuncs.py:141 #, python-brace-format msgid "Could not convert section '{interval}' to a {option_key}." -msgstr "" +msgstr "Nie udało się przekonwertować '{interval}' na {option_key}." #: utils/validatorfuncs.py:153 #, python-brace-format msgid "That {option_key} is in the past! Must give a Future datetime!" -msgstr "" +msgstr "{option_key} jest w przeszłości! Musisz podać przyszłą datę!" #: utils/validatorfuncs.py:163 #, python-brace-format msgid "Must enter a whole number for {option_key}!" -msgstr "" +msgstr "{option_key} musi zawierać liczbę całkowitą!" #: utils/validatorfuncs.py:169 #, python-brace-format msgid "Could not convert '{entry}' to a whole number for {option_key}!" msgstr "" +"Nie udało się przekonwertować '{entry}' na liczbę całkowitą dla: " +"{option_key}!" #: utils/validatorfuncs.py:180 #, python-brace-format msgid "Must enter a whole number greater than 0 for {option_key}!" -msgstr "" +msgstr "Musisz podać liczbę całkowitą większą niż 0 dla: {option_key}!" #: utils/validatorfuncs.py:191 #, python-brace-format msgid "{option_key} must be a whole number greater than or equal to 0!" -msgstr "" +msgstr "{option_key} musi być liczbą całkowitą większą lub równą 0!" #: utils/validatorfuncs.py:210 #, python-brace-format msgid "Must enter a true/false input for {option_key}. Accepts {alternatives}." msgstr "" +"{option_key} wymaga true/false jako input. Akceptowane są: {alternatives}." #: utils/validatorfuncs.py:240 #, python-brace-format msgid "That matched: {matches}. Please be more specific!" -msgstr "" +msgstr "Pokrywa się z: {matches}. Spróbuj być bardziej precyzyjny!" #: utils/validatorfuncs.py:247 #, python-brace-format msgid "Could not find timezone '{entry}' for {option_key}!" -msgstr "" +msgstr "Nie odnaleziono strefy czasowej '{entry}' dla: {option_key}!" #: utils/validatorfuncs.py:255 msgid "Email address field empty!" -msgstr "" +msgstr "Pole z adresem e-mail jest puste!" #: utils/validatorfuncs.py:258 #, python-brace-format msgid "That isn't a valid {option_key}!" -msgstr "" +msgstr "To nie jest poprawny {option_key}!" #: utils/validatorfuncs.py:265 #, python-brace-format msgid "No {option_key} entered to set!" -msgstr "" +msgstr "Nie ustawiono żadnych {option_key}}!" #: utils/validatorfuncs.py:269 msgid "Must enter an access type!" -msgstr "" +msgstr "Nie ustawiono żadnych access type!" #: utils/validatorfuncs.py:273 #, python-brace-format msgid "Access type must be one of: {alternatives}" -msgstr "" +msgstr "Access type musi należeć do: {alternatives}" #: utils/validatorfuncs.py:278 msgid "Lock func not entered." -msgstr "" +msgstr "Nie wprowadzono Lock func." #: web/templates/admin/app_list.html:19 msgid "Add" -msgstr "" +msgstr "Dodaj" #: web/templates/admin/app_list.html:26 msgid "View" -msgstr "" +msgstr "Zobacz" #: web/templates/admin/app_list.html:28 msgid "Change" -msgstr "" +msgstr "Zmień" #: web/templates/admin/app_list.html:39 msgid "You don’t have permission to view or edit anything." -msgstr "" +msgstr "Nie masz uprawnień, by odczytać lub edytować cokolwiek." #~ msgid "Say what?" #~ msgstr "Ze co?" From 266366b6e1c71911a23758887ec000438ab76baa Mon Sep 17 00:00:00 2001 From: Moonchasered Date: Sun, 28 Apr 2024 08:18:47 +0200 Subject: [PATCH 047/112] Add Polish translation (Moonchasered) --- evennia/locale/pl/LC_MESSAGES/django.mo | Bin 25353 -> 25356 bytes evennia/locale/pl/LC_MESSAGES/django.po | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/locale/pl/LC_MESSAGES/django.mo b/evennia/locale/pl/LC_MESSAGES/django.mo index ac86aeba6f01bd6619c3993da4f32886163ac617..ebef3b4c280487bf59208895e998a974d24add86 100644 GIT binary patch delta 1328 zcmXZbTS$~a6u|Mbh+39qWot^>YMQNOYFaCzrkk#1Ub}cHFS$XI6^Ut^hI~OGSk{Bk zlMoeBln}y7zYC`j@_D++`P-zw;T_&!+p%^BvKIdjfz@3gIN+BQ5JXhe(1 z^8yhsenx(hUno+EJ@^4X;szY{h(zHG#^HixaFNJ1;v|g3Dh$C^OvX#r{6h>Rp1=~E zDH22K8QAxUII$fQ@HWQdBnIJk+=4$b5hM1S@gCerd=fRGD;SFRaUG7JCj1f)<2>eK zX0bW%D>g*7GttjPHjd$5{EHcwcfj1J1GQ4U7>f^36Pv+RHjy{DmUxb9X@b9TGe(t) za9PR46zoAg?+Nb0d4qx8EV9hpI1lrQy{HQZQ5U{NO(3{jWH~0F-Yf;3Sd2Z`jvoAj zS{ZkR2)Rikrs5FNO_@ba(1@eaT5=Drz%!^lylBn$qbB&&at z2p#whxtuIJB9ex+=)&u$`%PdDenmIuOMI2cdM27tGw#N8yo)}3k6OyoYICC&3?sg0 z=|?;9B(A|v$UBoo)I?o1BAg`+$orKM)b;<+$@vmqYnCP#gNW;JC7wXNNh==1ag4<1 zqas=8LJzi~mUI-|_yc!fG9T%Do#hSG`DeHl7twGru-BQTEyDofPSni1QA;+6JZO1= zF&JKNRw^Ag5m%#b=tJ%O9n|k$<4yd9eR#RS{C9;m(iY;%M(W?eV2BB-A+g8JSKWo$ zid(3UauhYuDdhdhKa9ZgCbI&~sIU4lUc)6THoRu3r%>0=S}viDxKRA>LWZ2dnU2)V ithC+!s~I_At1H^NI=!8~rt@8nmZsC^{I3eKWBvmi9=b9B delta 1325 zcmXZbTS$~a6u|Mbc!|tH(=?@YHOtntG*<+5wY=sfyLc&6TQw^bBuveaVgo};B%voG zDx@eu0%ggUL{w~Fq6i^M@%(YUGKwqT($Hs5jjAdh*4ODLD+#Qc-@+Rjse88Sb@G0 z@kk?suwx<)?8LqJ5EF0?1Mvs$!(W();iV?d!Ck}`P!qa^Tk$D|;UsFpZ?GCyFc&k* z%zSs5MhT6j(Ykn9t!B>{cXeahNZPq1V1aU5gV-2QY zJL&-=sQXW&Uf?TgB2q2X(($bm2G5=X{B)71_Z=D{97ln2wLJ7Z*`WS$4*3bQwd4pIA;{ z1o0e(;%DTY$r@^+&a+en>yh^>lc?)A(82irG*H|fB7oWUrJJ|~ie zPAtYNs3o1keEf+AF^P|KzRq$Gb^bN($2Ih1G6-uhr7cH4;%lgx_o0?-6nW6HfH4@_ zXeyP8vBcG=8+N1i{t@bTZ}A@f#v#0U-u%0Q+$2R@>8AcY491wC8WPiFUUe^OEAFE{ z$|=-D=aKg(8yJZd&87masIPh)2XWnsZ?u?F&!etivRp?SaglfzgT@`3GVN)pS?Nyi aZKo?FxT?Fk{bE<2y{m2Ef%ijUPRxG=(zkN} diff --git a/evennia/locale/pl/LC_MESSAGES/django.po b/evennia/locale/pl/LC_MESSAGES/django.po index 537dae9ee7..5f45ba4ccf 100644 --- a/evennia/locale/pl/LC_MESSAGES/django.po +++ b/evennia/locale/pl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: Evennia Polish translation v0.2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-10-29 18:53+0000\n" -"PO-Revision-Date: 2024-04-27 10:34+0200\n" +"PO-Revision-Date: 2024-04-28 08:16+0200\n" "Last-Translator: MoonyTheDream \n" "Language-Team: MoonyTheDream\n" "Language: pl_PL\n" @@ -378,7 +378,7 @@ msgid "" "You become |c{name}|n.\n" msgstr "" "\n" -"Stajesz się |c{name}|n.\n" +"Przybywasz jako |c{name}|n.\n" #: objects/objects.py:2687 #, python-brace-format From c55d04c21a92bb9a4bf2dcf4246a6c693862e62c Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 28 Apr 2024 08:42:21 +0200 Subject: [PATCH 048/112] Update changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77d13d193c..bd466aa8b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ changes width (assuming client supports NAWS properly) (michaelfaith84) - [Feature][pull3502]: New `sethelp/locks` allows for editing help entry locks after they were first created (chiizujin) -- [Feature][pull3503]: `page` now shows timestamps in local time instead of in UTC. - [Feature][pull3514]: Support `$pron(pronoun, key)` and new `$pconj(verb, key)` (pronoun conjugation) for actor stance (InspectorCaracal) - [Feature][pull3521]: Allow `WORD` (fully capitalized) in GMCP command names From 246d6d00b9897b7debdf9a4cf0ccd6bdc9bdda05 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 28 Apr 2024 08:59:56 +0200 Subject: [PATCH 049/112] Update Changelog --- docs/source/Coding/Changelog.md | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index b603ec36ca..bd466aa8b3 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -1,5 +1,57 @@ # Changelog +## Main branch + +- [Feature][pull3470]: New `exit_order` kwarg to + `DefaultObject.get_display_exits` to easier customize the order in which + standard exits are displayed in a room (chiizujin) +- [Feature][pull3498]: Properly update Evennia's screen width when client + changes width (assuming client supports NAWS properly) (michaelfaith84) +- [Feature][pull3502]: New `sethelp/locks` allows for editing help entry + locks after they were first created (chiizujin) +- [Feature][pull3514]: Support `$pron(pronoun, key)` and new `$pconj(verb, key)` + (pronoun conjugation) for actor stance (InspectorCaracal) +- [Feature][pull3521]: Allow `WORD` (fully capitalized) in GMCP command names + instead of only `Word` (titled) to support specific clients better (InspectorCaracal) +- [Fix][pull3495]: Fix rate in Trait contribs not updating after reload (jaborsh) +- [Fix][pull3491]: Fix traceback in EvEditor when searching with malformed regex (chiizujin) +- [Fix][pull3489]: Superuser could break wilderness contrib exits (t34lbytes) +- [Fix][pull3496]: EvEditor would not correctly show search&replace feedback + when replacing colors (Chiizujin) +- [Fix][pull3499]: Dig/tunnel commands didn't echo the typeclass of the newly + created room properly (chiizujin) +- [Fix][pull3501]: Using `sethelp` to create a help entry colliding with a + command-name made the entry impossible to edit/delete later (chiizujin) +- [Fix][pull3506]: Fix Traceback when setting prototype parent in the in-game OLC wizard (chiizujin) +- [Fix][pull3507]: Prototype wizard would not save changes if aborting the + updating of existing spawned instances (chiizujun) +- [Fix][pull3516]: Quitting the chargen contrib menu will now trigger auto-look (InspectorCaracal) +- [Fix][pull3517]: Supply `Object.search` with an empty `candidates` list caused + defaults to be used instead of finding nothing (InspectorCaracal) +- [Fix][pull3518]: `GlobalScriptsContainer.all()` raised a traceback (InspectorCaracal) +- [Fix][pull3520]: Exits not included in exit sort order were not listed correctly (chiizujin) +- [Docs]: Doc fixes (Griatch, chiizujin) + +[pull3470]: https://github.com/evennia/evennia/pull/3470 +[pull3495]: https://github.com/evennia/evennia/pull/3495 +[pull3491]: https://github.com/evennia/evennia/pull/3491 +[pull3489]: https://github.com/evennia/evennia/pull/3489 +[pull3496]: https://github.com/evennia/evennia/pull/3496 +[pull3498]: https://github.com/evennia/evennia/pull/3498 +[pull3499]: https://github.com/evennia/evennia/pull/3499 +[pull3501]: https://github.com/evennia/evennia/pull/3501 +[pull3502]: https://github.com/evennia/evennia/pull/3502 +[pull3503]: https://github.com/evennia/evennia/pull/3503 +[pull3506]: https://github.com/evennia/evennia/pull/3506 +[pull3507]: https://github.com/evennia/evennia/pull/3507 +[pull3514]: https://github.com/evennia/evennia/pull/3514 +[pull3516]: https://github.com/evennia/evennia/pull/3516 +[pull3517]: https://github.com/evennia/evennia/pull/3517 +[pull3518]: https://github.com/evennia/evennia/pull/3518 +[pull3520]: https://github.com/evennia/evennia/pull/3520 +[pull3521]: https://github.com/evennia/evennia/pull/3521 + + ## Evennia 4.1.1 April 6, 2024 From dff3c3f82c8e84ce3dca17c5c09ae5d40a84f813 Mon Sep 17 00:00:00 2001 From: mike Date: Sun, 28 Apr 2024 12:04:00 -0700 Subject: [PATCH 050/112] Extended the colortest command for truecolor. Changed its output style because of the number of colors it supports (style borrowed from the termstandard repo's test) --- evennia/commands/default/account.py | 37 +++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index dd3878b080..c6ef103678 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -665,7 +665,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS): "INPUTDEBUG": validate_bool, "FORCEDENDLINE": validate_bool, "LOCALECHO": validate_bool, - "TRUECOLOR": validate_bool + "TRUECOLOR": validate_bool, } name = self.lhs.upper() @@ -789,12 +789,12 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS): testing which colors your client support Usage: - color ansi | xterm256 + color ansi | xterm256 | truecolor Prints a color map along with in-mud color codes to use to produce them. It also tests what is supported in your client. Choices are - 16-color ansi (supported in most muds) or the 256-color xterm256 - standard. No checking is done to determine your client supports + 16-color ansi (supported in most muds), the 256-color xterm256 + standard, or truecolor. No checking is done to determine your client supports color - if not you will see rubbish appear. """ @@ -833,6 +833,18 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS): ) return ftable + def make_hex_color_from_column(self, column_number): + r = 255 - column_number * 255 / 76 + g = column_number * 510 / 76 + b = column_number * 255 / 76 + + if g > 255: + g = 510 - g + + return ( + f"#{hex(round(r))[2:].zfill(2)}{hex(round(g))[2:].zfill(2)}{hex(round(b))[2:].zfill(2)}" + ) + def func(self): """Show color tables""" @@ -911,9 +923,24 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS): table = self.table_format(table) string += "\n" + "\n".join("".join(row) for row in table) self.msg(string) + + elif self.args.startswith("t"): + # show abbreviated truecolor sample (16.7 million colors in truecolor) + string = "" + for i in range(76): + string += f"|[{self.make_hex_color_from_column(i)} |n" + + string += ( + "\n" + + "some of the truecolor colors (if not all hues show, your client might not report that it can" + " handle trucolor.):" + ) + + self.msg(string) + else: # malformed input - self.msg("Usage: color ansi||xterm256") + self.msg("Usage: color ansi || xterm256 || truecolor") class CmdQuell(COMMAND_DEFAULT_CLASS): From 29e9e4df9841f16da816d481cfef5b6586093806 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 29 Apr 2024 09:10:10 +0200 Subject: [PATCH 051/112] Update i18n doc page --- docs/source/Concepts/Internationalization.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/Concepts/Internationalization.md b/docs/source/Concepts/Internationalization.md index fc9b53deb5..82d233303b 100644 --- a/docs/source/Concepts/Internationalization.md +++ b/docs/source/Concepts/Internationalization.md @@ -67,6 +67,7 @@ translate *hard-coded strings that the end player may see* - things you can't easily change from your mygame/ folder. Outputs from Commands and Typeclasses are generally *not* translated, nor are console/log outputs. +To cut down on work, you may consider only translating the player-facing commands (look, get etc) and leave the default admin commands in English. To change the language of some commands (such as `look`) you need to override the relevant hook-methods on your Typeclasses (check out the code for the default command to see what it calls). ``` ```{sidebar} Windows users From 4d72fd0ce4e83d8cd0ecdf0fecf3813988083401 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:42:18 -0600 Subject: [PATCH 052/112] update akami/linode info --- docs/source/Setup/Online-Setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/Setup/Online-Setup.md b/docs/source/Setup/Online-Setup.md index 77181c44d3..260774fb08 100644 --- a/docs/source/Setup/Online-Setup.md +++ b/docs/source/Setup/Online-Setup.md @@ -332,7 +332,7 @@ Evennia users: | [Heficed][5] | VPS & Cloud | $5/month | Multiple regions. Cheapest for 1GB ram server is $5/month. | | [Scaleway][6] | Cloud | €3/month / on-demand | EU based (Paris, Amsterdam). Smallest option provides 2GB RAM. | | [Prgmr][10] | VPS | $5/month | 1 month free with a year prepay. You likely want some experience with servers with this option as they don't have a lot of support.| -| [Akami (formerly Linode)][11] | Cloud | $5/month / on-demand | Multiple regions. Smallest option provides 1GB RAM| +| [Akami (formerly Linode)][11] | VPS | $5/month / on-demand | Multiple regions. Smallest option ($5/mo) provides 1GB RAM. Also offers cloud services. | | [Genesis MUD hosting][4] | Shell account | $8/month | Dedicated MUD host with very limited memory offerings. May run very old Python versions. Evennia needs *at least* the "Deluxe" package (50MB RAM) and probably *a lot* higher for a production game. While it's sometimes mentioned in a MUD context, this host is *not* recommended for Evennia.| *Please help us expand this list.* From ded62f6f8b364d5b8e20f42c6cf5a7de9c93d55c Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 29 Apr 2024 11:34:02 -0600 Subject: [PATCH 053/112] Update Contribs-Guidelines.md --- docs/source/Contribs/Contribs-Guidelines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/Contribs/Contribs-Guidelines.md b/docs/source/Contribs/Contribs-Guidelines.md index 2f6d42e48c..dd177248dd 100644 --- a/docs/source/Contribs/Contribs-Guidelines.md +++ b/docs/source/Contribs/Contribs-Guidelines.md @@ -24,7 +24,7 @@ Evennia has a [contrib](./Contribs-Overview.md) directory which contains optiona | `grid/` | _Systems related to the game world’s topology and structure. Contribs related to rooms, exits and map building._ | | `rpg/` | _Systems specifically related to roleplaying and rule implementation like character traits, dice rolling and emoting._ | | `tutorials/` | _Helper resources specifically meant to teach a development concept or to exemplify an Evennia system. Any extra resources tied to documentation tutorials are found here. Also the home of the Tutorial-World and Evadventure demo codes._ | -| `tools/` | _Miscellaneous tools for manipulating text, security auditing, and more._| +| `utils/` | _Miscellaneous tools for manipulating text, security auditing, and more._| - The folder (package) should be on the following form: From 0674486e2b6fbb353d4ebbca4d31b79b8d24b6e4 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Wed, 1 May 2024 16:48:11 +1000 Subject: [PATCH 054/112] Fix page/list command not showing received pages --- evennia/commands/default/comms.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index cac9905a5a..049e4d7c15 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -1413,12 +1413,15 @@ class CmdPage(COMMAND_DEFAULT_CLASS): message = f"{caller.key} {message.strip(':').strip()}" # create the persistent message object + target_perms = " or ".join( + [f"id({target.id})" for target in targets if target != caller] + ) create.create_message( caller, message, receivers=targets, locks=( - f"read:id({caller.id}) or perm(Admin);" + f"read:id({caller.id}) or {target_perms} or perm(Admin);" f"delete:id({caller.id}) or perm(Admin);" f"edit:id({caller.id}) or perm(Admin)" ), @@ -1498,7 +1501,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS): if lastpages: string = f"Your latest pages:\n {lastpages}" else: - string = "You haven't paged anyone yet." + string = "You haven't sent or received any pages yet." self.msg(string) return From bb0a2d3513ff54b3b901c0600760dc120daf8519 Mon Sep 17 00:00:00 2001 From: Chiizujin Date: Thu, 2 May 2024 16:41:46 +1000 Subject: [PATCH 055/112] Fix EvEditor responding to valid direction commands in the current location --- evennia/utils/eveditor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 0245c938c7..4e565f0fba 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -822,6 +822,7 @@ class EvEditorCmdSet(CmdSet): """CmdSet for the editor commands""" key = "editorcmdset" + priority = 150 # override other cmdsets. mergetype = "Replace" def at_cmdset_creation(self): From 3b84ec1b42c99563e06e32668759690676b10f4a Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 2 May 2024 17:01:28 +0200 Subject: [PATCH 056/112] Update evscaperoom contrib readme --- docs/source/Contribs/Contrib-Evscaperoom.md | 31 +++++++++++-------- docs/source/Contribs/Contribs-Overview.md | 4 +-- .../full_systems/evscaperoom/README.md | 31 +++++++++++-------- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/docs/source/Contribs/Contrib-Evscaperoom.md b/docs/source/Contribs/Contrib-Evscaperoom.md index acc69a7463..f31ec8cccf 100644 --- a/docs/source/Contribs/Contrib-Evscaperoom.md +++ b/docs/source/Contribs/Contrib-Evscaperoom.md @@ -5,22 +5,27 @@ Contribution by Griatch, 2019 A full engine for creating multiplayer escape-rooms in Evennia. Allows players to spawn and join puzzle rooms that track their state independently. Any number of players can join to solve a room together. This is the engine created for 'EvscapeRoom', which won -the MUD Coders Guild "One Room" Game Jam in April-May, 2019. The contrib has no game -content but contains the utilities and base classes and an empty example room. - -The original code for the contest is found at -https://github.com/Griatch/evscaperoom but the version on the public Evennia -demo is more updated, so if you really want the latest bug fixes etc you should -rather look at https://github.com/evennia/evdemo/tree/master/evdemo/evscaperoom -instead. A copy of the full game can also be played on the Evennia demo server -at https://demo.evennia.com - just connect to the server and write `evscaperoom` -in the first room to start! +the MUD Coders Guild "One Room" Game Jam in April-May, 2019. The contrib has only +very minimal game content, it contains the utilities and base classes and an empty example room. ## Introduction -Evscaperoom is, as it sounds, an escaperoom in text form. You start locked into -a room and have to figure out how to get out. This engine contains everything -needed to make a fully-featured puzzle game of this type! +Evscaperoom is, as it sounds, an [escape room](https://en.wikipedia.org/wiki/Escape_room) in text form. You start locked into +a room and have to figure out how to get out. This contrib contains everything +needed to make a fully-featured puzzle game of this type. It also contains a +'lobby' for creating new rooms, allowing players to join another person's room +to collaborate solving it! + +This is the game engine for the original _EvscapeRoom_. It +allows you to recreate the same game experience, but it doesn't contain any of +the story content created for the game jam. If you want to see the full game +(where you must escape the cottage of a very tricky jester girl or lose the +village's pie-eating contest...), you can find it at Griatch's github page [here](https://github.com/Griatch/evscaperoom), +(but the recommended version is the one that used to run on the Evennia demo server which has +some more bug fixes, found [here instead](https://github.com/evennia/evdemo/tree/master/evdemo/evscaperoom)). + +If you want to read more about how _EvscapeRoom_ was created and designed, you can read the +dev blog, [part 1](https://www.evennia.com/devblog/2019.html#2019-05-18-creating-evscaperoom-part-1) and [part 2](https://www.evennia.com/devblog/2019.html#2019-05-26-creating-evscaperoom-part-2). ## Installation diff --git a/docs/source/Contribs/Contribs-Overview.md b/docs/source/Contribs/Contribs-Overview.md index b102340566..5468f3f7ce 100644 --- a/docs/source/Contribs/Contribs-Overview.md +++ b/docs/source/Contribs/Contribs-Overview.md @@ -241,8 +241,8 @@ _Contribution by Griatch, 2019_ A full engine for creating multiplayer escape-rooms in Evennia. Allows players to spawn and join puzzle rooms that track their state independently. Any number of players can join to solve a room together. This is the engine created for 'EvscapeRoom', which won -the MUD Coders Guild "One Room" Game Jam in April-May, 2019. The contrib has no game -content but contains the utilities and base classes and an empty example room. +the MUD Coders Guild "One Room" Game Jam in April-May, 2019. The contrib has only +very minimal game content, it contains the utilities and base classes and an empty example room. [Read the documentation](./Contrib-Evscaperoom.md) - [Browse the Code](evennia.contrib.full_systems.evscaperoom) diff --git a/evennia/contrib/full_systems/evscaperoom/README.md b/evennia/contrib/full_systems/evscaperoom/README.md index 088753107c..309fae5ad5 100644 --- a/evennia/contrib/full_systems/evscaperoom/README.md +++ b/evennia/contrib/full_systems/evscaperoom/README.md @@ -5,22 +5,27 @@ Contribution by Griatch, 2019 A full engine for creating multiplayer escape-rooms in Evennia. Allows players to spawn and join puzzle rooms that track their state independently. Any number of players can join to solve a room together. This is the engine created for 'EvscapeRoom', which won -the MUD Coders Guild "One Room" Game Jam in April-May, 2019. The contrib has no game -content but contains the utilities and base classes and an empty example room. - -The original code for the contest is found at -https://github.com/Griatch/evscaperoom but the version on the public Evennia -demo is more updated, so if you really want the latest bug fixes etc you should -rather look at https://github.com/evennia/evdemo/tree/master/evdemo/evscaperoom -instead. A copy of the full game can also be played on the Evennia demo server -at https://demo.evennia.com - just connect to the server and write `evscaperoom` -in the first room to start! +the MUD Coders Guild "One Room" Game Jam in April-May, 2019. The contrib has only +very minimal game content, it contains the utilities and base classes and an empty example room. ## Introduction -Evscaperoom is, as it sounds, an escaperoom in text form. You start locked into -a room and have to figure out how to get out. This engine contains everything -needed to make a fully-featured puzzle game of this type! +Evscaperoom is, as it sounds, an [escape room](https://en.wikipedia.org/wiki/Escape_room) in text form. You start locked into +a room and have to figure out how to get out. This contrib contains everything +needed to make a fully-featured puzzle game of this type. It also contains a +'lobby' for creating new rooms, allowing players to join another person's room +to collaborate solving it! + +This is the game engine for the original _EvscapeRoom_. It +allows you to recreate the same game experience, but it doesn't contain any of +the story content created for the game jam. If you want to see the full game +(where you must escape the cottage of a very tricky jester girl or lose the +village's pie-eating contest...), you can find it at Griatch's github page [here](https://github.com/Griatch/evscaperoom), +(but the recommended version is the one that used to run on the Evennia demo server which has +some more bug fixes, found [here instead](https://github.com/evennia/evdemo/tree/master/evdemo/evscaperoom)). + +If you want to read more about how _EvscapeRoom_ was created and designed, you can read the +dev blog, [part 1](https://www.evennia.com/devblog/2019.html#2019-05-18-creating-evscaperoom-part-1) and [part 2](https://www.evennia.com/devblog/2019.html#2019-05-26-creating-evscaperoom-part-2). ## Installation From bda6bbe80b021ce263d7a5f91b2261b5a72d77d1 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Mon, 13 May 2024 17:16:07 -0400 Subject: [PATCH 057/112] Inject fields by metaclass and add test --- .../base_systems/components/component.py | 9 ++++---- .../contrib/base_systems/components/tests.py | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/evennia/contrib/base_systems/components/component.py b/evennia/contrib/base_systems/components/component.py index 6397b77a07..900092ed13 100644 --- a/evennia/contrib/base_systems/components/component.py +++ b/evennia/contrib/base_systems/components/component.py @@ -15,15 +15,14 @@ class BaseComponent(type): This is the metaclass for components, responsible for registering components to the listing. """ - - @classmethod - def __new__(cls, *args): + def __new__(cls, name, parents, attrs): """ Every class that uses this metaclass will be registered as a component in the Component Listing using its name. All of them require a unique name. """ - new_type = super().__new__(*args) + attrs['_fields'] = {} + new_type = super().__new__(cls, name, parents, attrs) if new_type.__base__ == object: return new_type @@ -53,7 +52,7 @@ class Component(metaclass=BaseComponent): name = "" slot = None - _fields = {} + _fields: dict | None = None def __init__(self, host=None): assert self.name, "All Components must have a name" diff --git a/evennia/contrib/base_systems/components/tests.py b/evennia/contrib/base_systems/components/tests.py index 57d072b8f7..6087a4d6f6 100644 --- a/evennia/contrib/base_systems/components/tests.py +++ b/evennia/contrib/base_systems/components/tests.py @@ -85,6 +85,27 @@ class TestComponents(EvenniaTest): self.assertTrue(self.char1.test_a) self.assertTrue(self.char1.test_b) + def test_character_components_set_fields_properly(self): + test_a_fields = self.char1.test_a._fields + self.assertIn('my_int', test_a_fields) + self.assertIn('my_list', test_a_fields) + self.assertEqual(len(test_a_fields), 2) + + test_b_fields = self.char1.test_b._fields + self.assertIn('my_int', test_b_fields) + self.assertIn('my_list', test_b_fields) + self.assertIn('default_tag', test_b_fields) + self.assertIn('single_tag', test_b_fields) + self.assertIn('multiple_tags', test_b_fields) + self.assertIn('default_single_tag', test_b_fields) + self.assertEqual(len(test_b_fields), 6) + + test_ic_a_fields = self.char1.ic_a._fields + self.assertIn('my_int', test_ic_a_fields) + self.assertIn('my_list', test_ic_a_fields) + self.assertIn('my_other_int', test_ic_a_fields) + self.assertEqual(len(test_ic_a_fields), 3) + def test_inherited_typeclass_does_not_include_child_class_components(self): char_with_c = create.create_object( InheritedTCWithComponents, key="char_with_c", location=self.room1, home=self.room1 From 8ded9e79147fc747d64b755a9aeff7937ddf887a Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Mon, 13 May 2024 17:34:50 -0400 Subject: [PATCH 058/112] Include parents --- evennia/contrib/base_systems/components/component.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/base_systems/components/component.py b/evennia/contrib/base_systems/components/component.py index 900092ed13..edcf21de78 100644 --- a/evennia/contrib/base_systems/components/component.py +++ b/evennia/contrib/base_systems/components/component.py @@ -21,7 +21,15 @@ class BaseComponent(type): as a component in the Component Listing using its name. All of them require a unique name. """ - attrs['_fields'] = {} + attrs_name = attrs.get('name') + if attrs_name and not COMPONENT_LISTING.get(attrs_name): + new_fields = {} + attrs['_fields'] = new_fields + for parent in parents: + _parent_fields = getattr(parent, "_fields") + if _parent_fields: + new_fields.update(_parent_fields) + new_type = super().__new__(cls, name, parents, attrs) if new_type.__base__ == object: return new_type From 421ae46bcd2e48e236d607616400e377cbc4e862 Mon Sep 17 00:00:00 2001 From: Cal Date: Sat, 18 May 2024 00:17:39 -0600 Subject: [PATCH 059/112] update character creator --- .../contrib/rpg/character_creator/README.md | 12 ++++++---- .../character_creator/character_creator.py | 23 ++++++++++++++++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/evennia/contrib/rpg/character_creator/README.md b/evennia/contrib/rpg/character_creator/README.md index 1d7b4d3739..1e47342351 100644 --- a/evennia/contrib/rpg/character_creator/README.md +++ b/evennia/contrib/rpg/character_creator/README.md @@ -100,15 +100,19 @@ character creator menu, as well as supporting exiting/resuming the process. In addition, unlike the core command, it's designed for the character name to be chosen later on via the menu, so it won't parse any arguments passed to it. -### Changes to `Account.at_look` +### Changes to `Account` -The contrib version works mostly the same as core evennia, but adds an -additional check to recognize an in-progress character. If you've modified your -own `at_look` hook, it's an easy addition to make: just add this section to the +The contrib version works mostly the same as core evennia, but modifies `ooc_appearance_template` +to match the contrib's command syntax, and the `at_look` method to recognize an in-progress +character. + +If you've modified your own `at_look` hook, it's an easy change to add: just add this section to the playable character list loop. ```python + # the beginning of the loop starts here for char in characters: + # ... # contrib code starts here if char.db.chargen_step: # currently in-progress character; don't display placeholder names diff --git a/evennia/contrib/rpg/character_creator/character_creator.py b/evennia/contrib/rpg/character_creator/character_creator.py index 7fae70332d..7ef722d320 100644 --- a/evennia/contrib/rpg/character_creator/character_creator.py +++ b/evennia/contrib/rpg/character_creator/character_creator.py @@ -98,10 +98,27 @@ class ContribCmdCharCreate(MuxAccountCommand): class ContribChargenAccount(DefaultAccount): """ - A modified Account class that makes minor changes to the OOC look - output to incorporate in-progress characters. + A modified Account class that changes the OOC look output to better match the contrib and + incorporate in-progress characters. """ + ooc_appearance_template = """ +-------------------------------------------------------------------- +{header} + +{sessions} + + |whelp|n - more commands + |wcharcreate|n - create new character + |wchardelete |n - delete a character + |wic |n - enter the game as character (|wooc|n to get back here) + |wic|n - enter the game as latest character controlled. + +{characters} +{footer} +-------------------------------------------------------------------- +""".strip() + def at_look(self, target=None, session=None, **kwargs): """ Called when this object executes a look. It allows to customize @@ -156,7 +173,7 @@ class ContribChargenAccount(DefaultAccount): txt_sessions = "|wConnected session(s):|n\n" + "\n".join(sess_strings) if not characters: - txt_characters = "You don't have a character yet. Use |wcharcreate|n." + txt_characters = "You don't have a character yet." else: max_chars = ( "unlimited" From 51a741c714f1e191646ca1ab149bfadc09991890 Mon Sep 17 00:00:00 2001 From: iLPdev Date: Sun, 19 May 2024 06:15:14 -0700 Subject: [PATCH 060/112] fix(docs): :memo: fix path to test --- .../Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rules.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rules.md index c882d43c63..f9112a0998 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rules.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rules.md @@ -586,7 +586,7 @@ class TestEvAdventureRuleEngine(BaseEvenniaTest): As before, run the specific test with - evennia test --settings settings.py .evadventure.tests.test_rules + evennia test --settings settings.py evadventure.tests.test_rules ### Mocking and patching From a38291f1b1aba2055be577dbca16fd98dd14c5bb Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 20 May 2024 14:25:21 -0600 Subject: [PATCH 061/112] remove invalid `None` cmdset value --- evennia/commands/cmdhandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 45605f758b..5d5d29d11f 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -350,7 +350,7 @@ def get_and_merge_cmdsets( """ # Gather cmdsets from location, objects in location or carried try: - local_obj_cmdsets = [None] + local_obj_cmdsets = [] try: location = obj.location except Exception: From 379b856e2ae6311130ff504c5e39e9fba139ca9f Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Mon, 20 May 2024 14:27:33 -0600 Subject: [PATCH 062/112] don't attempt to msg unset report_to --- evennia/commands/cmdhandler.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 5d5d29d11f..e8bfe6c4c5 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -438,11 +438,12 @@ def get_and_merge_cmdsets( cmdset for cmdset in object_cmdsets if cmdset and cmdset.key != "_EMPTY_CMDSET" ] # report cmdset errors to user (these should already have been logged) - yield [ - report_to.msg(err_helper(cmdset.errmessage, cmdid=cmdid)) - for cmdset in cmdsets - if cmdset.key == "_CMDSET_ERROR" - ] + if report_to: + yield [ + report_to.msg(err_helper(cmdset.errmessage, cmdid=cmdid)) + for cmdset in cmdsets + if cmdset.key == "_CMDSET_ERROR" + ] if cmdsets: # faster to do tuple on list than to build tuple directly From 4773ee6ac8901192d62c554f74ffd0e3794ce2f0 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Sat, 25 May 2024 21:31:15 -0600 Subject: [PATCH 063/112] add "collectstatic" to git command --- .../utils/git_integration/git_integration.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/evennia/contrib/utils/git_integration/git_integration.py b/evennia/contrib/utils/git_integration/git_integration.py index a1020b8fe7..0a4b66ee04 100644 --- a/evennia/contrib/utils/git_integration/git_integration.py +++ b/evennia/contrib/utils/git_integration/git_integration.py @@ -2,6 +2,7 @@ import datetime import git from django.conf import settings +from django.core.management import call_command import evennia from evennia import CmdSet, InterruptCommand @@ -140,21 +141,25 @@ class GitCommand(MuxCommand): Provide basic Git functionality within the game. """ caller = self.caller + reload = False if self.action == "status": caller.msg(self.get_status()) elif self.action == "branch" or (self.action == "checkout" and not self.args): caller.msg(self.get_branches()) elif self.action == "checkout": - if self.checkout(): - evennia.SESSION_HANDLER.portal_restart_server() + reload = self.checkout() elif self.action == "pull": - if self.pull(): - evennia.SESSION_HANDLER.portal_restart_server() + reload = self.pull() else: caller.msg("You can only git status, git branch, git checkout, or git pull.") return + if reload: + # reload the server and the static file cache + evennia.SESSION_HANDLER.portal_restart_server() + call_command("collectstatic", interactive=False) + class CmdGitEvennia(GitCommand): """ From ff3ae5ccfc662403dcb3a2b588126f3f5fb91580 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Sun, 2 Jun 2024 16:31:20 -0600 Subject: [PATCH 064/112] add session to account execute_cmd --- evennia/contrib/rpg/character_creator/character_creator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/rpg/character_creator/character_creator.py b/evennia/contrib/rpg/character_creator/character_creator.py index 7fae70332d..a51b3273f2 100644 --- a/evennia/contrib/rpg/character_creator/character_creator.py +++ b/evennia/contrib/rpg/character_creator/character_creator.py @@ -87,11 +87,11 @@ class ContribCmdCharCreate(MuxAccountCommand): char = session.new_char if char.db.chargen_step: # this means the character creation process was exited in the middle - account.execute_cmd("look") + account.execute_cmd("look", session=session) else: # this means character creation was completed - start playing! # execute the ic command to start puppeting the character - account.execute_cmd("ic {}".format(char.key)) + account.execute_cmd("ic {}".format(char.key), session=session) EvMenu(session, _CHARGEN_MENU, startnode=startnode, cmd_on_exit=finish_char_callback) From ee92c3fd86d2ea89f27f1fb0455831b656f7e781 Mon Sep 17 00:00:00 2001 From: Cal Date: Sun, 2 Jun 2024 17:06:09 -0600 Subject: [PATCH 065/112] catch WIP characters in IC command --- .../contrib/rpg/character_creator/README.md | 6 ++--- .../character_creator/character_creator.py | 24 ++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/evennia/contrib/rpg/character_creator/README.md b/evennia/contrib/rpg/character_creator/README.md index 1d7b4d3739..e3249281bd 100644 --- a/evennia/contrib/rpg/character_creator/README.md +++ b/evennia/contrib/rpg/character_creator/README.md @@ -7,17 +7,17 @@ Commands for managing and initiating an in-game character-creation menu. ## Installation In your game folder `commands/default_cmdsets.py`, import and add -`ContribCmdCharCreate` to your `AccountCmdSet`. +`ContribChargenCmdSet` to your `AccountCmdSet`. Example: ```python -from evennia.contrib.rpg.character_creator.character_creator import ContribCmdCharCreate +from evennia.contrib.rpg.character_creator.character_creator import ContribChargenCmdSet class AccountCmdSet(default_cmds.AccountCmdSet): def at_cmdset_creation(self): super().at_cmdset_creation() - self.add(ContribCmdCharCreate) + self.add(ContribChargenCmdSet) ``` In your game folder `typeclasses/accounts.py`, import and inherit from `ContribChargenAccount` diff --git a/evennia/contrib/rpg/character_creator/character_creator.py b/evennia/contrib/rpg/character_creator/character_creator.py index a51b3273f2..189c5d4a47 100644 --- a/evennia/contrib/rpg/character_creator/character_creator.py +++ b/evennia/contrib/rpg/character_creator/character_creator.py @@ -23,9 +23,11 @@ from django.conf import settings from evennia import DefaultAccount from evennia.commands.default.muxcommand import MuxAccountCommand +from evennia.commands.default.account import CmdIC +from evennia.commands.cmdset import CmdSet from evennia.objects.models import ObjectDB from evennia.utils.evmenu import EvMenu -from evennia.utils.utils import is_iter +from evennia.utils.utils import is_iter, string_partial_matching _MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS @@ -35,6 +37,17 @@ except AttributeError: _CHARGEN_MENU = "evennia.contrib.rpg.character_creator.example_menu" +class ContribCmdIC(CmdIC): + def func(self): + if self.args: + # check if the args match an in-progress character + wips = [chara for chara in self.account.characters if chara.db.chargen_step] + if matches := string_partial_matching([c.key for c in wips], self.args): + # the character is in progress, resume creation + return self.execute_cmd("charcreate") + super().func() + + class ContribCmdCharCreate(MuxAccountCommand): """ create a new character @@ -96,6 +109,15 @@ class ContribCmdCharCreate(MuxAccountCommand): EvMenu(session, _CHARGEN_MENU, startnode=startnode, cmd_on_exit=finish_char_callback) +class ContribChargenCmdSet(CmdSet): + key = "Contrib Chargen CmdSet" + + def at_cmdset_creation(self): + super().at_cmdset_creation() + self.add(ContribCmdIC) + self.add(ContribCmdCharCreate) + + class ContribChargenAccount(DefaultAccount): """ A modified Account class that makes minor changes to the OOC look From aff765ce32f8d0d495e62390fe8dde22813f8840 Mon Sep 17 00:00:00 2001 From: Moony <39983684+Moonchasered@users.noreply.github.com> Date: Tue, 11 Jun 2024 08:54:12 +0200 Subject: [PATCH 066/112] unidecode added to depedencies --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 7c507aada5..38530697c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ dependencies = [ "Jinja2 < 3.1", "tzdata >= 2022.6", "pydantic < 2.0", + "unidecode <= 1.3.8", # for unit tests and code formatting "mock >= 4.0.3", "model_mommy >= 2.0", From abcd2221b7fb96347e046af3548e4bf206c1033f Mon Sep 17 00:00:00 2001 From: iLPdev Date: Tue, 11 Jun 2024 11:33:29 -0700 Subject: [PATCH 067/112] (docs): Update extended_room.py --- evennia/contrib/grid/extended_room/extended_room.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/grid/extended_room/extended_room.py b/evennia/contrib/grid/extended_room/extended_room.py index 882b235576..1cb2661d83 100644 --- a/evennia/contrib/grid/extended_room/extended_room.py +++ b/evennia/contrib/grid/extended_room/extended_room.py @@ -34,8 +34,8 @@ class CharacterCmdset(default_cmds.Character_CmdSet): ``` -Then reload to make the bew commands available. Note that they only work -on rooms with the typeclass `ExtendedRoom`. Create new rooms with the right +Then, reload to make the new commands available. Note that they only work +on rooms with the `ExtendedRoom` typeclass. Create new rooms with the correct typeclass or use the `typeclass` command to swap existing rooms. """ From f9064e0165ff2bd66ff8dd3792db8a82f106c415 Mon Sep 17 00:00:00 2001 From: Moony <39983684+Moonchasered@users.noreply.github.com> Date: Fri, 14 Jun 2024 12:31:04 +0200 Subject: [PATCH 068/112] Unnecessary dependency removed --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 38530697c3..7c507aada5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,6 @@ dependencies = [ "Jinja2 < 3.1", "tzdata >= 2022.6", "pydantic < 2.0", - "unidecode <= 1.3.8", # for unit tests and code formatting "mock >= 4.0.3", "model_mommy >= 2.0", From be8c024465dc3b40180a130a6143dc95544eac1b Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 14 Jun 2024 12:43:17 +0200 Subject: [PATCH 069/112] Update CHANGELOG, update all auto-generated docs --- CHANGELOG.md | 29 +- docs/source/Coding/Changelog.md | 29 +- docs/source/Concepts/Internationalization.md | 2 +- docs/source/Contribs/Contrib-Achievements.md | 251 ++++++++++++++++++ docs/source/Contribs/Contribs-Overview.md | 33 ++- ....game_systems.achievements.achievements.md | 10 + ...ennia.contrib.game_systems.achievements.md | 18 ++ ...contrib.game_systems.achievements.tests.md | 10 + .../api/evennia.contrib.game_systems.md | 1 + .../contrib/tutorials/evadventure/quests.py | 4 +- 10 files changed, 371 insertions(+), 16 deletions(-) create mode 100644 docs/source/Contribs/Contrib-Achievements.md create mode 100644 docs/source/api/evennia.contrib.game_systems.achievements.achievements.md create mode 100644 docs/source/api/evennia.contrib.game_systems.achievements.md create mode 100644 docs/source/api/evennia.contrib.game_systems.achievements.tests.md diff --git a/CHANGELOG.md b/CHANGELOG.md index bd466aa8b3..0a1b94d6c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,14 @@ (pronoun conjugation) for actor stance (InspectorCaracal) - [Feature][pull3521]: Allow `WORD` (fully capitalized) in GMCP command names instead of only `Word` (titled) to support specific clients better (InspectorCaracal) +- [Feature][pull3447]: New Contrib: Achievements (InspectorCaracal) +- [Feature][pull3494]: Xterm truecolor hex support `|#0f0` style. Expanded + `color true` to test (michaelFaith84) +- [Feature][pull3497]: Add optional width to EvEditor flood-fill commands using + new `=` argument, for example `:f=40` or `:j 1:2 l = 60` (chiizujin) +- [Feature][pull3549]: Run the `collectstatic` command when reloading server to + keep game assets in sync automatically (InspectorCaracal) +- [Language][pull3523]: Updated Polish translation (Moonchasered) - [Fix][pull3495]: Fix rate in Trait contribs not updating after reload (jaborsh) - [Fix][pull3491]: Fix traceback in EvEditor when searching with malformed regex (chiizujin) - [Fix][pull3489]: Superuser could break wilderness contrib exits (t34lbytes) @@ -30,7 +38,15 @@ defaults to be used instead of finding nothing (InspectorCaracal) - [Fix][pull3518]: `GlobalScriptsContainer.all()` raised a traceback (InspectorCaracal) - [Fix][pull3520]: Exits not included in exit sort order were not listed correctly (chiizujin) -- [Docs]: Doc fixes (Griatch, chiizujin) +- [Fix][pull3529]: Fix page/list command not showing received pages correctly (chiizujin) +- [Fix][pull3530]: EvEditor cmdset priority increased so it doesn't respond to + movement commands while in editor (chiizujin) +- [Fix][pull3537]: Bug setting `_fields` in Components contrib (ChrisLR) +- [Fix][pull3542]: Update `character_creator` contrib to use the Account's look + template properly (InspectorCaracal) +- [Fix][pull3545]: Fix fallback issue in cmdhandler for local-object cmdsets (InspectorCaracal) +- [Fix][pull3554]: Fix/readd custom `ic` command to the `character_creator` contrib (InspectorCaracal) +- [Docs]: Doc fixes (Griatch, chiizujin, InspectorCaracal, iLPDev) [pull3470]: https://github.com/evennia/evennia/pull/3470 [pull3495]: https://github.com/evennia/evennia/pull/3495 @@ -50,6 +66,17 @@ [pull3518]: https://github.com/evennia/evennia/pull/3518 [pull3520]: https://github.com/evennia/evennia/pull/3520 [pull3521]: https://github.com/evennia/evennia/pull/3521 +[pull3447]: https://github.com/evennia/evennia/pull/3447 +[pull3494]: https://github.com/evennia/evennia/pull/3494 +[pull3497]: https://github.com/evennia/evennia/pull/3497 +[pull3529]: https://github.com/evennia/evennia/pull/3529 +[pull3530]: https://github.com/evennia/evennia/pull/3530 +[pull3537]: https://github.com/evennia/evennia/pull/3537 +[pull3542]: https://github.com/evennia/evennia/pull/3542 +[pull3545]: https://github.com/evennia/evennia/pull/3545 +[pull3549]: https://github.com/evennia/evennia/pull/3549 +[pull3554]: https://github.com/evennia/evennia/pull/3554 +[pull3523]: https://github.com/evennia/evennia/pull/3523 ## Evennia 4.1.1 diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index bd466aa8b3..0a1b94d6c1 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -13,6 +13,14 @@ (pronoun conjugation) for actor stance (InspectorCaracal) - [Feature][pull3521]: Allow `WORD` (fully capitalized) in GMCP command names instead of only `Word` (titled) to support specific clients better (InspectorCaracal) +- [Feature][pull3447]: New Contrib: Achievements (InspectorCaracal) +- [Feature][pull3494]: Xterm truecolor hex support `|#0f0` style. Expanded + `color true` to test (michaelFaith84) +- [Feature][pull3497]: Add optional width to EvEditor flood-fill commands using + new `=` argument, for example `:f=40` or `:j 1:2 l = 60` (chiizujin) +- [Feature][pull3549]: Run the `collectstatic` command when reloading server to + keep game assets in sync automatically (InspectorCaracal) +- [Language][pull3523]: Updated Polish translation (Moonchasered) - [Fix][pull3495]: Fix rate in Trait contribs not updating after reload (jaborsh) - [Fix][pull3491]: Fix traceback in EvEditor when searching with malformed regex (chiizujin) - [Fix][pull3489]: Superuser could break wilderness contrib exits (t34lbytes) @@ -30,7 +38,15 @@ defaults to be used instead of finding nothing (InspectorCaracal) - [Fix][pull3518]: `GlobalScriptsContainer.all()` raised a traceback (InspectorCaracal) - [Fix][pull3520]: Exits not included in exit sort order were not listed correctly (chiizujin) -- [Docs]: Doc fixes (Griatch, chiizujin) +- [Fix][pull3529]: Fix page/list command not showing received pages correctly (chiizujin) +- [Fix][pull3530]: EvEditor cmdset priority increased so it doesn't respond to + movement commands while in editor (chiizujin) +- [Fix][pull3537]: Bug setting `_fields` in Components contrib (ChrisLR) +- [Fix][pull3542]: Update `character_creator` contrib to use the Account's look + template properly (InspectorCaracal) +- [Fix][pull3545]: Fix fallback issue in cmdhandler for local-object cmdsets (InspectorCaracal) +- [Fix][pull3554]: Fix/readd custom `ic` command to the `character_creator` contrib (InspectorCaracal) +- [Docs]: Doc fixes (Griatch, chiizujin, InspectorCaracal, iLPDev) [pull3470]: https://github.com/evennia/evennia/pull/3470 [pull3495]: https://github.com/evennia/evennia/pull/3495 @@ -50,6 +66,17 @@ [pull3518]: https://github.com/evennia/evennia/pull/3518 [pull3520]: https://github.com/evennia/evennia/pull/3520 [pull3521]: https://github.com/evennia/evennia/pull/3521 +[pull3447]: https://github.com/evennia/evennia/pull/3447 +[pull3494]: https://github.com/evennia/evennia/pull/3494 +[pull3497]: https://github.com/evennia/evennia/pull/3497 +[pull3529]: https://github.com/evennia/evennia/pull/3529 +[pull3530]: https://github.com/evennia/evennia/pull/3530 +[pull3537]: https://github.com/evennia/evennia/pull/3537 +[pull3542]: https://github.com/evennia/evennia/pull/3542 +[pull3545]: https://github.com/evennia/evennia/pull/3545 +[pull3549]: https://github.com/evennia/evennia/pull/3549 +[pull3554]: https://github.com/evennia/evennia/pull/3554 +[pull3523]: https://github.com/evennia/evennia/pull/3523 ## Evennia 4.1.1 diff --git a/docs/source/Concepts/Internationalization.md b/docs/source/Concepts/Internationalization.md index 82d233303b..31c57c5407 100644 --- a/docs/source/Concepts/Internationalization.md +++ b/docs/source/Concepts/Internationalization.md @@ -27,7 +27,7 @@ updated after Sept 2022 will be missing some translations. +---------------+----------------------+--------------+ | la | Latin | Feb 2021 | +---------------+----------------------+--------------+ -| pl | Polish | Feb 2019 | +| pl | Polish | Apr 2024 | +---------------+----------------------+--------------+ | pt | Portugese | Oct 2022 | +---------------+----------------------+--------------+ diff --git a/docs/source/Contribs/Contrib-Achievements.md b/docs/source/Contribs/Contrib-Achievements.md new file mode 100644 index 0000000000..3e4dfa0857 --- /dev/null +++ b/docs/source/Contribs/Contrib-Achievements.md @@ -0,0 +1,251 @@ +# Achievements + +A simple, but reasonably comprehensive, system for tracking achievements. Achievements are defined using ordinary Python dicts, reminiscent of the core prototypes system, and while it's expected you'll use it only on Characters or Accounts, they can be tracked for any typeclassed object. + +The contrib provides several functions for tracking and accessing achievements, as well as a basic in-game command for viewing achievement status. + +## Installation + +This contrib requires creation one or more module files containing your achievement data, which you then add to your settings file to make them available. + +> See the section below on "Creating Achievements" for what to put in this module. + +```python +# in server/conf/settings.py + +ACHIEVEMENT_CONTRIB_MODULES = ["world.achievements"] +``` + +To allow players to check their achievements, you'll also want to add the `achievements` command to your default Character and/or Account command sets. + +```python +# in commands/default_cmdsets.py + +from evennia.contrib.game_systems.achievements.achievements import CmdAchieve + +class CharacterCmdSet(default_cmds.CharacterCmdSet): + key = "DefaultCharacter" + + def at_cmdset_creation(self): + # ... + self.add(CmdAchieve) +``` + +**Optional** - The achievements contrib stores individual progress data on the `achievements` attribute by default, visible via `obj.db.attributes`. You can change this by assigning an attribute (key, category) tuple to the setting `ACHIEVEMENT_CONTRIB_ATTRIBUTE` + +Example: +```py +# in settings.py + +ACHIEVEMENT_CONTRIB_ATTRIBUTE = ("progress_data", "achievements") +``` + + +## Creating achievements + +An achievement is represented by a simple python dictionary defined at the module level in your achievements module(s). + +Each achievement requires certain specific keys to be defined to work properly, along with several optional keys that you can use to override defaults. + +> Note: Any additional keys not described here are included in the achievement data when you access those acheivements through the contrib, so you can easily add your own extended features. + +#### Required keys + +- **name** (str): The searchable name for the achievement. Doesn't need to be unique. +- **category** (str): The category, or general type, of condition which can progress this achievement. Usually this will be a player action or result. e.g. you would use a category of "defeat" on an achievement for killing 10 rats. +- **tracking** (str or list): The specific subset of condition which can progress this achievement. e.g. you would use a tracking value of "rat" on an achievement for killing 10 rats. An achievement can also track multiple things, for example killing 10 rats or snakes. For that situation, assign a list of all the values to check against, e.g. `["rat", "snake"]` + +#### Optional keys + +- **key** (str): *Default value if unset: the variable name.* The unique, case-insensitive key identifying this achievement. +> Note: If any achievements have the same unique key, only *one* will be loaded. It is case-insensitive, but punctuation is respected - "ten_rats", "Ten_Rats" and "TEN_RATS" will conflict, but "ten_rats" and "ten rats" will not. +- **desc** (str): A longer description of the achievement. Common uses for this would be flavor text or hints on how to complete it. +- **count** (int): *Default value if unset: 1* The number of tallies this achievement's requirements need to build up in order to complete the achievement. e.g. killing 10 rats would have a `"count"` value of `10`. For achievements using the "separate" tracking type, *each* item being tracked must tally up to this number to be completed. +- **tracking_type** (str): *Default value if unset: `"sum"`* There are two valid tracking types: "sum" (which is the default) and "separate". `"sum"` will increment a single counter every time any of the tracked items match. `"separate"` will have a counter for each individual item in the tracked items. ("See the Example Achievements" section for a demonstration of the difference.) +- **prereqs** (str or list): The *keys* of any achievements which must be completed before this achievement can start tracking progress. + + +### Example achievements + +A simple achievement which you can get just for logging in the first time. This achievement has no prerequisites and it only needs to be fulfilled once to complete. +```python +# This achievement has the unique key of "first_login_achieve" +FIRST_LOGIN_ACHIEVE = { + "name": "Welcome!", # the searchable, player-friendly display name + "desc": "We're glad to have you here.", # the longer description + "category": "login", # the type of action this tracks + "tracking": "first", # the specific login action +} +``` + +An achievement for killing a total of 10 rats, and another for killing 10 *dire* rats which requires the "kill 10 rats" achievement to be completed first. The dire rats achievement won't begin tracking *any* progress until the first achievement is completed. +```python +# This achievement has the unique key of "ten_rats" instead of "achieve_ten_rats" +ACHIEVE_TEN_RATS = { + "key": "ten_rats", + "name": "The Usual", + "desc": "Why do all these inns have rat problems?", + "category": "defeat", + "tracking": "rat", + "count": 10, +} + +ACHIEVE_DIRE_RATS = { + "name": "Once More, But Bigger", + "desc": "Somehow, normal rats just aren't enough any more.", + "category": "defeat", + "tracking": "dire rat", + "count": 10, + "prereqs": "ACHIEVE_TEN_RATS", +} +``` + +An achievement for buying a total of 5 of apples, oranges, *or* pears. The "sum" tracking types means that all items are tallied together - so it can be completed by buying 5 apples, or 5 pears, or 3 apples, 1 orange and 1 pear, or any other combination of those three fruits that totals to 5. + +```python +FRUIT_FAN_ACHIEVEMENT = { + "name": "A Fan of Fruit", # note, there is no desc here - that's allowed! + "category": "buy", + "tracking": ("apple", "orange", "pear"), + "count": 5, + "tracking_type": "sum", # this is the default, but it's included here for clarity +} +``` + +An achievement for buying 5 *each* of apples, oranges, and pears. The "separate" tracking type means that each of the tracked items is tallied independently of the other items - so you will need 5 apples, 5 oranges, and 5 pears. +```python +FRUIT_BASKET_ACHIEVEMENT = { + "name": "Fruit Basket", + "desc": "One kind of fruit just isn't enough.", + "category": "buy", + "tracking": ("apple", "orange", "pear"), + "count": 5, + "tracking_type": "separate", +} +``` + + +## Usage + +The two main things you'll need to do in order to use the achievements contrib in your game are **tracking achievements** and **getting achievement information**. The first is done with the function `track_achievements`; the second can be done with `search_achievement` or `get_achievement`. + +### Tracking achievements + +#### `track_achievements` + +In any actions or functions in your game's mechanics which you might want to track in an achievement, add a call to `track_achievements` to update that player's achievement progress. + +Using the "kill 10 rats" example achievement from earlier, you might have some code that triggers when a character is defeated: for the sake of example, we'll pretend we have an `at_defeated` method on the base Object class that gets called when the Object is defeated. + +Adding achievement tracking to it could then look something like this: + +```python +# in typeclasses/objects.py + +from contrib.game_systems.achievements import track_achievements + +class Object(ObjectParent, DefaultObject): + # .... + + def at_defeated(self, victor): + """called when this object is defeated in combat""" + # we'll use the "mob_type" tag-category as the tracked info + # this way we can have rats named "black rat" and "brown rat" that are both rats + mob_type = self.tags.get(category="mob_type") + # only one mob was defeated, so we include a count of 1 + track_achievements(victor, category="defeated", tracking=mob_type, count=1) +``` + +If a player defeats something tagged `rat` with a tag category of `mob_type`, it'd now count towards the rat-killing achievement. + +You can also have the tracking information hard-coded into your game, for special or unique situations. The achievement described earlier, `FIRST_LOGIN_ACHIEVE`, for example, would be tracked like this: + +```py +# in typeclasses/accounts.py +from contrib.game_systems.achievements import track_achievements + +class Account(DefaultAccount): + # ... + + def at_first_login(self, **kwargs): + # this function is only called on the first time the account logs in + # so we already know and can just tell the tracker that this is the first + track_achievements(self, category="login", tracking="first") +``` + +The `track_achievements` function does also return a value: an iterable of keys for any achievements which were newly completed by that update. You can ignore this value, or you can use it to e.g. send a message to the player with their latest achievements. + +### Getting achievements + +The main method for getting a specific achievement's information is `get_achievement`, which takes an already-known achievement key and returns the data for that one achievement. + +For handling more variable and player-friendly input, however, there is also `search_achievement`, which does partial matching on not just the keys, but also the display names and descriptions for the achievements. + +#### `get_achievement` + +A utility function for retrieving a specific achievement's data from the achievement's unique key. It cannot be used for searching, but if you already have an achievement's key - for example, from the results of `track_achievements` - you can retrieve its data this way. + +#### Example: + +```py +from evennia.contrib.game_systems.achievements import get_achievement + +def toast(achiever, completed_list): + if completed_list: + # `completed_data` will be a list of dictionaries - unrecognized keys return empty dictionaries + completed_data = [get_achievement(key) for key in args] + names = [data.get('name') for data in completed] + achiever.msg(f"|wAchievement Get!|n {iter_to_str(name for name in names if name)}")) +``` + +#### `search_achievement` + +A utility function for searching achievements by name or description. It handles partial matching and returns a dictionary of matching achievements. The provided `achievement` command for in-game uses this function to find matching achievements from user inputs. + +#### Example: + +The first example does a search for "fruit", which returns the fruit medley achievement as it contains "fruit" in the key and name. + +The second example searches for "usual", which returns the ten rats achievement due to its display name. + +```py +>>> from evennia.contrib.game_systems.achievements import search_achievement +>>> search_achievement("fruit") +{'fruit_basket_achievement': {'name': 'Fruit Basket', 'desc': "One kind of fruit just isn't enough.", 'category': 'buy', 'tracking': ('apple', 'orange', 'pear'), 'count': 5, 'tracking_type': 'separate'}} +>>> search_achievement("usual") +{'ten_rats': {'key': 'ten_rats', 'name': 'The Usual', 'desc': 'Why do all these inns have rat problems?', 'category': 'defeat', 'tracking': 'rat', 'count': 10}} +``` + +### The `achievements` command + +The contrib's provided command, `CmdAchieve`, aims to be usable as-is, with multiple switches to filter achievements by various progress statuses and the ability to search by achievement names. + +To make it easier to customize for your own game (e.g. displaying some of that extra achievement data you might have added), the format and style code is split out from the command logic into the `format_achievement` method and the `template` attribute, both on `CmdAchieve` + +#### Example output + +``` +> achievements +The Usual +Why do all these inns have rat problems? +70% complete +A Fan of Fruit + +Not Started +``` +``` +> achievements/progress +The Usual +Why do all these inns have rat problems? +70% complete +``` +``` +> achievements/done +There are no matching achievements. +``` + + +---- + +This document page is generated from `evennia/contrib/game_systems/achievements/README.md`. Changes to this +file will be overwritten, so edit that file rather than this one. diff --git a/docs/source/Contribs/Contribs-Overview.md b/docs/source/Contribs/Contribs-Overview.md index 5468f3f7ce..49f0e4f431 100644 --- a/docs/source/Contribs/Contribs-Overview.md +++ b/docs/source/Contribs/Contribs-Overview.md @@ -7,7 +7,7 @@ in the [Community Contribs & Snippets][forum] forum. _Contribs_ are optional code snippets and systems contributed by the Evennia community. They vary in size and complexity and may be more specific about game types and styles than 'core' Evennia. -This page is auto-generated and summarizes all **49** contribs currently included +This page is auto-generated and summarizes all **50** contribs currently included with the Evennia distribution. All contrib categories are imported from `evennia.contrib`, such as @@ -29,16 +29,16 @@ If you want to add a contrib, see [the contrib guidelines](./Contribs-Guidelines | | | | | | |---|---|---|---|---| -| [auditing](#auditing) | [awsstorage](#awsstorage) | [barter](#barter) | [batchprocessor](#batchprocessor) | [bodyfunctions](#bodyfunctions) | -| [buffs](#buffs) | [building_menu](#building_menu) | [character_creator](#character_creator) | [clothing](#clothing) | [color_markups](#color_markups) | -| [components](#components) | [containers](#containers) | [cooldowns](#cooldowns) | [crafting](#crafting) | [custom_gametime](#custom_gametime) | -| [dice](#dice) | [email_login](#email_login) | [evadventure](#evadventure) | [evscaperoom](#evscaperoom) | [extended_room](#extended_room) | -| [fieldfill](#fieldfill) | [gendersub](#gendersub) | [git_integration](#git_integration) | [godotwebsocket](#godotwebsocket) | [health_bar](#health_bar) | -| [ingame_map_display](#ingame_map_display) | [ingame_python](#ingame_python) | [llm](#llm) | [mail](#mail) | [mapbuilder](#mapbuilder) | -| [menu_login](#menu_login) | [mirror](#mirror) | [multidescer](#multidescer) | [mux_comms_cmds](#mux_comms_cmds) | [name_generator](#name_generator) | -| [puzzles](#puzzles) | [random_string_generator](#random_string_generator) | [red_button](#red_button) | [rpsystem](#rpsystem) | [simpledoor](#simpledoor) | -| [slow_exit](#slow_exit) | [talking_npc](#talking_npc) | [traits](#traits) | [tree_select](#tree_select) | [turnbattle](#turnbattle) | -| [tutorial_world](#tutorial_world) | [unixcommand](#unixcommand) | [wilderness](#wilderness) | [xyzgrid](#xyzgrid) | +| [achievements](#achievements) | [auditing](#auditing) | [awsstorage](#awsstorage) | [barter](#barter) | [batchprocessor](#batchprocessor) | +| [bodyfunctions](#bodyfunctions) | [buffs](#buffs) | [building_menu](#building_menu) | [character_creator](#character_creator) | [clothing](#clothing) | +| [color_markups](#color_markups) | [components](#components) | [containers](#containers) | [cooldowns](#cooldowns) | [crafting](#crafting) | +| [custom_gametime](#custom_gametime) | [dice](#dice) | [email_login](#email_login) | [evadventure](#evadventure) | [evscaperoom](#evscaperoom) | +| [extended_room](#extended_room) | [fieldfill](#fieldfill) | [gendersub](#gendersub) | [git_integration](#git_integration) | [godotwebsocket](#godotwebsocket) | +| [health_bar](#health_bar) | [ingame_map_display](#ingame_map_display) | [ingame_python](#ingame_python) | [llm](#llm) | [mail](#mail) | +| [mapbuilder](#mapbuilder) | [menu_login](#menu_login) | [mirror](#mirror) | [multidescer](#multidescer) | [mux_comms_cmds](#mux_comms_cmds) | +| [name_generator](#name_generator) | [puzzles](#puzzles) | [random_string_generator](#random_string_generator) | [red_button](#red_button) | [rpsystem](#rpsystem) | +| [simpledoor](#simpledoor) | [slow_exit](#slow_exit) | [talking_npc](#talking_npc) | [traits](#traits) | [tree_select](#tree_select) | +| [turnbattle](#turnbattle) | [tutorial_world](#tutorial_world) | [unixcommand](#unixcommand) | [wilderness](#wilderness) | [xyzgrid](#xyzgrid) | @@ -266,6 +266,7 @@ Contribs-Guidelines.md ```{toctree} :maxdepth: 1 +Contrib-Achievements.md Contrib-Barter.md Contrib-Clothing.md Contrib-Containers.md @@ -279,6 +280,16 @@ Contrib-Turnbattle.md ``` +### `achievements` + +_A simple, but reasonably comprehensive, system for tracking achievements. Achievements are defined using ordinary Python dicts, reminiscent of the core prototypes system, and while it's expected you'll use it only on Characters or Accounts, they can be tracked for any typeclassed object._ + +The contrib provides several functions for tracking and accessing achievements, as well as a basic in-game command for viewing achievement status. + +[Read the documentation](./Contrib-Achievements.md) - [Browse the Code](evennia.contrib.game_systems.achievements) + + + ### `barter` _Contribution by Griatch, 2012_ diff --git a/docs/source/api/evennia.contrib.game_systems.achievements.achievements.md b/docs/source/api/evennia.contrib.game_systems.achievements.achievements.md new file mode 100644 index 0000000000..f0ac457954 --- /dev/null +++ b/docs/source/api/evennia.contrib.game_systems.achievements.achievements.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.game\_systems.achievements.achievements +============================================================== + +.. automodule:: evennia.contrib.game_systems.achievements.achievements + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.game_systems.achievements.md b/docs/source/api/evennia.contrib.game_systems.achievements.md new file mode 100644 index 0000000000..ecff7b9b81 --- /dev/null +++ b/docs/source/api/evennia.contrib.game_systems.achievements.md @@ -0,0 +1,18 @@ +```{eval-rst} +evennia.contrib.game\_systems.achievements +================================================== + +.. automodule:: evennia.contrib.game_systems.achievements + :members: + :undoc-members: + :show-inheritance: + + + +.. toctree:: + :maxdepth: 6 + + evennia.contrib.game_systems.achievements.achievements + evennia.contrib.game_systems.achievements.tests + +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.game_systems.achievements.tests.md b/docs/source/api/evennia.contrib.game_systems.achievements.tests.md new file mode 100644 index 0000000000..f883c8ee86 --- /dev/null +++ b/docs/source/api/evennia.contrib.game_systems.achievements.tests.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.game\_systems.achievements.tests +======================================================= + +.. automodule:: evennia.contrib.game_systems.achievements.tests + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.game_systems.md b/docs/source/api/evennia.contrib.game_systems.md index 72bbb06be7..07ddfa33e1 100644 --- a/docs/source/api/evennia.contrib.game_systems.md +++ b/docs/source/api/evennia.contrib.game_systems.md @@ -11,6 +11,7 @@ evennia.contrib.game\_systems .. toctree:: :maxdepth: 6 + evennia.contrib.game_systems.achievements evennia.contrib.game_systems.barter evennia.contrib.game_systems.clothing evennia.contrib.game_systems.containers diff --git a/evennia/contrib/tutorials/evadventure/quests.py b/evennia/contrib/tutorials/evadventure/quests.py index 5e862ede29..e4ec77372f 100644 --- a/evennia/contrib/tutorials/evadventure/quests.py +++ b/evennia/contrib/tutorials/evadventure/quests.py @@ -68,8 +68,8 @@ class EvAdventureQuest: self.data = self.questhandler.load_quest_data(self.key) self._current_step = self.get_data("current_step") - if not self.current_step: - self.current_step = self.start_step + if not self._current_step: + self._current_step = self.start_step def add_data(self, key, value): """ From 7b299f2cad1b5435f3d75ffeb69a09db8581003a Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 14 Jun 2024 23:44:41 +0200 Subject: [PATCH 070/112] Make all model .created_date properties resolve to TIME_ZONE time zone. Resolve #3522. --- CHANGELOG.md | 3 + docs/source/Coding/Changelog.md | 3 + docs/source/Components/Typeclasses.md | 108 +++++------------- .../Contribs/Contrib-Character-Creator.md | 18 +-- docs/source/api/evennia.utils.md | 1 + evennia/commands/default/comms.py | 5 +- evennia/comms/models.py | 8 +- evennia/help/models.py | 10 +- evennia/typeclasses/models.py | 41 ++++--- 9 files changed, 81 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a1b94d6c1..a358bbcdc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ new `=` argument, for example `:f=40` or `:j 1:2 l = 60` (chiizujin) - [Feature][pull3549]: Run the `collectstatic` command when reloading server to keep game assets in sync automatically (InspectorCaracal) +- [Feature][issue3522]: (also a fix) Make `.created_date` property on all models property return + a time adjusted based on `settings.TIME_ZONE` (Griatch) - [Language][pull3523]: Updated Polish translation (Moonchasered) - [Fix][pull3495]: Fix rate in Trait contribs not updating after reload (jaborsh) - [Fix][pull3491]: Fix traceback in EvEditor when searching with malformed regex (chiizujin) @@ -77,6 +79,7 @@ [pull3549]: https://github.com/evennia/evennia/pull/3549 [pull3554]: https://github.com/evennia/evennia/pull/3554 [pull3523]: https://github.com/evennia/evennia/pull/3523 +[issue3522]: https://github.com/evennia/evennia/issue/3522 ## Evennia 4.1.1 diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 0a1b94d6c1..a358bbcdc1 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -20,6 +20,8 @@ new `=` argument, for example `:f=40` or `:j 1:2 l = 60` (chiizujin) - [Feature][pull3549]: Run the `collectstatic` command when reloading server to keep game assets in sync automatically (InspectorCaracal) +- [Feature][issue3522]: (also a fix) Make `.created_date` property on all models property return + a time adjusted based on `settings.TIME_ZONE` (Griatch) - [Language][pull3523]: Updated Polish translation (Moonchasered) - [Fix][pull3495]: Fix rate in Trait contribs not updating after reload (jaborsh) - [Fix][pull3491]: Fix traceback in EvEditor when searching with malformed regex (chiizujin) @@ -77,6 +79,7 @@ [pull3549]: https://github.com/evennia/evennia/pull/3549 [pull3554]: https://github.com/evennia/evennia/pull/3554 [pull3523]: https://github.com/evennia/evennia/pull/3523 +[issue3522]: https://github.com/evennia/evennia/issue/3522 ## Evennia 4.1.1 diff --git a/docs/source/Components/Typeclasses.md b/docs/source/Components/Typeclasses.md index 2cdc2034b3..19e221ac23 100644 --- a/docs/source/Components/Typeclasses.md +++ b/docs/source/Components/Typeclasses.md @@ -45,8 +45,7 @@ The `typeclass/list` command will provide a list of all typeclasses known to Eve ## Difference between typeclasses and classes -All Evennia classes inheriting from class in the table above share one important feature and two -[]()important limitations. This is why we don't simply call them "classes" but "typeclasses". +All Evennia classes inheriting from class in the table above share one important feature and two important limitations. This is why we don't simply call them "classes" but "typeclasses". 1. A typeclass can save itself to the database. This means that some properties (actually not that many) on the class actually represents database fields and can only hold very specific data types. 1. Due to its connection to the database, the typeclass' name must be *unique* across the _entire_ server namespace. That is, there must never be two same-named classes defined anywhere. So the below code would give an error (since `DefaultObject` is now globally found both in this module and in the default library): @@ -66,15 +65,14 @@ All Evennia classes inheriting from class in the table above share one important # my content ``` -Apart from this, a typeclass works like any normal Python class and you can -treat it as such. +Apart from this, a typeclass works like any normal Python class and you can treat it as such. ## Working with typeclasses ### Creating a new typeclass It's easy to work with Typeclasses. Either you use an existing typeclass or you create a new Python class inheriting from an existing typeclass. Here is an example of creating a new type of Object: - + ```python from evennia import DefaultObject @@ -94,9 +92,7 @@ chair.save() ``` -To use this you must give the database field names as keywords to the call. Which are available -depends on the entity you are creating, but all start with `db_*` in Evennia. This is a method you -may be familiar with if you know Django from before. +To use this you must give the database field names as keywords to the call. Which are available depends on the entity you are creating, but all start with `db_*` in Evennia. This is a method you may be familiar with if you know Django from before. It is recommended that you instead use the `create_*` functions to create typeclassed entities: @@ -109,17 +105,9 @@ chair = create_object(Furniture, key="Chair") chair = create_object("furniture.Furniture", key="Chair") ``` -The `create_object` (`create_account`, `create_script` etc) takes the typeclass as its first -argument; this can both be the actual class or the python path to the typeclass as found under your -game directory. So if your `Furniture` typeclass sits in `mygame/typeclasses/furniture.py`, you -could point to it as `typeclasses.furniture.Furniture`. Since Evennia will itself look in -`mygame/typeclasses`, you can shorten this even further to just `furniture.Furniture`. The create- -functions take a lot of extra keywords allowing you to set things like [Attributes](./Attributes.md) and -[Tags](./Tags.md) all in one go. These keywords don't use the `db_*` prefix. This will also automatically -save the new instance to the database, so you don't need to call `save()` explicitly. +The `create_object` (`create_account`, `create_script` etc) takes the typeclass as its first argument; this can both be the actual class or the python path to the typeclass as found under your game directory. So if your `Furniture` typeclass sits in `mygame/typeclasses/furniture.py`, you could point to it as `typeclasses.furniture.Furniture`. Since Evennia will itself look in `mygame/typeclasses`, you can shorten this even further to just `furniture.Furniture`. The create-functions take a lot of extra keywords allowing you to set things like [Attributes](./Attributes.md) and [Tags](./Tags.md) all in one go. These keywords don't use the `db_*` prefix. This will also automatically save the new instance to the database, so you don't need to call `save()` explicitly. -An example of a database field is `db_key`. This stores the "name" of the entity you are modifying -and can thus only hold a string. This is one way of making sure to update the `db_key`: +An example of a database field is `db_key`. This stores the "name" of the entity you are modifying and can thus only hold a string. This is one way of making sure to update the `db_key`: ```python chair.db_key = "Table" @@ -129,9 +117,7 @@ print(chair.db_key) <<< Table ``` -That is, we change the chair object to have the `db_key` "Table", then save this to the database. -However, you almost never do things this way; Evennia defines property wrappers for all the database -fields. These are named the same as the field, but without the `db_` part: +That is, we change the chair object to have the `db_key` "Table", then save this to the database. However, you almost never do things this way; Evennia defines property wrappers for all the database fields. These are named the same as the field, but without the `db_` part: ```python chair.key = "Table" @@ -141,44 +127,32 @@ print(chair.key) ``` -The `key` wrapper is not only shorter to write, it will make sure to save the field for you, and -does so more efficiently by levering sql update mechanics under the hood. So whereas it is good to -be aware that the field is named `db_key` you should use `key` as much as you can. +The `key` wrapper is not only shorter to write, it will make sure to save the field for you, and does so more efficiently by levering sql update mechanics under the hood. So whereas it is good to be aware that the field is named `db_key` you should use `key` as much as you can. Each typeclass entity has some unique fields relevant to that type. But all also share the following fields (the wrapper name without `db_` is given): - - `key` (str): The main identifier for the entity, like "Rose", "myscript" or "Paul". `name` is an -alias. + - `key` (str): The main identifier for the entity, like "Rose", "myscript" or "Paul". `name` is an alias. - `date_created` (datetime): Time stamp when this object was created. - `typeclass_path` (str): A python path pointing to the location of this (type)class There is one special field that doesn't use the `db_` prefix (it's defined by Django): - - `id` (int): the database id (database ref) of the object. This is an ever-increasing, unique -integer. It can also be accessed as `dbid` (database ID) or `pk` (primary key). The `dbref` property -returns the string form "#id". + - `id` (int): the database id (database ref) of the object. This is an ever-increasing, unique integer. It can also be accessed as `dbid` (database ID) or `pk` (primary key). The `dbref` property returns the string form "#id". The typeclassed entity has several common handlers: - `tags` - the [TagHandler](./Tags.md) that handles tagging. Use `tags.add()` , `tags.get()` etc. - - `locks` - the [LockHandler](./Locks.md) that manages access restrictions. Use `locks.add()`, -`locks.get()` etc. - - `attributes` - the [AttributeHandler](./Attributes.md) that manages Attributes on the object. Use -`attributes.add()` + - `locks` - the [LockHandler](./Locks.md) that manages access restrictions. Use `locks.add()`, `locks.get()` etc. + - `attributes` - the [AttributeHandler](./Attributes.md) that manages Attributes on the object. Use `attributes.add()` etc. - `db` (DataBase) - a shortcut property to the AttributeHandler; allowing `obj.db.attrname = value` - `nattributes` - the [Non-persistent AttributeHandler](./Attributes.md) for attributes not saved in the database. - - `ndb` (NotDataBase) - a shortcut property to the Non-peristent AttributeHandler. Allows -`obj.ndb.attrname = value` + - `ndb` (NotDataBase) - a shortcut property to the Non-peristent AttributeHandler. Allows `obj.ndb.attrname = value` -Each of the typeclassed entities then extend this list with their own properties. Go to the -respective pages for [Objects](./Objects.md), [Scripts](./Scripts.md), [Accounts](./Accounts.md) and -[Channels](./Channels.md) for more info. It's also recommended that you explore the available -entities using [Evennia's flat API](../Evennia-API.md) to explore which properties and methods they have -available. +Each of the typeclassed entities then extend this list with their own properties. Go to the respective pages for [Objects](./Objects.md), [Scripts](./Scripts.md), [Accounts](./Accounts.md) and [Channels](./Channels.md) for more info. It's also recommended that you explore the available entities using [Evennia's flat API](../Evennia-API.md) to explore which properties and methods they have available. ### Overloading hooks @@ -186,25 +160,16 @@ The way to customize typeclasses is usually to overload *hook methods* on them. ### Querying for typeclasses -Most of the time you search for objects in the database by using convenience methods like the -`caller.search()` of [Commands](./Commands.md) or the search functions like `evennia.search_objects`. +Most of the time you search for objects in the database by using convenience methods like the `caller.search()` of [Commands](./Commands.md) or the search functions like `evennia.search_objects`. -You can however also query for them directly using [Django's query -language](https://docs.djangoproject.com/en/4.1/topics/db/queries/). This makes use of a _database -manager_ that sits on all typeclasses, named `objects`. This manager holds methods that allow -database searches against that particular type of object (this is the way Django normally works -too). When using Django queries, you need to use the full field names (like `db_key`) to search: +You can however also query for them directly using [Django's query language](https://docs.djangoproject.com/en/4.1/topics/db/queries/). This makes use of a _database manager_ that sits on all typeclasses, named `objects`. This manager holds methods that allow database searches against that particular type of object (this is the way Django normally works too). When using Django queries, you need to use the full field names (like `db_key`) to search: ```python matches = Furniture.objects.get(db_key="Chair") ``` -It is important that this will *only* find objects inheriting directly from `Furniture` in your -database. If there was a subclass of `Furniture` named `Sitables` you would not find any chairs -derived from `Sitables` with this query (this is not a Django feature but special to Evennia). To -find objects from subclasses Evennia instead makes the `get_family` and `filter_family` query -methods available: +It is important that this will *only* find objects inheriting directly from `Furniture` in your database. If there was a subclass of `Furniture` named `Sitables` you would not find any chairs derived from `Sitables` with this query (this is not a Django feature but special to Evennia). To find objects from subclasses Evennia instead makes the `get_family` and `filter_family` query methods available: ```python # search for all furnitures and subclasses of furnitures @@ -213,26 +178,18 @@ matches = Furniture.objects.filter_family(db_key__startswith="Chair") ``` -To make sure to search, say, all `Scripts` *regardless* of typeclass, you need to query from the -database model itself. So for Objects, this would be `ObjectDB` in the diagram above. Here's an -example for Scripts: +To make sure to search, say, all `Scripts` *regardless* of typeclass, you need to query from the database model itself. So for Objects, this would be `ObjectDB` in the diagram above. Here's an example for Scripts: ```python from evennia import ScriptDB matches = ScriptDB.objects.filter(db_key__contains="Combat") ``` -When querying from the database model parent you don't need to use `filter_family` or `get_family` - -you will always query all children on the database model. +When querying from the database model parent you don't need to use `filter_family` or `get_family` - you will always query all children on the database model. ### Updating existing typeclass instances -If you already have created instances of Typeclasses, you can modify the *Python code* at any time - -due to how Python inheritance works your changes will automatically be applied to all children once you have reloaded the server. - -However, database-saved data, like `db_*` fields, [Attributes](./Attributes.md), [Tags](./Tags.md) etc, are -not themselves embedded into the class and will *not* be updated automatically. This you need to -manage yourself, by searching for all relevant objects and updating or adding the data: +If you already have created instances of Typeclasses, you can modify the *Python code* at any time - due to how Python inheritance works your changes will automatically be applied to all children once you have reloaded the server. However, database-saved data, like `db_*` fields, [Attributes](./Attributes.md), [Tags](./Tags.md) etc, are not themselves embedded into the class and will *not* be updated automatically. This you need to manage yourself, by searching for all relevant objects and updating or adding the data: ```python # add a worth Attribute to all existing Furniture @@ -241,11 +198,7 @@ for obj in Furniture.objects.all(): obj.db.worth = 100 ``` -A common use case is putting all Attributes in the `at_*_creation` hook of the entity, such as -`at_object_creation` for `Objects`. This is called every time an object is created - and only then. -This is usually what you want but it does mean already existing objects won't get updated if you -change the contents of `at_object_creation` later. You can fix this in a similar way as above -(manually setting each Attribute) or with something like this: +A common use case is putting all Attributes in the `at_*_creation` hook of the entity, such as `at_object_creation` for `Objects`. This is called every time an object is created - and only then. This is usually what you want but it does mean already existing objects won't get updated if you change the contents of `at_object_creation` later. You can fix this in a similar way as above (manually setting each Attribute) or with something like this: ```python # Re-run at_object_creation only on those objects not having the new Attribute @@ -254,19 +207,14 @@ for obj in Furniture.objects.all(): obj.at_object_creation() ``` -The above examples can be run in the command prompt created by `evennia shell`. You could also run -it all in-game using `@py`. That however requires you to put the code (including imports) as one -single line using `;` and [list -comprehensions](http://www.secnetix.de/olli/Python/list_comprehensions.hawk), like this (ignore the -line break, that's only for readability in the wiki): +The above examples can be run in the command prompt created by `evennia shell`. You could also run it all in-game using `@py`. That however requires you to put the code (including imports) as one single line using `;` and [list comprehensions](http://www.secnetix.de/olli/Python/list_comprehensions.hawk), like this (ignore the line break, that's only for readability in the wiki): ``` py from typeclasses.furniture import Furniture; [obj.at_object_creation() for obj in Furniture.objects.all() if not obj.db.worth] ``` -It is recommended that you plan your game properly before starting to build, to avoid having to -retroactively update objects more than necessary. +It is recommended that you plan your game properly before starting to build, to avoid having to retroactively update objects more than necessary. ### Swap typeclass @@ -294,8 +242,7 @@ The arguments to this method are described [in the API docs here](github:evennia *This is considered an advanced section.* -Technically, typeclasses are [Django proxy models](https://docs.djangoproject.com/en/4.1/topics/db/models/#proxy-models). The only database -models that are "real" in the typeclass system (that is, are represented by actual tables in the database) are `AccountDB`, `ObjectDB`, `ScriptDB` and `ChannelDB` (there are also [Attributes](./Attributes.md) and [Tags](./Tags.md) but they are not typeclasses themselves). All the subclasses of them are "proxies", extending them with Python code without actually modifying the database layout. +Technically, typeclasses are [Django proxy models](https://docs.djangoproject.com/en/4.1/topics/db/models/#proxy-models). The only database models that are "real" in the typeclass system (that is, are represented by actual tables in the database) are `AccountDB`, `ObjectDB`, `ScriptDB` and `ChannelDB` (there are also [Attributes](./Attributes.md) and [Tags](./Tags.md) but they are not typeclasses themselves). All the subclasses of them are "proxies", extending them with Python code without actually modifying the database layout. Evennia modifies Django's proxy model in various ways to allow them to work without any boiler plate (for example you don't need to set the Django "proxy" property in the model `Meta` subclass, Evennia handles this for you using metaclasses). Evennia also makes sure you can query subclasses as well as patches django to allow multiple inheritance from the same base class. @@ -303,9 +250,8 @@ Evennia modifies Django's proxy model in various ways to allow them to work with Evennia uses the *idmapper* to cache its typeclasses (Django proxy models) in memory. The idmapper allows things like on-object handlers and properties to be stored on typeclass instances and to not get lost as long as the server is running (they will only be cleared on a Server reload). Django does not work like this by default; by default every time you search for an object in the database you'll get a *different* instance of that object back and anything you stored on it that was not in the database would be lost. The bottom line is that Evennia's Typeclass instances subside in memory a lot longer than vanilla Django model instance do. -There is one caveat to consider with this, and that relates to [making your own models](New- -Models): Foreign relationships to typeclasses are cached by Django and that means that if you were to change an object in a foreign relationship via some other means than via that relationship, the object seeing the relationship may not reliably update but will still see its old cached version. Due to typeclasses staying so long in memory, stale caches of such relationships could be more -visible than common in Django. See the [closed issue #1098 and its comments](https://github.com/evennia/evennia/issues/1098) for examples and solutions. +There is one caveat to consider with this, and that relates to [making your own models](New- +Models): Foreign relationships to typeclasses are cached by Django and that means that if you were to change an object in a foreign relationship via some other means than via that relationship, the object seeing the relationship may not reliably update but will still see its old cached version. Due to typeclasses staying so long in memory, stale caches of such relationships could be more visible than common in Django. See the [closed issue #1098 and its comments](https://github.com/evennia/evennia/issues/1098) for examples and solutions. ## Will I run out of dbrefs? @@ -315,4 +261,4 @@ The answer is simply **no**. For example, the max dbref value for the default sqlite3 database is `2**64`. If you *created 10 000 new objects every second of every minute of every day of the year it would take about **60 million years** for you to run out of dbref numbers*. That's a database of 140 TeraBytes, just to store the dbrefs, no other data. -If you are still using Evennia at that point and have this concern, get back to us and we can discuss adding dbref reuse then. +If you are still using Evennia at that point and have this concern, get back to us and we can discuss adding dbref reuse then. \ No newline at end of file diff --git a/docs/source/Contribs/Contrib-Character-Creator.md b/docs/source/Contribs/Contrib-Character-Creator.md index 6c749dc5a0..1206cd843b 100644 --- a/docs/source/Contribs/Contrib-Character-Creator.md +++ b/docs/source/Contribs/Contrib-Character-Creator.md @@ -7,17 +7,17 @@ Commands for managing and initiating an in-game character-creation menu. ## Installation In your game folder `commands/default_cmdsets.py`, import and add -`ContribCmdCharCreate` to your `AccountCmdSet`. +`ContribChargenCmdSet` to your `AccountCmdSet`. Example: ```python -from evennia.contrib.rpg.character_creator.character_creator import ContribCmdCharCreate +from evennia.contrib.rpg.character_creator.character_creator import ContribChargenCmdSet class AccountCmdSet(default_cmds.AccountCmdSet): def at_cmdset_creation(self): super().at_cmdset_creation() - self.add(ContribCmdCharCreate) + self.add(ContribChargenCmdSet) ``` In your game folder `typeclasses/accounts.py`, import and inherit from `ContribChargenAccount` @@ -100,15 +100,19 @@ character creator menu, as well as supporting exiting/resuming the process. In addition, unlike the core command, it's designed for the character name to be chosen later on via the menu, so it won't parse any arguments passed to it. -### Changes to `Account.at_look` +### Changes to `Account` -The contrib version works mostly the same as core evennia, but adds an -additional check to recognize an in-progress character. If you've modified your -own `at_look` hook, it's an easy addition to make: just add this section to the +The contrib version works mostly the same as core evennia, but modifies `ooc_appearance_template` +to match the contrib's command syntax, and the `at_look` method to recognize an in-progress +character. + +If you've modified your own `at_look` hook, it's an easy change to add: just add this section to the playable character list loop. ```python + # the beginning of the loop starts here for char in characters: + # ... # contrib code starts here if char.db.chargen_step: # currently in-progress character; don't display placeholder names diff --git a/docs/source/api/evennia.utils.md b/docs/source/api/evennia.utils.md index fb5b98c44a..220de6c2e9 100644 --- a/docs/source/api/evennia.utils.md +++ b/docs/source/api/evennia.utils.md @@ -24,6 +24,7 @@ evennia.utils evennia.utils.evtable evennia.utils.funcparser evennia.utils.gametime + evennia.utils.hex_colors evennia.utils.logger evennia.utils.optionclasses evennia.utils.optionhandler diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index 049e4d7c15..c75d837407 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -9,7 +9,6 @@ Communication commands: from django.conf import settings from django.db.models import Q - from evennia.accounts import bots from evennia.accounts.models import AccountDB from evennia.comms.comms import DefaultChannel @@ -1414,14 +1413,14 @@ class CmdPage(COMMAND_DEFAULT_CLASS): # create the persistent message object target_perms = " or ".join( - [f"id({target.id})" for target in targets if target != caller] + [f"id({target.id})" for target in targets + [caller]] ) create.create_message( caller, message, receivers=targets, locks=( - f"read:id({caller.id}) or {target_perms} or perm(Admin);" + f"read:{target_perms} or perm(Admin);" f"delete:id({caller.id}) or perm(Admin);" f"edit:id({caller.id}) or perm(Admin)" ), diff --git a/evennia/comms/models.py b/evennia/comms/models.py index a37cb733bc..d323770858 100644 --- a/evennia/comms/models.py +++ b/evennia/comms/models.py @@ -22,7 +22,6 @@ necessary to easily be able to delete connections on the fly). from django.conf import settings from django.db import models from django.utils import timezone - from evennia.comms import managers from evennia.locks.lockhandler import LockHandler from evennia.typeclasses.models import TypedObject @@ -151,7 +150,7 @@ class Msg(SharedMemoryModel): db_header = models.TextField("header", null=True, blank=True) # the message body itself db_message = models.TextField("message") - # send date + # send date (note - this is in UTC. Use the .date_created property to get it in local time) db_date_created = models.DateTimeField( "date sent", editable=False, auto_now_add=True, db_index=True ) @@ -194,6 +193,11 @@ class Msg(SharedMemoryModel): def tags(self): return TagHandler(self) + @property + def date_created(self): + """Return the field in localized time based on settings.TIME_ZONE.""" + return timezone.localtime(self.db_date_created) + # Wrapper properties to easily set database fields. These are # @property decorators that allows to access these fields using # normal python operations (without having to remember to save() diff --git a/evennia/help/models.py b/evennia/help/models.py index e9bdcf9d33..8d1a1e8509 100644 --- a/evennia/help/models.py +++ b/evennia/help/models.py @@ -13,8 +13,8 @@ game world, policy info, rules and similar. from django.contrib.contenttypes.models import ContentType from django.db import models from django.urls import reverse +from django.utils import timezone from django.utils.text import slugify - from evennia.help.manager import HelpEntryManager from evennia.locks.lockhandler import LockHandler from evennia.typeclasses.models import AliasHandler, Tag, TagHandler @@ -79,7 +79,8 @@ class HelpEntry(SharedMemoryModel): help_text="tags on this object. Tags are simple string markers to " "identify, group and alias objects.", ) - # Creation date. This is not changed once the object is created. + # Creation date. This is not changed once the object is created. This is in UTC, + # use the property date_created to get it in local time. db_date_created = models.DateTimeField("creation date", editable=False, auto_now=True) # Database manager @@ -100,6 +101,11 @@ class HelpEntry(SharedMemoryModel): def aliases(self): return AliasHandler(self) + @property + def date_created(self): + """Return the field in localized time based on settings.TIME_ZONE.""" + return timezone.localtime(self.db_date_created) + class Meta: "Define Django meta options" verbose_name = "Help Entry" diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index 8f39c66227..ac33727c8e 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -26,6 +26,7 @@ these to create custom managers. """ +import evennia from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist @@ -33,32 +34,24 @@ from django.db import models from django.db.models import signals from django.db.models.base import ModelBase from django.urls import reverse +from django.utils import timezone from django.utils.encoding import smart_str from django.utils.text import slugify - -import evennia from evennia.locks.lockhandler import LockHandler from evennia.server.signals import SIGNAL_TYPED_OBJECT_POST_RENAME from evennia.typeclasses import managers -from evennia.typeclasses.attributes import ( - Attribute, - AttributeHandler, - AttributeProperty, - DbHolder, - InMemoryAttributeBackend, - ModelAttributeBackend, -) -from evennia.typeclasses.tags import ( - AliasHandler, - PermissionHandler, - Tag, - TagCategoryProperty, - TagHandler, - TagProperty, -) -from evennia.utils.idmapper.models import SharedMemoryModel, SharedMemoryModelBase +from evennia.typeclasses.attributes import (Attribute, AttributeHandler, + AttributeProperty, DbHolder, + InMemoryAttributeBackend, + ModelAttributeBackend) +from evennia.typeclasses.tags import (AliasHandler, PermissionHandler, Tag, + TagCategoryProperty, TagHandler, + TagProperty) +from evennia.utils.idmapper.models import (SharedMemoryModel, + SharedMemoryModelBase) from evennia.utils.logger import log_trace -from evennia.utils.utils import class_from_module, inherits_from, is_iter, lazy_property +from evennia.utils.utils import (class_from_module, inherits_from, is_iter, + lazy_property) __all__ = ("TypedObject",) @@ -225,7 +218,8 @@ class TypedObject(SharedMemoryModel): ), db_index=True, ) - # Creation date. This is not changed once the object is created. + # Creation date. This is not changed once the object is created. Note that this is UTC, + # use the .date_created property to get a localized version. db_date_created = models.DateTimeField("creation date", editable=False, auto_now_add=True) # Lock storage db_lock_storage = models.TextField( @@ -420,6 +414,11 @@ class TypedObject(SharedMemoryModel): self.at_rename(oldname, value) SIGNAL_TYPED_OBJECT_POST_RENAME.send(sender=self, old_key=oldname, new_key=value) + @property + def date_created(self): + """Get the localized date created, based on settings.TIME_ZONE.""" + return timezone.localtime(self.db_date_created) + # # # TypedObject main class methods and properties From 381d34522ba1ab66162d0a6a92bb7ab5e627ea87 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 15 Jun 2024 11:50:03 +0200 Subject: [PATCH 071/112] Fix many py3.12 SyntaxWarnings as per #3561 --- docs/source/api/evennia.utils.hex_colors.md | 10 ++++++++++ evennia/accounts/migrations/0001_initial.py | 2 +- .../accounts/migrations/0004_auto_20150403_2339.py | 5 ++--- .../accounts/migrations/0005_auto_20160905_0902.py | 2 +- evennia/accounts/tests.py | 14 +++++--------- evennia/comms/comms.py | 7 +++---- evennia/contrib/base_systems/awsstorage/tests.py | 2 +- evennia/utils/tests/test_funcparser.py | 11 +++++------ 8 files changed, 28 insertions(+), 25 deletions(-) create mode 100644 docs/source/api/evennia.utils.hex_colors.md diff --git a/docs/source/api/evennia.utils.hex_colors.md b/docs/source/api/evennia.utils.hex_colors.md new file mode 100644 index 0000000000..ef26cae11e --- /dev/null +++ b/docs/source/api/evennia.utils.hex_colors.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.utils.hex\_colors +================================ + +.. automodule:: evennia.utils.hex_colors + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/evennia/accounts/migrations/0001_initial.py b/evennia/accounts/migrations/0001_initial.py index 6540708fd0..1c094f23e3 100644 --- a/evennia/accounts/migrations/0001_initial.py +++ b/evennia/accounts/migrations/0001_initial.py @@ -44,7 +44,7 @@ class Migration(migrations.Migration): verbose_name="username", validators=[ django.core.validators.RegexValidator( - "^[\\w.@+-]+$", "Enter a valid username.", "invalid" + r"^[\w.@+-]+$", "Enter a valid username.", "invalid" ) ], ), diff --git a/evennia/accounts/migrations/0004_auto_20150403_2339.py b/evennia/accounts/migrations/0004_auto_20150403_2339.py index 1f10552c4f..5191eaf51b 100644 --- a/evennia/accounts/migrations/0004_auto_20150403_2339.py +++ b/evennia/accounts/migrations/0004_auto_20150403_2339.py @@ -2,9 +2,8 @@ import django.core.validators -from django.db import migrations, models - import evennia.accounts.manager +from django.db import migrations, models class Migration(migrations.Migration): @@ -47,7 +46,7 @@ class Migration(migrations.Migration): max_length=30, validators=[ django.core.validators.RegexValidator( - "^[\\w.@+-]+$", + r"^[\w.@+-]+$", "Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.", "invalid", ) diff --git a/evennia/accounts/migrations/0005_auto_20160905_0902.py b/evennia/accounts/migrations/0005_auto_20160905_0902.py index 12f5f154df..e005408f03 100644 --- a/evennia/accounts/migrations/0005_auto_20160905_0902.py +++ b/evennia/accounts/migrations/0005_auto_20160905_0902.py @@ -21,7 +21,7 @@ class Migration(migrations.Migration): unique=True, validators=[ django.core.validators.RegexValidator( - "^[\\w.@+-]+$", + r"^[\w.@+-]+$", "Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.", ) ], diff --git a/evennia/accounts/tests.py b/evennia/accounts/tests.py index f49438f69b..87bca0eea6 100644 --- a/evennia/accounts/tests.py +++ b/evennia/accounts/tests.py @@ -3,18 +3,14 @@ from random import randint from unittest import TestCase -from django.test import override_settings -from mock import MagicMock, Mock, patch - import evennia -from evennia.accounts.accounts import ( - AccountSessionHandler, - DefaultAccount, - DefaultGuest, -) +from django.test import override_settings +from evennia.accounts.accounts import (AccountSessionHandler, DefaultAccount, + DefaultGuest) from evennia.utils import create from evennia.utils.test_resources import BaseEvenniaTest from evennia.utils.utils import uses_database +from mock import MagicMock, Mock, patch class TestAccountSessionHandler(TestCase): @@ -172,7 +168,7 @@ class TestDefaultAccountAuth(BaseEvenniaTest): if not uses_database("mysql"): # TODO As of Mar 2019, mysql does not pass this test due to collation problems # that has not been possible to resolve - result, error = DefaultAccount.validate_username("¯\_(ツ)_/¯") + result, error = DefaultAccount.validate_username(r"¯\_(ツ)_/¯") self.assertFalse(result, "Validator allowed kanji in username.") # Should not allow duplicate username diff --git a/evennia/comms/comms.py b/evennia/comms/comms.py index a32b425df8..d6666a2bd4 100644 --- a/evennia/comms/comms.py +++ b/evennia/comms/comms.py @@ -5,11 +5,10 @@ Base typeclass for in-game Channels. import re +import evennia from django.contrib.contenttypes.models import ContentType from django.urls import reverse from django.utils.text import slugify - -import evennia from evennia.comms.managers import ChannelManager from evennia.comms.models import ChannelDB from evennia.typeclasses.models import TypeclassBase @@ -18,7 +17,7 @@ from evennia.utils.utils import inherits_from, make_iter class DefaultChannel(ChannelDB, metaclass=TypeclassBase): - """ + r""" This is the base class for all Channel Comms. Inherit from this to create different types of communication channels. @@ -35,7 +34,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase): in front of every channel message; use `{channelmessage}` token to insert the name of the current channel. Set to `None` if you want no prefix (or want to handle it in a hook during message generation instead. - - `channel_msg_nick_pattern`(str, default `"{alias}\\s*?|{alias}\\s+?(?P.+?)") - + - `channel_msg_nick_pattern`(str, default `"{alias}\s*?|{alias}\s+?(?P.+?)") - this is what used when a channel subscriber gets a channel nick assigned to this channel. The nickhandler uses the pattern to pick out this channel's name from user input. The `{alias}` token will get both the channel's key and any set/custom aliases diff --git a/evennia/contrib/base_systems/awsstorage/tests.py b/evennia/contrib/base_systems/awsstorage/tests.py index 887aa5d7b9..37736fcc59 100644 --- a/evennia/contrib/base_systems/awsstorage/tests.py +++ b/evennia/contrib/base_systems/awsstorage/tests.py @@ -65,7 +65,7 @@ class S3Boto3StorageTests(S3Boto3TestCase): """ Test the _clean_name when the path has a trailing slash """ - path = self.storage._clean_name("path\\to\\somewhere") + path = self.storage._clean_name(r"path\to\somewhere") self.assertEqual(path, "path/to/somewhere") def test_pickle_with_bucket(self): diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index 5ca8824186..96892283fe 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -10,11 +10,10 @@ from ast import literal_eval from unittest.mock import MagicMock, patch from django.test import TestCase, override_settings +from evennia.utils import funcparser, test_resources from parameterized import parameterized from simpleeval import simple_eval -from evennia.utils import funcparser, test_resources - def _test_callable(*args, **kwargs): kwargs.pop("funcparser", None) @@ -144,12 +143,12 @@ class TestFuncParser(TestCase): (r'Test args3 $bar(foo, bar, " too")', "Test args3 _test(foo, bar, too)"), ("Test args4 $foo('')", "Test args4 _test('')"), # ' treated as literal ('Test args4 $foo("")', "Test args4 _test()"), - ("Test args5 $foo(\(\))", "Test args5 _test(())"), - ("Test args6 $foo(\()", "Test args6 _test(()"), + (r"Test args5 $foo(\(\))", "Test args5 _test(())"), + (r"Test args6 $foo(\()", "Test args6 _test(()"), ("Test args7 $foo(())", "Test args7 _test(())"), ("Test args8 $foo())", "Test args8 _test())"), ("Test args9 $foo(=)", "Test args9 _test(=)"), - ("Test args10 $foo(\,)", "Test args10 _test(,)"), + (r"Test args10 $foo(\,)", "Test args10 _test(,)"), (r'Test args10 $foo(",")', "Test args10 _test(,)"), ("Test args11 $foo(()", "Test args11 $foo(()"), # invalid syntax ( @@ -327,7 +326,7 @@ class TestFuncParser(TestCase): """ string = "Test $foo(a) and $bar() and $rep(c) things" ret = self.parser.parse(string, escape=True) - self.assertEqual("Test \$foo(a) and \$bar() and \$rep(c) things", ret) + self.assertEqual(r"Test \$foo(a) and \$bar() and \$rep(c) things", ret) def test_parse_lit(self): """ From a2e4591ac2d1018c710830084858782f3cadbc3a Mon Sep 17 00:00:00 2001 From: Cal Date: Mon, 24 Jun 2024 14:41:51 -0600 Subject: [PATCH 072/112] specify website base template --- evennia/web/templates/website/channel_detail.html | 2 +- evennia/web/templates/website/channel_list.html | 2 +- evennia/web/templates/website/character_form.html | 2 +- evennia/web/templates/website/character_list.html | 2 +- evennia/web/templates/website/character_manage_list.html | 2 +- evennia/web/templates/website/generic_form.html | 2 +- evennia/web/templates/website/help_detail.html | 2 +- evennia/web/templates/website/help_list.html | 2 +- evennia/web/templates/website/object_confirm_delete.html | 2 +- evennia/web/templates/website/object_detail.html | 2 +- evennia/web/templates/website/object_list.html | 2 +- .../templates/website/registration/password_change_done.html | 2 +- .../templates/website/registration/password_change_form.html | 2 +- .../templates/website/registration/password_reset_complete.html | 2 +- .../templates/website/registration/password_reset_confirm.html | 2 +- .../web/templates/website/registration/password_reset_done.html | 2 +- .../web/templates/website/registration/password_reset_form.html | 2 +- evennia/web/templates/website/registration/register.html | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/evennia/web/templates/website/channel_detail.html b/evennia/web/templates/website/channel_detail.html index 4af6751b3c..bb9d305a10 100644 --- a/evennia/web/templates/website/channel_detail.html +++ b/evennia/web/templates/website/channel_detail.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} {{ view.page_title }} ({{ object }}) diff --git a/evennia/web/templates/website/channel_list.html b/evennia/web/templates/website/channel_list.html index 3fda972479..982b29dccb 100644 --- a/evennia/web/templates/website/channel_list.html +++ b/evennia/web/templates/website/channel_list.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} {{ view.page_title }} diff --git a/evennia/web/templates/website/character_form.html b/evennia/web/templates/website/character_form.html index 20ad71e261..78211597b4 100644 --- a/evennia/web/templates/website/character_form.html +++ b/evennia/web/templates/website/character_form.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} {{ view.page_title }} diff --git a/evennia/web/templates/website/character_list.html b/evennia/web/templates/website/character_list.html index 4e7601d49a..61e2f96f6d 100644 --- a/evennia/web/templates/website/character_list.html +++ b/evennia/web/templates/website/character_list.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} {{ view.page_title }} diff --git a/evennia/web/templates/website/character_manage_list.html b/evennia/web/templates/website/character_manage_list.html index c8845f7165..57117caf85 100644 --- a/evennia/web/templates/website/character_manage_list.html +++ b/evennia/web/templates/website/character_manage_list.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} {{ view.page_title }} diff --git a/evennia/web/templates/website/generic_form.html b/evennia/web/templates/website/generic_form.html index bacbfbd183..336589bc56 100644 --- a/evennia/web/templates/website/generic_form.html +++ b/evennia/web/templates/website/generic_form.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} Form diff --git a/evennia/web/templates/website/help_detail.html b/evennia/web/templates/website/help_detail.html index a5ed6045ef..606365d0dc 100644 --- a/evennia/web/templates/website/help_detail.html +++ b/evennia/web/templates/website/help_detail.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} {{ view.page_title }} ({{ object|title }}) diff --git a/evennia/web/templates/website/help_list.html b/evennia/web/templates/website/help_list.html index ec9c07b2b4..7f9999f76a 100644 --- a/evennia/web/templates/website/help_list.html +++ b/evennia/web/templates/website/help_list.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} {{ view.page_title }} diff --git a/evennia/web/templates/website/object_confirm_delete.html b/evennia/web/templates/website/object_confirm_delete.html index 1073b26d0c..f98caad088 100644 --- a/evennia/web/templates/website/object_confirm_delete.html +++ b/evennia/web/templates/website/object_confirm_delete.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} {{ view.page_title }} diff --git a/evennia/web/templates/website/object_detail.html b/evennia/web/templates/website/object_detail.html index 420ddd1f8a..42fbfc8d5f 100644 --- a/evennia/web/templates/website/object_detail.html +++ b/evennia/web/templates/website/object_detail.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} {{ view.page_title }} ({{ object }}) diff --git a/evennia/web/templates/website/object_list.html b/evennia/web/templates/website/object_list.html index 4e7601d49a..61e2f96f6d 100644 --- a/evennia/web/templates/website/object_list.html +++ b/evennia/web/templates/website/object_list.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} {{ view.page_title }} diff --git a/evennia/web/templates/website/registration/password_change_done.html b/evennia/web/templates/website/registration/password_change_done.html index 8869d8d69c..98e55e5207 100644 --- a/evennia/web/templates/website/registration/password_change_done.html +++ b/evennia/web/templates/website/registration/password_change_done.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} Password Changed diff --git a/evennia/web/templates/website/registration/password_change_form.html b/evennia/web/templates/website/registration/password_change_form.html index 1911b83cf0..806e1e06c8 100644 --- a/evennia/web/templates/website/registration/password_change_form.html +++ b/evennia/web/templates/website/registration/password_change_form.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} Password Change diff --git a/evennia/web/templates/website/registration/password_reset_complete.html b/evennia/web/templates/website/registration/password_reset_complete.html index 697b4bc4ad..73bb5f2284 100644 --- a/evennia/web/templates/website/registration/password_reset_complete.html +++ b/evennia/web/templates/website/registration/password_reset_complete.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} Forgot Password - Reset Successful diff --git a/evennia/web/templates/website/registration/password_reset_confirm.html b/evennia/web/templates/website/registration/password_reset_confirm.html index a7bdc683be..0a43e0a587 100644 --- a/evennia/web/templates/website/registration/password_reset_confirm.html +++ b/evennia/web/templates/website/registration/password_reset_confirm.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} Forgot Password - Reset diff --git a/evennia/web/templates/website/registration/password_reset_done.html b/evennia/web/templates/website/registration/password_reset_done.html index 8de85a5ba3..3741ead5b0 100644 --- a/evennia/web/templates/website/registration/password_reset_done.html +++ b/evennia/web/templates/website/registration/password_reset_done.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} Forgot Password - Reset Link Sent diff --git a/evennia/web/templates/website/registration/password_reset_form.html b/evennia/web/templates/website/registration/password_reset_form.html index eb73118856..86ed7f54d8 100644 --- a/evennia/web/templates/website/registration/password_reset_form.html +++ b/evennia/web/templates/website/registration/password_reset_form.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} Forgot Password diff --git a/evennia/web/templates/website/registration/register.html b/evennia/web/templates/website/registration/register.html index f54d75054d..ffc06710f9 100644 --- a/evennia/web/templates/website/registration/register.html +++ b/evennia/web/templates/website/registration/register.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "website/base.html" %} {% block titleblock %} Register From 544639e04e79c1b406320b647ef67aabba6fa667 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 13:58:20 +0200 Subject: [PATCH 073/112] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a358bbcdc1..84735c95f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,8 @@ template properly (InspectorCaracal) - [Fix][pull3545]: Fix fallback issue in cmdhandler for local-object cmdsets (InspectorCaracal) - [Fix][pull3554]: Fix/readd custom `ic` command to the `character_creator` contrib (InspectorCaracal) +- [Fix][pull3466]: Make sure the `website/base.html` website base is targeted + explicitly so it doesn't get overridden by same file name elsewhere in app (InspectorCaracal) - [Docs]: Doc fixes (Griatch, chiizujin, InspectorCaracal, iLPDev) [pull3470]: https://github.com/evennia/evennia/pull/3470 @@ -79,6 +81,7 @@ [pull3549]: https://github.com/evennia/evennia/pull/3549 [pull3554]: https://github.com/evennia/evennia/pull/3554 [pull3523]: https://github.com/evennia/evennia/pull/3523 +[pull3566]: https://github.com/evennia/evennia/pull/3566 [issue3522]: https://github.com/evennia/evennia/issue/3522 From 07cf42ac8fd78efe55d221223dae60020130b121 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 14:19:23 +0200 Subject: [PATCH 074/112] Better explain EvMenu Union merge behavior. Resolve #3546 --- evennia/utils/evmenu.py | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index fc6e6e5ce4..1a4838371f 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -272,28 +272,17 @@ from fnmatch import fnmatch from inspect import getfullargspec, isfunction from math import ceil +import evennia from django.conf import settings - # i18n from django.utils.translation import gettext as _ - -import evennia from evennia import CmdSet, Command from evennia.commands import cmdhandler from evennia.utils import logger from evennia.utils.ansi import strip_ansi from evennia.utils.evtable import EvColumn, EvTable -from evennia.utils.utils import ( - crop, - dedent, - inherits_from, - is_iter, - m_len, - make_iter, - mod_import, - pad, - to_str, -) +from evennia.utils.utils import (crop, dedent, inherits_from, is_iter, m_len, + make_iter, mod_import, pad, to_str) # read from protocol NAWS later? _MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH @@ -520,13 +509,15 @@ class EvMenu: startnode (str, optional): The starting node name in the menufile. cmdset_mergetype (str, optional): 'Replace' (default) means the menu commands will be exclusive - no other normal commands will - be usable while the user is in the menu. 'Union' means the - menu commands will be integrated with the existing commands - (it will merge with `merge_priority`), if so, make sure that - the menu's command names don't collide with existing commands - in an unexpected way. Also the CMD_NOMATCH and CMD_NOINPUT will - be overloaded by the menu cmdset. Other cmdser mergetypes - has little purpose for the menu. + be usable while the user is in the menu. 'Union' does merge the menu + command, but note that the only command used in EvMenu has key/alias + of NOINPUT/NOMATCH. So if you merge with 'Union' and a high `cmdset_prio` + (below), you won't replace individual normal commands as you may + expect. Instead commands will work normally and you'll only always fall + back to menu commands when no other command is found. There is no way + to partially replace normal commands with EvMenu actions - to do this, + remove the normal command from the caller's cmdset - if not found + the menu's version will kick in instead. cmdset_priority (int, optional): The merge priority for the menu command set. The default (1) is usually enough for most types of menus. From 01c045bd410c5e35f1fb70ccfb0ec400b560c38d Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 14:35:13 +0200 Subject: [PATCH 075/112] Fix unescaped color codes in ExtendedRoom desc command. Resolve #3565 --- .../contrib/grid/extended_room/extended_room.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/evennia/contrib/grid/extended_room/extended_room.py b/evennia/contrib/grid/extended_room/extended_room.py index 1cb2661d83..b91c3c1f30 100644 --- a/evennia/contrib/grid/extended_room/extended_room.py +++ b/evennia/contrib/grid/extended_room/extended_room.py @@ -47,17 +47,8 @@ from collections import deque from django.conf import settings from django.db.models import Q - -from evennia import ( - CmdSet, - DefaultRoom, - EvEditor, - FuncParser, - InterruptCommand, - default_cmds, - gametime, - utils, -) +from evennia import (CmdSet, DefaultRoom, EvEditor, FuncParser, + InterruptCommand, default_cmds, gametime, utils) from evennia.typeclasses.attributes import AttributeProperty from evennia.utils.utils import list_to_string, repeat @@ -677,7 +668,7 @@ class CmdExtendedRoomDesc(default_cmds.CmdDesc): edit - Open up a line editor for more advanced editing. del - Delete the description of an object. If another state is given, its description will be deleted. - spring|summer|autumn|winter - room description to use in respective in-game season + spring||summer||autumn||winter - room description to use in respective in-game season - room description to use with an arbitrary room state. Sets the description an object. If an object is not given, From 9ca41b5d0d850f2578d46604f2d63d63bddb7c75 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 15:58:11 +0200 Subject: [PATCH 076/112] Update game template class doc strings to be more up-to-date. Resolve #3387 --- CHANGELOG.md | 3 + evennia/accounts/accounts.py | 75 +++++-- evennia/comms/comms.py | 66 +++++- evennia/game_template/typeclasses/accounts.py | 136 +++++++----- evennia/game_template/typeclasses/channels.py | 66 +++++- .../game_template/typeclasses/characters.py | 19 +- evennia/game_template/typeclasses/exits.py | 23 +-- evennia/game_template/typeclasses/objects.py | 104 +++++++--- evennia/game_template/typeclasses/scripts.py | 13 +- evennia/objects/objects.py | 195 ++++++++++++++++-- evennia/scripts/scripts.py | 86 +++++++- 11 files changed, 628 insertions(+), 158 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84735c95f4..43f6d0ad1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,8 @@ - [Fix][pull3554]: Fix/readd custom `ic` command to the `character_creator` contrib (InspectorCaracal) - [Fix][pull3466]: Make sure the `website/base.html` website base is targeted explicitly so it doesn't get overridden by same file name elsewhere in app (InspectorCaracal) +- [fix][issue3387]: Update all game template doc strings to be more up-to-date + (Griatch) - [Docs]: Doc fixes (Griatch, chiizujin, InspectorCaracal, iLPDev) [pull3470]: https://github.com/evennia/evennia/pull/3470 @@ -83,6 +85,7 @@ [pull3523]: https://github.com/evennia/evennia/pull/3523 [pull3566]: https://github.com/evennia/evennia/pull/3566 [issue3522]: https://github.com/evennia/evennia/issue/3522 +[issue3387]: https://github.com/evennia/evennia/issue/3387 ## Evennia 4.1.1 diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 89137ff672..828dc53563 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -16,14 +16,13 @@ import time import typing from random import getrandbits +import evennia from django.conf import settings from django.contrib.auth import authenticate, password_validation from django.core.exceptions import ImproperlyConfigured, ValidationError from django.utils import timezone from django.utils.module_loading import import_string from django.utils.translation import gettext as _ - -import evennia from evennia.accounts.manager import AccountManager from evennia.accounts.models import AccountDB from evennia.commands.cmdsethandler import CmdSetHandler @@ -31,24 +30,17 @@ from evennia.comms.models import ChannelDB from evennia.objects.models import ObjectDB from evennia.scripts.scripthandler import ScriptHandler from evennia.server.models import ServerConfig -from evennia.server.signals import ( - SIGNAL_ACCOUNT_POST_CREATE, - SIGNAL_ACCOUNT_POST_LOGIN_FAIL, - SIGNAL_OBJECT_POST_PUPPET, - SIGNAL_OBJECT_POST_UNPUPPET, -) +from evennia.server.signals import (SIGNAL_ACCOUNT_POST_CREATE, + SIGNAL_ACCOUNT_POST_LOGIN_FAIL, + SIGNAL_OBJECT_POST_PUPPET, + SIGNAL_OBJECT_POST_UNPUPPET) from evennia.server.throttle import Throttle from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import class_from_module, create, logger from evennia.utils.optionhandler import OptionHandler -from evennia.utils.utils import ( - is_iter, - lazy_property, - make_iter, - to_str, - variable_from_module, -) +from evennia.utils.utils import (is_iter, lazy_property, make_iter, to_str, + variable_from_module) __all__ = ("DefaultAccount", "DefaultGuest") @@ -226,7 +218,6 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): - user (User, read-only) - django User authorization object - obj (Object) - game object controlled by account. 'character' can also be used. - - sessions (list of Sessions) - sessions connected to this account - is_superuser (bool, read-only) - if the connected user is a superuser * Handlers @@ -239,18 +230,47 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): - scripts - script-handler. Add new scripts to object with scripts.add() - cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object - nicks - nick-handler. New nicks with nicks.add(). + - sessions - session-handler. Use session.get() to see all sessions connected, if any + - options - option-handler. Defaults are taken from settings.OPTIONS_ACCOUNT_DEFAULT + - characters - handler for listing the account's playable characters - * Helper methods + * Helper methods (check autodocs for full updated listing) - msg(text=None, from_obj=None, session=None, options=None, **kwargs) - execute_cmd(raw_string) - - search(ostring, global_search=False, attribute_name=None, - use_nicks=False, location=None, - ignore_errors=False, account=False) + - search(searchdata, return_puppet=False, search_object=False, typeclass=None, + nofound_string=None, multimatch_string=None, use_nicks=True, + quiet=False, **kwargs) - is_typeclass(typeclass, exact=False) - swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) - - access(accessing_obj, access_type='read', default=False, no_superuser_bypass=False) + - access(accessing_obj, access_type='read', default=False, no_superuser_bypass=False, **kwargs) - check_permstring(permstring) + - get_cmdsets(caller, current, **kwargs) + - get_cmdset_providers() + - uses_screenreader(session=None) + - get_display_name(looker, **kwargs) + - get_extra_display_name_info(looker, **kwargs) + - disconnect_session_from_account() + - puppet_object(session, obj) + - unpuppet_object(session) + - unpuppet_all() + - get_puppet(session) + - get_all_puppets() + - is_banned(**kwargs) + - get_username_validators(validator_config=settings.AUTH_USERNAME_VALIDATORS) + - authenticate(username, password, ip="", **kwargs) + - normalize_username(username) + - validate_username(username) + - validate_password(password, account=None) + - set_password(password, **kwargs) + - get_character_slots() + - get_available_character_slots() + - create_character(*args, **kwargs) + - create(*args, **kwargs) + - delete(*args, **kwargs) + - channel_msg(message, channel, senders=None, **kwargs) + - idle_time() + - connection_time() * Hook methods @@ -261,15 +281,26 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): usually handled on the character level: - at_init() + - at_first_save() - at_access() - at_cmdset_get(**kwargs) + - at_password_change(**kwargs) - at_first_login() + - at_pre_login() - at_post_login(session=None) - - at_disconnect() + - at_failed_login(session, **kwargs) + - at_disconnect(reason=None, **kwargs) + - at_post_disconnect(**kwargs) - at_message_receive() - at_message_send() - at_server_reload() - at_server_shutdown() + - at_look(target=None, session=None, **kwargs) + - at_post_create_character(character, **kwargs) + - at_post_add_character(char) + - at_post_remove_character(char) + - at_pre_channel_msg(message, channel, senders=None, **kwargs) + - at_post_chnnel_msg(message, channel, senders=None, **kwargs) """ diff --git a/evennia/comms/comms.py b/evennia/comms/comms.py index d6666a2bd4..e901d4860d 100644 --- a/evennia/comms/comms.py +++ b/evennia/comms/comms.py @@ -50,6 +50,70 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase): the account-level `channel` command is used. If you were to rename that command you must tweak the output to something like `yourchannelcommandname {channelname} = $1`. + * Properties: + mutelist + banlist + wholist + + * Working methods: + get_log_filename() + set_log_filename(filename) + has_connection(account) - check if the given account listens to this channel + connect(account) - connect account to this channel + disconnect(account) - disconnect account from channel + access(access_obj, access_type='listen', default=False) - check the + access on this channel (default access_type is listen) + create(key, creator=None, *args, **kwargs) + delete() - delete this channel + message_transform(msg, emit=False, prefix=True, + sender_strings=None, external=False) - called by + the comm system and triggers the hooks below + msg(msgobj, header=None, senders=None, sender_strings=None, + persistent=None, online=False, emit=False, external=False) - main + send method, builds and sends a new message to channel. + tempmsg(msg, header=None, senders=None) - wrapper for sending non-persistent + messages. + distribute_message(msg, online=False) - send a message to all + connected accounts on channel, optionally sending only + to accounts that are currently online (optimized for very large sends) + mute(subscriber, **kwargs) + unmute(subscriber, **kwargs) + ban(target, **kwargs) + unban(target, **kwargs) + add_user_channel_alias(user, alias, **kwargs) + remove_user_channel_alias(user, alias, **kwargs) + + + Useful hooks: + at_channel_creation() - called once, when the channel is created + basetype_setup() + at_init() + at_first_save() + channel_prefix() - how the channel should be + prefixed when returning to user. Returns a string + format_senders(senders) - should return how to display multiple + senders to a channel + pose_transform(msg, sender_string) - should detect if the + sender is posing, and if so, modify the string + format_external(msg, senders, emit=False) - format messages sent + from outside the game, like from IRC + format_message(msg, emit=False) - format the message body before + displaying it to the user. 'emit' generally means that the + message should not be displayed with the sender's name. + channel_prefix() + + pre_join_channel(joiner) - if returning False, abort join + post_join_channel(joiner) - called right after successful join + pre_leave_channel(leaver) - if returning False, abort leave + post_leave_channel(leaver) - called right after successful leave + at_pre_msg(message, **kwargs) + at_post_msg(message, **kwargs) + web_get_admin_url() + web_get_create_url() + web_get_detail_url() + web_get_update_url() + web_get_delete_url() + """ objects = ChannelManager() @@ -856,7 +920,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase): # Used by Django Sites/Admin get_absolute_url = web_get_detail_url - # TODO Evennia 1.0+ removed hooks. Remove in 1.1. + # TODO Evennia 1.0+ removed hooks. Remove in 5.0 def message_transform(self, *args, **kwargs): raise RuntimeError( "Channel.message_transform is no longer used in 1.0+. " diff --git a/evennia/game_template/typeclasses/accounts.py b/evennia/game_template/typeclasses/accounts.py index 89d5e41295..12650e2cb8 100644 --- a/evennia/game_template/typeclasses/accounts.py +++ b/evennia/game_template/typeclasses/accounts.py @@ -27,68 +27,112 @@ from evennia.accounts.accounts import DefaultAccount, DefaultGuest class Account(DefaultAccount): """ - This class describes the actual OOC account (i.e. the user connecting - to the MUD). It does NOT have visual appearance in the game world (that - is handled by the character which is connected to this). Comm channels - are attended/joined using this object. + An Account is the actual OOC player entity. It doesn't exist in the game, + but puppets characters. - It can be useful e.g. for storing configuration options for your game, but - should generally not hold any character-related info (that's best handled - on the character level). + This is the base Typeclass for all Accounts. Accounts represent + the person playing the game and tracks account info, password + etc. They are OOC entities without presence in-game. An Account + can connect to a Character Object in order to "enter" the + game. - Can be set using BASE_ACCOUNT_TYPECLASS. + Account Typeclass API: + * Available properties (only available on initiated typeclass objects) - * available properties - - key (string) - name of account - name (string)- wrapper for user.username - aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. - dbref (int, read-only) - unique #id-number. Also "id" can be used. - date_created (string) - time stamp of object creation - permissions (list of strings) - list of permission strings - - user (User, read-only) - django User authorization object - obj (Object) - game object controlled by account. 'character' can also be used. - sessions (list of Sessions) - sessions connected to this account - is_superuser (bool, read-only) - if the connected user is a superuser + - key (string) - name of account + - name (string)- wrapper for user.username + - aliases (list of strings) - aliases to the object. Will be saved to + database as AliasDB entries but returned as strings. + - dbref (int, read-only) - unique #id-number. Also "id" can be used. + - date_created (string) - time stamp of object creation + - permissions (list of strings) - list of permission strings + - user (User, read-only) - django User authorization object + - obj (Object) - game object controlled by account. 'character' can also + be used. + - is_superuser (bool, read-only) - if the connected user is a superuser * Handlers - locks - lock-handler: use locks.add() to add new lock strings - db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr - ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data - scripts - script-handler. Add new scripts to object with scripts.add() - cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object - nicks - nick-handler. New nicks with nicks.add(). + - locks - lock-handler: use locks.add() to add new lock strings + - db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + - ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data + - scripts - script-handler. Add new scripts to object with scripts.add() + - cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object + - nicks - nick-handler. New nicks with nicks.add(). + - sessions - session-handler. Use session.get() to see all sessions connected, if any + - options - option-handler. Defaults are taken from settings.OPTIONS_ACCOUNT_DEFAULT + - characters - handler for listing the account's playable characters - * Helper methods + * Helper methods (check autodocs for full updated listing) - msg(text=None, **kwargs) - execute_cmd(raw_string, session=None) - search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, account=False) - is_typeclass(typeclass, exact=False) - swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) - access(accessing_obj, access_type='read', default=False) - check_permstring(permstring) + - msg(text=None, from_obj=None, session=None, options=None, **kwargs) + - execute_cmd(raw_string) + - search(searchdata, return_puppet=False, search_object=False, typeclass=None, + nofound_string=None, multimatch_string=None, use_nicks=True, + quiet=False, **kwargs) + - is_typeclass(typeclass, exact=False) + - swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) + - access(accessing_obj, access_type='read', default=False, no_superuser_bypass=False, **kwargs) + - check_permstring(permstring) + - get_cmdsets(caller, current, **kwargs) + - get_cmdset_providers() + - uses_screenreader(session=None) + - get_display_name(looker, **kwargs) + - get_extra_display_name_info(looker, **kwargs) + - disconnect_session_from_account() + - puppet_object(session, obj) + - unpuppet_object(session) + - unpuppet_all() + - get_puppet(session) + - get_all_puppets() + - is_banned(**kwargs) + - get_username_validators(validator_config=settings.AUTH_USERNAME_VALIDATORS) + - authenticate(username, password, ip="", **kwargs) + - normalize_username(username) + - validate_username(username) + - validate_password(password, account=None) + - set_password(password, **kwargs) + - get_character_slots() + - get_available_character_slots() + - create_character(*args, **kwargs) + - create(*args, **kwargs) + - delete(*args, **kwargs) + - channel_msg(message, channel, senders=None, **kwargs) + - idle_time() + - connection_time() - * Hook methods (when re-implementation, remember methods need to have self as first arg) + * Hook methods basetype_setup() at_account_creation() - - note that the following hooks are also found on Objects and are + > note that the following hooks are also found on Objects and are usually handled on the character level: - at_init() - at_cmdset_get(**kwargs) - at_first_login() - at_post_login(session=None) - at_disconnect() - at_message_receive() - at_message_send() - at_server_reload() - at_server_shutdown() + - at_init() + - at_first_save() + - at_access() + - at_cmdset_get(**kwargs) + - at_password_change(**kwargs) + - at_first_login() + - at_pre_login() + - at_post_login(session=None) + - at_failed_login(session, **kwargs) + - at_disconnect(reason=None, **kwargs) + - at_post_disconnect(**kwargs) + - at_message_receive() + - at_message_send() + - at_server_reload() + - at_server_shutdown() + - at_look(target=None, session=None, **kwargs) + - at_post_create_character(character, **kwargs) + - at_post_add_character(char) + - at_post_remove_character(char) + - at_pre_channel_msg(message, channel, senders=None, **kwargs) + - at_post_chnnel_msg(message, channel, senders=None, **kwargs) """ diff --git a/evennia/game_template/typeclasses/channels.py b/evennia/game_template/typeclasses/channels.py index f16e8897dc..39cc7885c4 100644 --- a/evennia/game_template/typeclasses/channels.py +++ b/evennia/game_template/typeclasses/channels.py @@ -16,14 +16,53 @@ from evennia.comms.comms import DefaultChannel class Channel(DefaultChannel): - """ - Working methods: - at_channel_creation() - called once, when the channel is created + r""" + This is the base class for all Channel Comms. Inherit from this to + create different types of communication channels. + + Class-level variables: + - `send_to_online_only` (bool, default True) - if set, will only try to + send to subscribers that are actually active. This is a useful optimization. + - `log_file` (str, default `"channel_{channelname}.log"`). This is the + log file to which the channel history will be saved. The `{channelname}` tag + will be replaced by the key of the Channel. If an Attribute 'log_file' + is set, this will be used instead. If this is None and no Attribute is found, + no history will be saved. + - `channel_prefix_string` (str, default `"[{channelname} ]"`) - this is used + as a simple template to get the channel prefix with `.channel_prefix()`. It is used + in front of every channel message; use `{channelmessage}` token to insert the + name of the current channel. Set to `None` if you want no prefix (or want to + handle it in a hook during message generation instead. + - `channel_msg_nick_pattern`(str, default `"{alias}\s*?|{alias}\s+?(?P.+?)") - + this is what used when a channel subscriber gets a channel nick assigned to this + channel. The nickhandler uses the pattern to pick out this channel's name from user + input. The `{alias}` token will get both the channel's key and any set/custom aliases + per subscriber. You need to allow for an `` regex group to catch any message + that should be send to the channel. You usually don't need to change this pattern + unless you are changing channel command-style entirely. + - `channel_msg_nick_replacement` (str, default `"channel {channelname} = $1"` - this + is used by the nickhandler to generate a replacement string once the nickhandler (using + the `channel_msg_nick_pattern`) identifies that the channel should be addressed + to send a message to it. The `` regex pattern match from `channel_msg_nick_pattern` + will end up at the `$1` position in the replacement. Together, this allows you do e.g. + 'public Hello' and have that become a mapping to `channel public = Hello`. By default, + the account-level `channel` command is used. If you were to rename that command you must + tweak the output to something like `yourchannelcommandname {channelname} = $1`. + + * Properties: + mutelist + banlist + wholist + + * Working methods: + get_log_filename() + set_log_filename(filename) has_connection(account) - check if the given account listens to this channel connect(account) - connect account to this channel disconnect(account) - disconnect account from channel access(access_obj, access_type='listen', default=False) - check the access on this channel (default access_type is listen) + create(key, creator=None, *args, **kwargs) delete() - delete this channel message_transform(msg, emit=False, prefix=True, sender_strings=None, external=False) - called by @@ -36,8 +75,19 @@ class Channel(DefaultChannel): distribute_message(msg, online=False) - send a message to all connected accounts on channel, optionally sending only to accounts that are currently online (optimized for very large sends) + mute(subscriber, **kwargs) + unmute(subscriber, **kwargs) + ban(target, **kwargs) + unban(target, **kwargs) + add_user_channel_alias(user, alias, **kwargs) + remove_user_channel_alias(user, alias, **kwargs) + Useful hooks: + at_channel_creation() - called once, when the channel is created + basetype_setup() + at_init() + at_first_save() channel_prefix() - how the channel should be prefixed when returning to user. Returns a string format_senders(senders) - should return how to display multiple @@ -49,13 +99,19 @@ class Channel(DefaultChannel): format_message(msg, emit=False) - format the message body before displaying it to the user. 'emit' generally means that the message should not be displayed with the sender's name. + channel_prefix() pre_join_channel(joiner) - if returning False, abort join post_join_channel(joiner) - called right after successful join pre_leave_channel(leaver) - if returning False, abort leave post_leave_channel(leaver) - called right after successful leave - pre_send_message(msg) - runs just before a message is sent to channel - post_send_message(msg) - called just after message was sent to channel + at_pre_msg(message, **kwargs) + at_post_msg(message, **kwargs) + web_get_admin_url() + web_get_create_url() + web_get_detail_url() + web_get_update_url() + web_get_delete_url() """ diff --git a/evennia/game_template/typeclasses/characters.py b/evennia/game_template/typeclasses/characters.py index b022c1f293..eeb1b2d737 100644 --- a/evennia/game_template/typeclasses/characters.py +++ b/evennia/game_template/typeclasses/characters.py @@ -15,22 +15,11 @@ from .objects import ObjectParent class Character(ObjectParent, DefaultCharacter): """ - The Character defaults to reimplementing some of base Object's hook methods with the - following functionality: + The Character just re-implements some of the Object's methods and hooks + to represent a Character entity in-game. - at_basetype_setup - always assigns the DefaultCmdSet to this object type - (important!)sets locks so character cannot be picked up - and its commands only be called by itself, not anyone else. - (to change things, use at_object_creation() instead). - at_post_move(source_location) - Launches the "look" command after every move. - at_post_unpuppet(account) - when Account disconnects from the Character, we - store the current location in the prelogout_location Attribute and - move it to a None-location so the "unpuppeted" character - object does not need to stay on grid. Echoes "Account has disconnected" - to the room. - at_pre_puppet - Just before Account re-connects, retrieves the character's - prelogout_location Attribute and move it back on the grid. - at_post_puppet - Echoes "AccountName has entered the game" to the room. + See mygame/typeclasses/objects.py for a list of + properties and methods available on all Object child classes like this. """ diff --git a/evennia/game_template/typeclasses/exits.py b/evennia/game_template/typeclasses/exits.py index 3a53753c2e..1b1c00561b 100644 --- a/evennia/game_template/typeclasses/exits.py +++ b/evennia/game_template/typeclasses/exits.py @@ -15,27 +15,12 @@ from .objects import ObjectParent class Exit(ObjectParent, DefaultExit): """ Exits are connectors between rooms. Exits are normal Objects except - they defines the `destination` property. It also does work in the - following methods: + they defines the `destination` property and overrides some hooks + and methods to represent the exits. - basetype_setup() - sets default exit locks (to change, use `at_object_creation` instead). - at_cmdset_get(**kwargs) - this is called when the cmdset is accessed and should - rebuild the Exit cmdset along with a command matching the name - of the Exit object. Conventionally, a kwarg `force_init` - should force a rebuild of the cmdset, this is triggered - by the `@alias` command when aliases are changed. - at_failed_traverse() - gives a default error message ("You cannot - go there") if exit traversal fails and an - attribute `err_traverse` is not defined. + See mygame/typeclasses/objects.py for a list of + properties and methods available on all Objects child classes like this. - Relevant hooks to overload (compared to other types of Objects): - at_traverse(traveller, target_loc) - called to do the actual traversal and calling of the other hooks. - If overloading this, consider using super() to use the default - movement implementation (and hook-calling). - at_post_traverse(traveller, source_loc) - called by at_traverse just after traversing. - at_failed_traverse(traveller) - called by at_traverse if traversal failed for some reason. Will - not be called if the attribute `err_traverse` is - defined, in which case that will simply be echoed. """ pass diff --git a/evennia/game_template/typeclasses/objects.py b/evennia/game_template/typeclasses/objects.py index 11b7363505..9734c2fbde 100644 --- a/evennia/game_template/typeclasses/objects.py +++ b/evennia/game_template/typeclasses/objects.py @@ -1,13 +1,10 @@ """ Object -The Object is the "naked" base class for things in the game world. +The Object is the class for general items in the game world. -Note that the default Character, Room and Exit does not inherit from -this Object, but from their respective default implementations in the -evennia library. If you want to use this class as a parent to change -the other types, you can do so by adding this as a multiple -inheritance. +Use the ObjectParent class to implement common features for *all* entities +with a location in the game world (like Characters, Rooms, Exits). """ @@ -28,20 +25,18 @@ class ObjectParent: class Object(ObjectParent, DefaultObject): """ - This is the root typeclass object, implementing an in-game Evennia - game object, such as having a location, being able to be - manipulated or looked at, etc. If you create a new typeclass, it - must always inherit from this object (or any of the other objects - in this file, since they all actually inherit from BaseObject, as - seen in src.object.objects). + This is the root Object typeclass, representing all entities that + have an actual presence in-game. DefaultObjects generally have a + location. They can also be manipulated and looked at. Game + entities you define should inherit from DefaultObject at some distance. - The BaseObject class implements several hooks tying into the game - engine. By re-implementing these hooks you can control the - system. You should never need to re-implement special Python - methods, such as __init__ and especially never __getattribute__ and - __setattr__ since these are used heavily by the typeclass system - of Evennia and messing with them might well break things for you. + It is recommended to create children of this class using the + `evennia.create_object()` function rather than to initialize the class + directly - this will both set things up and efficiently save the object + without `obj.save()` having to be called explicitly. + Note: Check the autodocs for complete class members, this may not always + be up-to date. * Base properties defined/available on all Objects @@ -58,12 +53,16 @@ class Object(ObjectParent, DefaultObject): location (Object) - current location. Is None if this is a room home (Object) - safety start-location has_account (bool, read-only)- will only return *connected* accounts - contents (list of Objects, read-only) - returns all objects inside this - object (including exits) + contents (list, read only) - returns all objects inside this object exits (list of Objects, read-only) - returns all exits from this object, if any destination (Object) - only set if this object is an exit. is_superuser (bool, read-only) - True/False if this user is a superuser + is_connected (bool, read-only) - True if this object is associated with + an Account with any connected sessions. + has_account (bool, read-only) - True is this object has an associated account. + is_superuser (bool, read-only): True if this object has an account and that + account is a superuser. * Handlers available @@ -84,18 +83,49 @@ class Object(ObjectParent, DefaultObject): * Helper methods (see src.objects.objects.py for full headers) - search(ostring, global_search=False, attribute_name=None, - use_nicks=False, location=None, ignore_errors=False, account=False) - execute_cmd(raw_string) - msg(text=None, **kwargs) - msg_contents(message, exclude=None, from_obj=None, **kwargs) + get_search_query_replacement(searchdata, **kwargs) + get_search_direct_match(searchdata, **kwargs) + get_search_candidates(searchdata, **kwargs) + get_search_result(searchdata, attribute_name=None, typeclass=None, + candidates=None, exact=False, use_dbref=None, tags=None, **kwargs) + get_stacked_result(results, **kwargs) + handle_search_results(searchdata, results, **kwargs) + search(searchdata, global_search=False, use_nicks=True, typeclass=None, + location=None, attribute_name=None, quiet=False, exact=False, + candidates=None, use_locks=True, nofound_string=None, + multimatch_string=None, use_dbref=None, tags=None, stacked=0) + search_account(searchdata, quiet=False) + execute_cmd(raw_string, session=None, **kwargs)) + msg(text=None, from_obj=None, session=None, options=None, **kwargs) + for_contents(func, exclude=None, **kwargs) + msg_contents(message, exclude=None, from_obj=None, mapping=None, + raise_funcparse_errors=False, **kwargs) move_to(destination, quiet=False, emit_to_obj=None, use_destination=True) + clear_contents() + create(key, account, caller, method, **kwargs) copy(new_key=None) + at_object_post_copy(new_obj, **kwargs) delete() is_typeclass(typeclass, exact=False) swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) - access(accessing_obj, access_type='read', default=False) + access(accessing_obj, access_type='read', default=False, + no_superuser_bypass=False, **kwargs) + filter_visible(obj_list, looker, **kwargs) + get_default_lockstring() + get_cmdsets(caller, current, **kwargs) check_permstring(permstring) + get_cmdset_providers() + get_display_name(looker=None, **kwargs) + get_extra_display_name_info(looker=None, **kwargs) + get_numbered_name(count, looker, **kwargs) + get_display_header(looker, **kwargs) + get_display_desc(looker, **kwargs) + get_display_exits(looker, **kwargs) + get_display_characters(looker, **kwargs) + get_display_things(looker, **kwargs) + get_display_footer(looker, **kwargs) + format_appearance(appearance, looker, **kwargs) + return_apperance(looker, **kwargs) * Hooks (these are class methods, so args should start with self): @@ -113,6 +143,7 @@ class Object(ObjectParent, DefaultObject): at_init() - called whenever typeclass is cached from memory, at least once every server restart/reload + at_first_save() at_cmdset_get(**kwargs) - this is called just before the command handler requests a cmdset from this object. The kwargs are not normally used unless the cmdset is created @@ -140,12 +171,16 @@ class Object(ObjectParent, DefaultObject): after move, if obj.move_to() has quiet=False at_post_move(source_location) - always called after a move has been successfully performed. + at_pre_object_leave(leaving_object, destination, **kwargs) + at_object_leave(obj, target_location, move_type="move", **kwargs) at_object_leave(obj, target_location) - called when an object leaves this object in any fashion - at_object_receive(obj, source_location) - called when this object receives + at_pre_object_receive(obj, source_location) + at_object_receive(obj, source_location, move_type="move", **kwargs) - called when this object receives another object + at_post_move(source_location, move_type="move", **kwargs) - at_traverse(traversing_object, source_loc) - (exit-objects only) + at_traverse(traversing_object, target_location, **kwargs) - (exit-objects only) handles all moving across the exit, including calling the other exit hooks. Use super() to retain the default functionality. @@ -164,11 +199,18 @@ class Object(ObjectParent, DefaultObject): command by default at_desc(looker=None) - called by 'look' whenever the appearance is requested. + at_pre_get(getter, **kwargs) at_get(getter) - called after object has been picked up. Does not stop pickup. - at_drop(dropper) - called when this object has been dropped. - at_say(speaker, message) - by default, called if an object inside this - object speaks + at_pre_give(giver, getter, **kwargs) + at_give(giver, getter, **kwargs) + at_pre_drop(dropper, **kwargs) + at_drop(dropper, **kwargs) - called when this object has been dropped. + at_pre_say(speaker, message, **kwargs) + at_say(message, msg_self=None, msg_location=None, receivers=None, msg_receivers=None, **kwargs) + + at_look(target, **kwargs) + at_desc(looker=None) """ diff --git a/evennia/game_template/typeclasses/scripts.py b/evennia/game_template/typeclasses/scripts.py index 63f3bb8e83..06e2385143 100644 --- a/evennia/game_template/typeclasses/scripts.py +++ b/evennia/game_template/typeclasses/scripts.py @@ -17,10 +17,17 @@ from evennia.scripts.scripts import DefaultScript class Script(DefaultScript): """ + This is the base TypeClass for all Scripts. Scripts describe + all entities/systems without a physical existence in the game world + that require database storage (like an economic system or + combat tracker). They + can also have a timer/ticker component. + A script type is customized by redefining some or all of its hook methods and variables. - * available properties + * available properties (check docs for full listing, this could be + outdated). key (string) - name of object name (string)- same as key @@ -52,6 +59,7 @@ class Script(DefaultScript): * Helper methods + create(key, **kwargs) start() - start script (this usually happens automatically at creation and obj.script.add() etc) stop() - stop script, and delete it @@ -81,11 +89,14 @@ class Script(DefaultScript): will delay the first call of this method by self.interval seconds. If self.interval==0, this method will never be called. + at_pause() at_stop() - Called as the script object is stopped and is about to be removed from the game, e.g. because is_valid() returned False. + at_script_delete() at_server_reload() - Called when server reloads. Can be used to save temporary variables you want should survive a reload. at_server_shutdown() - called at a full server shutdown. + at_server_start() """ diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 7e4b72bb4c..7cc4266324 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -10,11 +10,10 @@ import time import typing from collections import defaultdict +import evennia import inflect from django.conf import settings from django.utils.translation import gettext as _ - -import evennia from evennia.commands import cmdset from evennia.commands.cmdsethandler import CmdSetHandler from evennia.objects.manager import ObjectManager @@ -24,17 +23,9 @@ from evennia.server.signals import SIGNAL_EXIT_TRAVERSED from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import ansi, create, funcparser, logger, search -from evennia.utils.utils import ( - class_from_module, - compress_whitespace, - dbref, - is_iter, - iter_to_str, - lazy_property, - make_iter, - to_str, - variable_from_module, -) +from evennia.utils.utils import (class_from_module, compress_whitespace, dbref, + is_iter, iter_to_str, lazy_property, + make_iter, to_str, variable_from_module) _INFLECT = inflect.engine() _MULTISESSION_MODE = settings.MULTISESSION_MODE @@ -204,6 +195,184 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): directly - this will both set things up and efficiently save the object without `obj.save()` having to be called explicitly. + Note: Check the autodocs for complete class members, this may not always + be up-to date. + + * Base properties defined/available on all Objects + + key (string) - name of object + name (string)- same as key + dbref (int, read-only) - unique #id-number. Also "id" can be used. + date_created (string) - time stamp of object creation + + account (Account) - controlling account (if any, only set together with + sessid below) + sessid (int, read-only) - session id (if any, only set together with + account above). Use `sessions` handler to get the + Sessions directly. + location (Object) - current location. Is None if this is a room + home (Object) - safety start-location + has_account (bool, read-only)- will only return *connected* accounts + contents (list, read only) - returns all objects inside this object + exits (list of Objects, read-only) - returns all exits from this + object, if any + destination (Object) - only set if this object is an exit. + is_superuser (bool, read-only) - True/False if this user is a superuser + is_connected (bool, read-only) - True if this object is associated with + an Account with any connected sessions. + has_account (bool, read-only) - True is this object has an associated account. + is_superuser (bool, read-only): True if this object has an account and that + account is a superuser. + + * Handlers available + + aliases - alias-handler: use aliases.add/remove/get() to use. + permissions - permission-handler: use permissions.add/remove() to + add/remove new perms. + locks - lock-handler: use locks.add() to add new lock strings + scripts - script-handler. Add new scripts to object with scripts.add() + cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object + nicks - nick-handler. New nicks with nicks.add(). + sessions - sessions-handler. Get Sessions connected to this + object with sessions.get() + attributes - attribute-handler. Use attributes.add/remove/get. + db - attribute-handler: Shortcut for attribute-handler. Store/retrieve + database attributes using self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not create + a database entry when storing data + + * Helper methods (see src.objects.objects.py for full headers) + + get_search_query_replacement(searchdata, **kwargs) + get_search_direct_match(searchdata, **kwargs) + get_search_candidates(searchdata, **kwargs) + get_search_result(searchdata, attribute_name=None, typeclass=None, + candidates=None, exact=False, use_dbref=None, tags=None, **kwargs) + get_stacked_result(results, **kwargs) + handle_search_results(searchdata, results, **kwargs) + search(searchdata, global_search=False, use_nicks=True, typeclass=None, + location=None, attribute_name=None, quiet=False, exact=False, + candidates=None, use_locks=True, nofound_string=None, + multimatch_string=None, use_dbref=None, tags=None, stacked=0) + search_account(searchdata, quiet=False) + execute_cmd(raw_string, session=None, **kwargs)) + msg(text=None, from_obj=None, session=None, options=None, **kwargs) + for_contents(func, exclude=None, **kwargs) + msg_contents(message, exclude=None, from_obj=None, mapping=None, + raise_funcparse_errors=False, **kwargs) + move_to(destination, quiet=False, emit_to_obj=None, use_destination=True) + clear_contents() + create(key, account, caller, method, **kwargs) + copy(new_key=None) + at_object_post_copy(new_obj, **kwargs) + delete() + is_typeclass(typeclass, exact=False) + swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) + access(accessing_obj, access_type='read', default=False, + no_superuser_bypass=False, **kwargs) + filter_visible(obj_list, looker, **kwargs) + get_default_lockstring() + get_cmdsets(caller, current, **kwargs) + check_permstring(permstring) + get_cmdset_providers() + get_display_name(looker=None, **kwargs) + get_extra_display_name_info(looker=None, **kwargs) + get_numbered_name(count, looker, **kwargs) + get_display_header(looker, **kwargs) + get_display_desc(looker, **kwargs) + get_display_exits(looker, **kwargs) + get_display_characters(looker, **kwargs) + get_display_things(looker, **kwargs) + get_display_footer(looker, **kwargs) + format_appearance(appearance, looker, **kwargs) + return_apperance(looker, **kwargs) + + * Hooks (these are class methods, so args should start with self): + + basetype_setup() - only called once, used for behind-the-scenes + setup. Normally not modified. + basetype_posthook_setup() - customization in basetype, after the object + has been created; Normally not modified. + + at_object_creation() - only called once, when object is first created. + Object customizations go here. + at_object_delete() - called just before deleting an object. If returning + False, deletion is aborted. Note that all objects + inside a deleted object are automatically moved + to their , they don't need to be removed here. + + at_init() - called whenever typeclass is cached from memory, + at least once every server restart/reload + at_first_save() + at_cmdset_get(**kwargs) - this is called just before the command handler + requests a cmdset from this object. The kwargs are + not normally used unless the cmdset is created + dynamically (see e.g. Exits). + at_pre_puppet(account)- (account-controlled objects only) called just + before puppeting + at_post_puppet() - (account-controlled objects only) called just + after completing connection account<->object + at_pre_unpuppet() - (account-controlled objects only) called just + before un-puppeting + at_post_unpuppet(account) - (account-controlled objects only) called just + after disconnecting account<->object link + at_server_reload() - called before server is reloaded + at_server_shutdown() - called just before server is fully shut down + + at_access(result, accessing_obj, access_type) - called with the result + of a lock access check on this object. Return value + does not affect check result. + + at_pre_move(destination) - called just before moving object + to the destination. If returns False, move is cancelled. + announce_move_from(destination) - called in old location, just + before move, if obj.move_to() has quiet=False + announce_move_to(source_location) - called in new location, just + after move, if obj.move_to() has quiet=False + at_post_move(source_location) - always called after a move has + been successfully performed. + at_pre_object_leave(leaving_object, destination, **kwargs) + at_object_leave(obj, target_location, move_type="move", **kwargs) + at_object_leave(obj, target_location) - called when an object leaves + this object in any fashion + at_pre_object_receive(obj, source_location) + at_object_receive(obj, source_location, move_type="move", **kwargs) - called when this object receives + another object + at_post_move(source_location, move_type="move", **kwargs) + + at_traverse(traversing_object, target_location, **kwargs) - (exit-objects only) + handles all moving across the exit, including + calling the other exit hooks. Use super() to retain + the default functionality. + at_post_traverse(traversing_object, source_location) - (exit-objects only) + called just after a traversal has happened. + at_failed_traverse(traversing_object) - (exit-objects only) called if + traversal fails and property err_traverse is not defined. + + at_msg_receive(self, msg, from_obj=None, **kwargs) - called when a message + (via self.msg()) is sent to this obj. + If returns false, aborts send. + at_msg_send(self, msg, to_obj=None, **kwargs) - called when this objects + sends a message to someone via self.msg(). + + return_appearance(looker) - describes this object. Used by "look" + command by default + at_desc(looker=None) - called by 'look' whenever the + appearance is requested. + at_pre_get(getter, **kwargs) + at_get(getter) - called after object has been picked up. + Does not stop pickup. + at_pre_give(giver, getter, **kwargs) + at_give(giver, getter, **kwargs) + at_pre_drop(dropper, **kwargs) + at_drop(dropper, **kwargs) - called when this object has been dropped. + at_pre_say(speaker, message, **kwargs) + at_say(message, msg_self=None, msg_location=None, receivers=None, msg_receivers=None, **kwargs) + + at_look(target, **kwargs) + at_desc(looker=None) + + """ # Determines which order command sets begin to be assembled from. diff --git a/evennia/scripts/scripts.py b/evennia/scripts/scripts.py index c4417af3ad..2fb0de1408 100644 --- a/evennia/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -6,13 +6,12 @@ ability to run timers. """ from django.utils.translation import gettext as _ -from twisted.internet.defer import Deferred, maybeDeferred -from twisted.internet.task import LoopingCall - from evennia.scripts.manager import ScriptManager from evennia.scripts.models import ScriptDB from evennia.typeclasses.models import TypeclassBase from evennia.utils import create, logger +from twisted.internet.defer import Deferred, maybeDeferred +from twisted.internet.task import LoopingCall __all__ = ["DefaultScript", "DoNothing", "Store"] @@ -672,8 +671,85 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase): class DefaultScript(ScriptBase): """ This is the base TypeClass for all Scripts. Scripts describe - events, timers and states in game, they can have a time component - or describe a state that changes under certain conditions. + all entities/systems without a physical existence in the game world + that require database storage (like an economic system or + combat tracker). They + can also have a timer/ticker component. + + A script type is customized by redefining some or all of its hook + methods and variables. + + * available properties (check docs for full listing, this could be + outdated). + + key (string) - name of object + name (string)- same as key + aliases (list of strings) - aliases to the object. Will be saved + to database as AliasDB entries but returned as strings. + dbref (int, read-only) - unique #id-number. Also "id" can be used. + date_created (string) - time stamp of object creation + permissions (list of strings) - list of permission strings + + desc (string) - optional description of script, shown in listings + obj (Object) - optional object that this script is connected to + and acts on (set automatically by obj.scripts.add()) + interval (int) - how often script should run, in seconds. <0 turns + off ticker + start_delay (bool) - if the script should start repeating right away or + wait self.interval seconds + repeats (int) - how many times the script should repeat before + stopping. 0 means infinite repeats + persistent (bool) - if script should survive a server shutdown or not + is_active (bool) - if script is currently running + + * Handlers + + locks - lock-handler: use locks.add() to add new lock strings + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data + + * Helper methods + + create(key, **kwargs) + start() - start script (this usually happens automatically at creation + and obj.script.add() etc) + stop() - stop script, and delete it + pause() - put the script on hold, until unpause() is called. If script + is persistent, the pause state will survive a shutdown. + unpause() - restart a previously paused script. The script will continue + from the paused timer (but at_start() will be called). + time_until_next_repeat() - if a timed script (interval>0), returns time + until next tick + + * Hook methods (should also include self as the first argument): + + at_script_creation() - called only once, when an object of this + class is first created. + is_valid() - is called to check if the script is valid to be running + at the current time. If is_valid() returns False, the running + script is stopped and removed from the game. You can use this + to check state changes (i.e. an script tracking some combat + stats at regular intervals is only valid to run while there is + actual combat going on). + at_start() - Called every time the script is started, which for persistent + scripts is at least once every server start. Note that this is + unaffected by self.delay_start, which only delays the first + call to at_repeat(). + at_repeat() - Called every self.interval seconds. It will be called + immediately upon launch unless self.delay_start is True, which + will delay the first call of this method by self.interval + seconds. If self.interval==0, this method will never + be called. + at_pause() + at_stop() - Called as the script object is stopped and is about to be + removed from the game, e.g. because is_valid() returned False. + at_script_delete() + at_server_reload() - Called when server reloads. Can be used to + save temporary variables you want should survive a reload. + at_server_shutdown() - called at a full server shutdown. + at_server_start() """ From 9c3ba936e40783c4fe727e1754923192a72140b0 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 16:01:09 +0200 Subject: [PATCH 077/112] Run black on sources --- docs/source/Coding/Changelog.md | 6 ++++ evennia/accounts/accounts.py | 22 +++++++++---- .../migrations/0004_auto_20150403_2339.py | 3 +- evennia/accounts/tests.py | 12 ++++--- evennia/commands/default/comms.py | 5 ++- evennia/comms/comms.py | 3 +- evennia/comms/models.py | 1 + .../base_systems/components/component.py | 5 +-- .../contrib/base_systems/components/tests.py | 22 ++++++------- .../game_systems/achievements/__init__.py | 10 +++--- .../game_systems/achievements/achievements.py | 20 ++++++++---- .../game_systems/achievements/tests.py | 4 ++- .../grid/extended_room/extended_room.py | 13 ++++++-- .../character_creator/character_creator.py | 4 +-- evennia/help/models.py | 1 + evennia/objects/objects.py | 17 +++++++--- evennia/scripts/scripts.py | 5 +-- evennia/server/portal/telnet.py | 4 +-- evennia/server/portal/ttype.py | 22 ++++++------- evennia/typeclasses/models.py | 32 ++++++++++++------- evennia/utils/ansi.py | 2 +- evennia/utils/eveditor.py | 2 +- evennia/utils/evmenu.py | 17 ++++++++-- evennia/utils/tests/test_funcparser.py | 3 +- evennia/utils/tests/test_text2html.py | 4 +-- evennia/utils/tests/test_truecolor.py | 3 +- evennia/utils/text2html.py | 1 - 27 files changed, 153 insertions(+), 90 deletions(-) diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index a358bbcdc1..43f6d0ad1d 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -48,6 +48,10 @@ template properly (InspectorCaracal) - [Fix][pull3545]: Fix fallback issue in cmdhandler for local-object cmdsets (InspectorCaracal) - [Fix][pull3554]: Fix/readd custom `ic` command to the `character_creator` contrib (InspectorCaracal) +- [Fix][pull3466]: Make sure the `website/base.html` website base is targeted + explicitly so it doesn't get overridden by same file name elsewhere in app (InspectorCaracal) +- [fix][issue3387]: Update all game template doc strings to be more up-to-date + (Griatch) - [Docs]: Doc fixes (Griatch, chiizujin, InspectorCaracal, iLPDev) [pull3470]: https://github.com/evennia/evennia/pull/3470 @@ -79,7 +83,9 @@ [pull3549]: https://github.com/evennia/evennia/pull/3549 [pull3554]: https://github.com/evennia/evennia/pull/3554 [pull3523]: https://github.com/evennia/evennia/pull/3523 +[pull3566]: https://github.com/evennia/evennia/pull/3566 [issue3522]: https://github.com/evennia/evennia/issue/3522 +[issue3387]: https://github.com/evennia/evennia/issue/3387 ## Evennia 4.1.1 diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 828dc53563..e92c245fb6 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -16,13 +16,14 @@ import time import typing from random import getrandbits -import evennia from django.conf import settings from django.contrib.auth import authenticate, password_validation from django.core.exceptions import ImproperlyConfigured, ValidationError from django.utils import timezone from django.utils.module_loading import import_string from django.utils.translation import gettext as _ + +import evennia from evennia.accounts.manager import AccountManager from evennia.accounts.models import AccountDB from evennia.commands.cmdsethandler import CmdSetHandler @@ -30,17 +31,24 @@ from evennia.comms.models import ChannelDB from evennia.objects.models import ObjectDB from evennia.scripts.scripthandler import ScriptHandler from evennia.server.models import ServerConfig -from evennia.server.signals import (SIGNAL_ACCOUNT_POST_CREATE, - SIGNAL_ACCOUNT_POST_LOGIN_FAIL, - SIGNAL_OBJECT_POST_PUPPET, - SIGNAL_OBJECT_POST_UNPUPPET) +from evennia.server.signals import ( + SIGNAL_ACCOUNT_POST_CREATE, + SIGNAL_ACCOUNT_POST_LOGIN_FAIL, + SIGNAL_OBJECT_POST_PUPPET, + SIGNAL_OBJECT_POST_UNPUPPET, +) from evennia.server.throttle import Throttle from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import class_from_module, create, logger from evennia.utils.optionhandler import OptionHandler -from evennia.utils.utils import (is_iter, lazy_property, make_iter, to_str, - variable_from_module) +from evennia.utils.utils import ( + is_iter, + lazy_property, + make_iter, + to_str, + variable_from_module, +) __all__ = ("DefaultAccount", "DefaultGuest") diff --git a/evennia/accounts/migrations/0004_auto_20150403_2339.py b/evennia/accounts/migrations/0004_auto_20150403_2339.py index 5191eaf51b..8b8e642c5e 100644 --- a/evennia/accounts/migrations/0004_auto_20150403_2339.py +++ b/evennia/accounts/migrations/0004_auto_20150403_2339.py @@ -2,9 +2,10 @@ import django.core.validators -import evennia.accounts.manager from django.db import migrations, models +import evennia.accounts.manager + class Migration(migrations.Migration): diff --git a/evennia/accounts/tests.py b/evennia/accounts/tests.py index 87bca0eea6..d6268b3162 100644 --- a/evennia/accounts/tests.py +++ b/evennia/accounts/tests.py @@ -3,14 +3,18 @@ from random import randint from unittest import TestCase -import evennia from django.test import override_settings -from evennia.accounts.accounts import (AccountSessionHandler, DefaultAccount, - DefaultGuest) +from mock import MagicMock, Mock, patch + +import evennia +from evennia.accounts.accounts import ( + AccountSessionHandler, + DefaultAccount, + DefaultGuest, +) from evennia.utils import create from evennia.utils.test_resources import BaseEvenniaTest from evennia.utils.utils import uses_database -from mock import MagicMock, Mock, patch class TestAccountSessionHandler(TestCase): diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index c75d837407..5c5df209b2 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -9,6 +9,7 @@ Communication commands: from django.conf import settings from django.db.models import Q + from evennia.accounts import bots from evennia.accounts.models import AccountDB from evennia.comms.comms import DefaultChannel @@ -1412,9 +1413,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS): message = f"{caller.key} {message.strip(':').strip()}" # create the persistent message object - target_perms = " or ".join( - [f"id({target.id})" for target in targets + [caller]] - ) + target_perms = " or ".join([f"id({target.id})" for target in targets + [caller]]) create.create_message( caller, message, diff --git a/evennia/comms/comms.py b/evennia/comms/comms.py index e901d4860d..5b1a0bc028 100644 --- a/evennia/comms/comms.py +++ b/evennia/comms/comms.py @@ -5,10 +5,11 @@ Base typeclass for in-game Channels. import re -import evennia from django.contrib.contenttypes.models import ContentType from django.urls import reverse from django.utils.text import slugify + +import evennia from evennia.comms.managers import ChannelManager from evennia.comms.models import ChannelDB from evennia.typeclasses.models import TypeclassBase diff --git a/evennia/comms/models.py b/evennia/comms/models.py index d323770858..efab6ee4d3 100644 --- a/evennia/comms/models.py +++ b/evennia/comms/models.py @@ -22,6 +22,7 @@ necessary to easily be able to delete connections on the fly). from django.conf import settings from django.db import models from django.utils import timezone + from evennia.comms import managers from evennia.locks.lockhandler import LockHandler from evennia.typeclasses.models import TypedObject diff --git a/evennia/contrib/base_systems/components/component.py b/evennia/contrib/base_systems/components/component.py index edcf21de78..2754373a3c 100644 --- a/evennia/contrib/base_systems/components/component.py +++ b/evennia/contrib/base_systems/components/component.py @@ -15,16 +15,17 @@ class BaseComponent(type): This is the metaclass for components, responsible for registering components to the listing. """ + def __new__(cls, name, parents, attrs): """ Every class that uses this metaclass will be registered as a component in the Component Listing using its name. All of them require a unique name. """ - attrs_name = attrs.get('name') + attrs_name = attrs.get("name") if attrs_name and not COMPONENT_LISTING.get(attrs_name): new_fields = {} - attrs['_fields'] = new_fields + attrs["_fields"] = new_fields for parent in parents: _parent_fields = getattr(parent, "_fields") if _parent_fields: diff --git a/evennia/contrib/base_systems/components/tests.py b/evennia/contrib/base_systems/components/tests.py index 6087a4d6f6..a40b239eb1 100644 --- a/evennia/contrib/base_systems/components/tests.py +++ b/evennia/contrib/base_systems/components/tests.py @@ -87,23 +87,23 @@ class TestComponents(EvenniaTest): def test_character_components_set_fields_properly(self): test_a_fields = self.char1.test_a._fields - self.assertIn('my_int', test_a_fields) - self.assertIn('my_list', test_a_fields) + self.assertIn("my_int", test_a_fields) + self.assertIn("my_list", test_a_fields) self.assertEqual(len(test_a_fields), 2) test_b_fields = self.char1.test_b._fields - self.assertIn('my_int', test_b_fields) - self.assertIn('my_list', test_b_fields) - self.assertIn('default_tag', test_b_fields) - self.assertIn('single_tag', test_b_fields) - self.assertIn('multiple_tags', test_b_fields) - self.assertIn('default_single_tag', test_b_fields) + self.assertIn("my_int", test_b_fields) + self.assertIn("my_list", test_b_fields) + self.assertIn("default_tag", test_b_fields) + self.assertIn("single_tag", test_b_fields) + self.assertIn("multiple_tags", test_b_fields) + self.assertIn("default_single_tag", test_b_fields) self.assertEqual(len(test_b_fields), 6) test_ic_a_fields = self.char1.ic_a._fields - self.assertIn('my_int', test_ic_a_fields) - self.assertIn('my_list', test_ic_a_fields) - self.assertIn('my_other_int', test_ic_a_fields) + self.assertIn("my_int", test_ic_a_fields) + self.assertIn("my_list", test_ic_a_fields) + self.assertIn("my_other_int", test_ic_a_fields) self.assertEqual(len(test_ic_a_fields), 3) def test_inherited_typeclass_does_not_include_child_class_components(self): diff --git a/evennia/contrib/game_systems/achievements/__init__.py b/evennia/contrib/game_systems/achievements/__init__.py index 15d57e7025..76553e0b47 100644 --- a/evennia/contrib/game_systems/achievements/__init__.py +++ b/evennia/contrib/game_systems/achievements/__init__.py @@ -1,8 +1,8 @@ from .achievements import ( - get_achievement, - search_achievement, - all_achievements, - track_achievements, - get_achievement_progress, CmdAchieve, + all_achievements, + get_achievement, + get_achievement_progress, + search_achievement, + track_achievements, ) diff --git a/evennia/contrib/game_systems/achievements/achievements.py b/evennia/contrib/game_systems/achievements/achievements.py index f0cd189e12..82eed2a9e5 100644 --- a/evennia/contrib/game_systems/achievements/achievements.py +++ b/evennia/contrib/game_systems/achievements/achievements.py @@ -50,11 +50,18 @@ Example: """ from collections import Counter + from django.conf import settings -from evennia.utils import logger -from evennia.utils.utils import all_from_module, is_iter, make_iter, string_partial_matching -from evennia.utils.evmore import EvMore + from evennia.commands.default.muxcommand import MuxCommand +from evennia.utils import logger +from evennia.utils.evmore import EvMore +from evennia.utils.utils import ( + all_from_module, + is_iter, + make_iter, + string_partial_matching, +) # this is either a string of the attribute name, or a tuple of strings of the attribute name and category _ACHIEVEMENT_ATTR = make_iter(getattr(settings, "ACHIEVEMENT_CONTRIB_ATTRIBUTE", "achievements")) @@ -322,12 +329,12 @@ class CmdAchieve(MuxCommand): elif not achievement_data.get("progress"): status = "|yNot Started|n" else: - count = achievement_data.get("count",1) + count = achievement_data.get("count", 1) # is this achievement tracking items separately? if is_iter(achievement_data["progress"]): # we'll display progress as how many items have been completed completed = Counter(val >= count for val in achievement_data["progress"])[True] - pct = (completed * 100) // len(achievement_data['progress']) + pct = (completed * 100) // len(achievement_data["progress"]) else: # we display progress as the percent of the total count pct = (achievement_data["progress"] * 100) // count @@ -379,8 +386,7 @@ class CmdAchieve(MuxCommand): elif "all" in self.switches: # we merge our progress data into the full dict of achievements achievement_data = { - key: data | progress_data.get(key, {}) - for key, data in achievements.items() + key: data | progress_data.get(key, {}) for key, data in achievements.items() } # we show all of the currently available achievements regardless of progress status diff --git a/evennia/contrib/game_systems/achievements/tests.py b/evennia/contrib/game_systems/achievements/tests.py index b552972af3..74975fe329 100644 --- a/evennia/contrib/game_systems/achievements/tests.py +++ b/evennia/contrib/game_systems/achievements/tests.py @@ -1,5 +1,7 @@ -from evennia.utils.test_resources import BaseEvenniaTest, BaseEvenniaCommandTest from mock import patch + +from evennia.utils.test_resources import BaseEvenniaCommandTest, BaseEvenniaTest + from . import achievements _dummy_achievements = { diff --git a/evennia/contrib/grid/extended_room/extended_room.py b/evennia/contrib/grid/extended_room/extended_room.py index b91c3c1f30..a313a8fc17 100644 --- a/evennia/contrib/grid/extended_room/extended_room.py +++ b/evennia/contrib/grid/extended_room/extended_room.py @@ -47,8 +47,17 @@ from collections import deque from django.conf import settings from django.db.models import Q -from evennia import (CmdSet, DefaultRoom, EvEditor, FuncParser, - InterruptCommand, default_cmds, gametime, utils) + +from evennia import ( + CmdSet, + DefaultRoom, + EvEditor, + FuncParser, + InterruptCommand, + default_cmds, + gametime, + utils, +) from evennia.typeclasses.attributes import AttributeProperty from evennia.utils.utils import list_to_string, repeat diff --git a/evennia/contrib/rpg/character_creator/character_creator.py b/evennia/contrib/rpg/character_creator/character_creator.py index 05b88b201b..694393b75a 100644 --- a/evennia/contrib/rpg/character_creator/character_creator.py +++ b/evennia/contrib/rpg/character_creator/character_creator.py @@ -22,9 +22,9 @@ from random import choices from django.conf import settings from evennia import DefaultAccount -from evennia.commands.default.muxcommand import MuxAccountCommand -from evennia.commands.default.account import CmdIC from evennia.commands.cmdset import CmdSet +from evennia.commands.default.account import CmdIC +from evennia.commands.default.muxcommand import MuxAccountCommand from evennia.objects.models import ObjectDB from evennia.utils.evmenu import EvMenu from evennia.utils.utils import is_iter, string_partial_matching diff --git a/evennia/help/models.py b/evennia/help/models.py index 8d1a1e8509..37565256f7 100644 --- a/evennia/help/models.py +++ b/evennia/help/models.py @@ -15,6 +15,7 @@ from django.db import models from django.urls import reverse from django.utils import timezone from django.utils.text import slugify + from evennia.help.manager import HelpEntryManager from evennia.locks.lockhandler import LockHandler from evennia.typeclasses.models import AliasHandler, Tag, TagHandler diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 7cc4266324..7038ae9574 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -10,10 +10,11 @@ import time import typing from collections import defaultdict -import evennia import inflect from django.conf import settings from django.utils.translation import gettext as _ + +import evennia from evennia.commands import cmdset from evennia.commands.cmdsethandler import CmdSetHandler from evennia.objects.manager import ObjectManager @@ -23,9 +24,17 @@ from evennia.server.signals import SIGNAL_EXIT_TRAVERSED from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import ansi, create, funcparser, logger, search -from evennia.utils.utils import (class_from_module, compress_whitespace, dbref, - is_iter, iter_to_str, lazy_property, - make_iter, to_str, variable_from_module) +from evennia.utils.utils import ( + class_from_module, + compress_whitespace, + dbref, + is_iter, + iter_to_str, + lazy_property, + make_iter, + to_str, + variable_from_module, +) _INFLECT = inflect.engine() _MULTISESSION_MODE = settings.MULTISESSION_MODE diff --git a/evennia/scripts/scripts.py b/evennia/scripts/scripts.py index 2fb0de1408..64d279e414 100644 --- a/evennia/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -6,12 +6,13 @@ ability to run timers. """ from django.utils.translation import gettext as _ +from twisted.internet.defer import Deferred, maybeDeferred +from twisted.internet.task import LoopingCall + from evennia.scripts.manager import ScriptManager from evennia.scripts.models import ScriptDB from evennia.typeclasses.models import TypeclassBase from evennia.utils import create, logger -from twisted.internet.defer import Deferred, maybeDeferred -from twisted.internet.task import LoopingCall __all__ = ["DefaultScript", "DoNothing", "Store"] diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index cc0d2256a4..46d1de0baa 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -474,7 +474,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS): _RE_N.sub("", prompt) + ("||n" if prompt.endswith("|") else "|n"), strip_ansi=nocolor, xterm256=xterm256, - truecolor=truecolor + truecolor=truecolor, ) if mxp: prompt = mxp_parse(prompt) @@ -511,7 +511,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS): strip_ansi=nocolor, xterm256=xterm256, mxp=mxp, - truecolor=truecolor + truecolor=truecolor, ) if mxp: linetosend = mxp_parse(linetosend) diff --git a/evennia/server/portal/ttype.py b/evennia/server/portal/ttype.py index 58705eb12d..d1b4c738b2 100644 --- a/evennia/server/portal/ttype.py +++ b/evennia/server/portal/ttype.py @@ -130,10 +130,10 @@ class Ttype: self.protocol.protocol_flags["NOPROMPTGOAHEAD"] = False if ( - clientname.startswith("XTERM") - or clientname.endswith("-256COLOR") - or clientname - in ( + clientname.startswith("XTERM") + or clientname.endswith("-256COLOR") + or clientname + in ( "ATLANTIS", # > 0.9.9.0 (aug 2009) "CMUD", # > 3.04 (mar 2009) "KILDCLIENT", # > 2.2.0 (sep 2005) @@ -143,17 +143,13 @@ class Ttype: "BEIP", # > 2.00.206 (late 2009) (BeipMu) "POTATO", # > 2.00 (maybe earlier) "TINYFUGUE", # > 4.x (maybe earlier) - ) + ) ): xterm256 = True # use name to identify support for xterm truecolor truecolor = False - if (clientname.endswith("-TRUECOLOR") or - clientname in ( - "AXMUD", - "TINTIN" - )): + if clientname.endswith("-TRUECOLOR") or clientname in ("AXMUD", "TINTIN"): truecolor = True # all clients supporting TTYPE at all seem to support ANSI @@ -169,9 +165,9 @@ class Ttype: tupper = term.upper() # identify xterm256 based on flag xterm256 = ( - tupper.endswith("-256COLOR") - or tupper.endswith("XTERM") # Apple Terminal, old Tintin - and not tupper.endswith("-COLOR") # old Tintin, Putty + tupper.endswith("-256COLOR") + or tupper.endswith("XTERM") # Apple Terminal, old Tintin + and not tupper.endswith("-COLOR") # old Tintin, Putty ) if xterm256: self.protocol.protocol_flags["ANSI"] = True diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index ac33727c8e..476e11b045 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -26,7 +26,6 @@ these to create custom managers. """ -import evennia from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist @@ -37,21 +36,30 @@ from django.urls import reverse from django.utils import timezone from django.utils.encoding import smart_str from django.utils.text import slugify + +import evennia from evennia.locks.lockhandler import LockHandler from evennia.server.signals import SIGNAL_TYPED_OBJECT_POST_RENAME from evennia.typeclasses import managers -from evennia.typeclasses.attributes import (Attribute, AttributeHandler, - AttributeProperty, DbHolder, - InMemoryAttributeBackend, - ModelAttributeBackend) -from evennia.typeclasses.tags import (AliasHandler, PermissionHandler, Tag, - TagCategoryProperty, TagHandler, - TagProperty) -from evennia.utils.idmapper.models import (SharedMemoryModel, - SharedMemoryModelBase) +from evennia.typeclasses.attributes import ( + Attribute, + AttributeHandler, + AttributeProperty, + DbHolder, + InMemoryAttributeBackend, + ModelAttributeBackend, +) +from evennia.typeclasses.tags import ( + AliasHandler, + PermissionHandler, + Tag, + TagCategoryProperty, + TagHandler, + TagProperty, +) +from evennia.utils.idmapper.models import SharedMemoryModel, SharedMemoryModelBase from evennia.utils.logger import log_trace -from evennia.utils.utils import (class_from_module, inherits_from, is_iter, - lazy_property) +from evennia.utils.utils import class_from_module, inherits_from, is_iter, lazy_property __all__ = ("TypedObject",) diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index 68dbc0c103..a3145c21d9 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -69,8 +69,8 @@ from collections import OrderedDict from django.conf import settings from evennia.utils import logger, utils -from evennia.utils.utils import to_str from evennia.utils.hex_colors import HexColors +from evennia.utils.utils import to_str hex2truecolor = HexColors() hex_sub = HexColors.hex_sub diff --git a/evennia/utils/eveditor.py b/evennia/utils/eveditor.py index 0259faeaa0..2657d61393 100644 --- a/evennia/utils/eveditor.py +++ b/evennia/utils/eveditor.py @@ -722,7 +722,7 @@ class CmdEditorGroup(CmdEditorBase): } align_name = {"f": "Full", "c": "Center", "l": "Left", "r": "Right"} # shift width arg right if no alignment specified - if self.arg1.startswith('='): + if self.arg1.startswith("="): self.arg2 = self.arg1 self.arg1 = None if self.arg1 and self.arg1.lower() not in align_map: diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 1a4838371f..6bf4361827 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -272,17 +272,28 @@ from fnmatch import fnmatch from inspect import getfullargspec, isfunction from math import ceil -import evennia from django.conf import settings + # i18n from django.utils.translation import gettext as _ + +import evennia from evennia import CmdSet, Command from evennia.commands import cmdhandler from evennia.utils import logger from evennia.utils.ansi import strip_ansi from evennia.utils.evtable import EvColumn, EvTable -from evennia.utils.utils import (crop, dedent, inherits_from, is_iter, m_len, - make_iter, mod_import, pad, to_str) +from evennia.utils.utils import ( + crop, + dedent, + inherits_from, + is_iter, + m_len, + make_iter, + mod_import, + pad, + to_str, +) # read from protocol NAWS later? _MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index 96892283fe..7756a95999 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -10,10 +10,11 @@ from ast import literal_eval from unittest.mock import MagicMock, patch from django.test import TestCase, override_settings -from evennia.utils import funcparser, test_resources from parameterized import parameterized from simpleeval import simple_eval +from evennia.utils import funcparser, test_resources + def _test_callable(*args, **kwargs): kwargs.pop("funcparser", None) diff --git a/evennia/utils/tests/test_text2html.py b/evennia/utils/tests/test_text2html.py index 74bd61bf18..f8aec0c1d4 100644 --- a/evennia/utils/tests/test_text2html.py +++ b/evennia/utils/tests/test_text2html.py @@ -49,9 +49,7 @@ class TestText2Html(TestCase): # True Color self.assertEqual( 'redfoo', - parser.format_styles( - f'\x1b[38;2;255;0;0m' + "red" + ansi.ANSI_NORMAL + "foo" - ), + parser.format_styles(f"\x1b[38;2;255;0;0m" + "red" + ansi.ANSI_NORMAL + "foo"), ) def test_remove_bells(self): diff --git a/evennia/utils/tests/test_truecolor.py b/evennia/utils/tests/test_truecolor.py index 33fddeca16..c505cc1f86 100644 --- a/evennia/utils/tests/test_truecolor.py +++ b/evennia/utils/tests/test_truecolor.py @@ -1,6 +1,7 @@ from django.test import TestCase -from evennia.utils.ansi import ANSIString as AN, ANSIParser +from evennia.utils.ansi import ANSIParser +from evennia.utils.ansi import ANSIString as AN parser = ANSIParser().parse_ansi diff --git a/evennia/utils/text2html.py b/evennia/utils/text2html.py index bb9d642915..6b1e0057d7 100644 --- a/evennia/utils/text2html.py +++ b/evennia/utils/text2html.py @@ -12,7 +12,6 @@ import re from html import escape as html_escape from .ansi import * - from .hex_colors import HexColors # All xterm256 RGB equivalents From 3e3d39122451dd8fd996e5b987a81a5c9aadd4ec Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 17:00:51 +0200 Subject: [PATCH 078/112] Make doc index page report version/update-time and link to changelog. Resolve #3547 --- docs/pylib/update_dynamic_pages.py | 35 ++++++++++++++++++++++++++++++ docs/source/index.md | 6 ++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/pylib/update_dynamic_pages.py b/docs/pylib/update_dynamic_pages.py index bafa945f23..4e3a032d53 100644 --- a/docs/pylib/update_dynamic_pages.py +++ b/docs/pylib/update_dynamic_pages.py @@ -3,8 +3,10 @@ Update dynamically generated doc pages based on github sources. """ +from datetime import datetime from os.path import abspath, dirname from os.path import join as pathjoin +from re import sub as re_sub ROOTDIR = dirname(dirname(dirname(abspath(__file__)))) DOCDIR = pathjoin(ROOTDIR, "docs") @@ -86,12 +88,45 @@ if settings.SERVERNAME == "Evennia": print(" -- Updated Settings-Default.md") +def update_index(): + """ + Read the index.md file and inject the latest version number and updated time. + + """ + indexfile = pathjoin(DOCSRCDIR, "index.md") + versionfile = pathjoin(EVENNIADIR, "VERSION.txt") + + with open(indexfile) as f: + srcdata = f.read() + + # replace the version number + with open(versionfile) as f: + version = f.read().strip() + + pattern = r"Evennia version is \d+\.\d+\.\d+\." + replacement = f"Evennia version is {version}." + + srcdata = re_sub(pattern, replacement, srcdata) + + # replace the last-updated time + now = datetime.now().strftime("%B %d, %Y") + + pattern = r"This manual was last updated [A-Z][a-z]+ \d{1,2}, \d{4}" + replacement = f"This manual was last updated {now}" + + srcdata = re_sub(pattern, replacement, srcdata) + + with open(indexfile, "w") as f: + f.write(srcdata) + + print(" -- Updated index.md") def update_dynamic_pages(): """ Run the various updaters """ + update_index() update_changelog() update_default_settings() update_code_style() diff --git a/docs/source/index.md b/docs/source/index.md index b0beec4e1c..897a181933 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -1,8 +1,8 @@ # Evennia Documentation -This is the manual of [Evennia](https://www.evennia.com), the open source Python `MU*` creation system. Use the Search bar on the left to find or discover interesting articles. +This is the manual of [Evennia](https://www.evennia.com), the open source Python `MU*` creation system. Use the Search bar on the left to find or discover interesting articles. This manual was last updated June 27, 2024, see the [Evennia Changelog](Coding/Changelog.md). Latest released Evennia version is 4.1.1. -- [Introduction](./Evennia-Introduction.md) - what is this Evennia thing? +- [Introduction](./Evennia-Introduction.md) - what is this Evennia thing? - [Evennia in Pictures](./Evennia-In-Pictures.md) - a visual overview of Evennia - [Contributing and Getting help](./Contributing.md) - when you get stuck or want to chip in @@ -10,7 +10,7 @@ This is the manual of [Evennia](https://www.evennia.com), the open source Python - [Installation](Setup/Setup-Overview.md#installation-and-running) - getting started - [Running the Game](Setup/Running-Evennia.md) - how to start, stop and reload Evennia -- [Updating the Server](Setup/Updating-Evennia.md) - how to update Evennia +- [Updating the Server](Setup/Updating-Evennia.md) - how to update Evennia - [Configuration](Setup/Setup-Overview.md#configuration) - how to set up Evennia the way you like it - [Going Online](Setup/Setup-Overview.md#going-online) - bringing your game online From d6de25f4ca774ec30d2b43bf8311b817b3e90013 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 17:09:45 +0200 Subject: [PATCH 079/112] Include multimatch template instructions in rpsystem installation instructions. Resolve #3551 --- evennia/contrib/rpg/rpsystem/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/rpg/rpsystem/README.md b/evennia/contrib/rpg/rpsystem/README.md index 8fa3df8f18..0df7407863 100644 --- a/evennia/contrib/rpg/rpsystem/README.md +++ b/evennia/contrib/rpg/rpsystem/README.md @@ -73,8 +73,14 @@ class Room(ContribRPRoom): # ... ``` +You need to set up Evennia to use the RPsystem's form to separate +between sdescs (`3-tall`) to make it compatible with how the rest of Evennia +separates between other multi-matches of searches/commands: -You will then need to reload the server and potentially force-reload + SEARCH_MULTIMATCH_REGEX = r"(?P[0-9]+)-(?P[^-]*)(?P.*)" + SEARCH_MULTIMATCH_TEMPLATE = " {number}-{name}{aliases}{info}\n" + +Finally, you will then need to reload the server and potentially force-reload your objects, if you originally created them without this. Example for your character: From a49b61a509074d3388b0fd0cee292b4b0f361659 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 17:22:28 +0200 Subject: [PATCH 080/112] Fix look of default command.py example in search beginner tutorial. Resolve #3555 --- .../Part1/Beginner-Tutorial-Adding-Commands.md | 5 ++--- .../Part1/Beginner-Tutorial-Searching-Things.md | 7 +++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md index fba93441f7..e2ed6f805a 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md @@ -3,8 +3,7 @@ In this lesson we'll learn how to create our own Evennia [Commands](../../../Components/Commands.md) If you are new to Python you'll also learn some more basics about how to manipulate strings and get information out of Evennia. A Command is something that handles the input from a user and causes a result to happen. -An example is `look`, which examines your current location and tells you what it looks like and -what is in it. +An example is `look`, which examines your current location and tells you what it looks like and what is in it. ```{sidebar} Commands are not typeclassed @@ -20,7 +19,7 @@ Command-Sets are then associated with objects, for example with your Character. ## Creating a custom command -Open `mygame/commands/command.py`: +Open `mygame/commands/command.py`. This file already has stuff filled in for you. ```python """ diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md index 8f1a58dfe7..e08a7d1a00 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md @@ -23,12 +23,15 @@ On the `DefaultObject` is a `.search` method which we have already tried out whe - It will always return exactly one match. If it found zero or more than one match, the return is `None`. This is different from `evennia.search` (see below), which always returns a list. - On a no-match or multimatch, `.search` will automatically send an error message to `obj`. So you don't have to worry about reporting messages if the result is `None`. -In other words, this method handles error messaging for you. A very common way to use it is in commands: +In other words, this method handles error messaging for you. A very common way to use it is in commands. You can put your command anywhere, but let's try the pre-filled-in `mygame/commands/command.py`. ```python # in for example mygame/commands/command.py -from evennia import Command +from evennia import Command as BaseCommand + +class Command(BaseCommand): + # ... class CmdQuickFind(Command): """ From 468d0812d9d02db7b2316622379e6fca55df4607 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 17:27:36 +0200 Subject: [PATCH 081/112] Reword weird sentence in beginner search tutorial. Resolve #3558 --- .../Part1/Beginner-Tutorial-Searching-Things.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md index e08a7d1a00..72a9709c75 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md @@ -135,9 +135,9 @@ Above we find first the rose and then an Account. You can try both using `py`: > py evennia.search_account("YourName")[0] -In the example above we used `[0]` to only get the first match of the queryset, which in this case gives us the rose and your Account respectively. Note that if you don't find any matches, using `[0]` like this leads to an error, so it's mostly useful for debugging. +The `search_object/account` returns all matches. We use `[0]` to only get the first match of the queryset, which in this case gives us the rose and your Account respectively. Note that if you don't find any matches, using `[0]` like this leads to an error, so it's mostly useful for debugging. -If you you really want all matches to the search parameters you specify. In other situations, having zero or more than one match is a sign of a problem and you need to handle this case yourself. This is too detailed for testing out just with `py`, but good to know if you want to make your own search methods: +In other situations, having zero or more than one match is a sign of a problem and you need to handle this case yourself. This is too detailed for testing out just with `py`, but good to know if you want to make your own search methods: ```python the_one_ring = evennia.search_object("The one Ring") From 86c65b08d77e21c128045e98b9ebd6aa7b1e7c20 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 17:30:55 +0200 Subject: [PATCH 082/112] Fix default locktype index for help entries in docs. Resolve #3562 --- docs/source/Components/Locks.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/Components/Locks.md b/docs/source/Components/Locks.md index 3d07b9b583..1683997158 100644 --- a/docs/source/Components/Locks.md +++ b/docs/source/Components/Locks.md @@ -99,7 +99,8 @@ Below are the access_types checked by the default commandset. - `send` - who may send to the channel. - `listen` - who may subscribe and listen to the channel. - [HelpEntry](./Help-System.md): - - `examine` - who may view this help entry (usually everyone) + - `view` - if the help entry header should show up in the help index + - `read` - who may view this help entry (usually everyone) - `edit` - who may edit this help entry. So to take an example, whenever an exit is to be traversed, a lock of the type *traverse* will be checked. Defining a suitable lock type for an exit object would thus involve a lockstring `traverse: `. From 094478c33736f3700b675290f4b0a1032dc91c77 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 18:04:49 +0200 Subject: [PATCH 083/112] Testing github alternative action for postgres --- .../workflows/github_action_test_suite.yml | 19 +++++++++++++++++++ .github/workflows/postgresql_settings.py | 9 ++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index c05faa95a5..34f1ea9ad7 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -32,10 +32,26 @@ jobs: UNIT_TEST_SETTINGS: "--settings=settings --keepdb --timing" COVERAGE_TEST_SETTINGS: "--settings=settings --timing" + services: + postgres: + image: postgres:12 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: evennia + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5s + ports: + - 5432:5432 + steps: - uses: actions/checkout@v3 - name: Set up database (${{ matrix.TESTING_DB }}) + if: matrix.TESTING_DB != 'postgresql' uses: ./.github/actions/setup-database with: database: ${{ matrix.TESTING_DB }} @@ -74,6 +90,9 @@ jobs: working-directory: testing_mygame run: | evennia test ${{ env.UNIT_TEST_SETTINGS }} evennia + env: + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 # OBS - it's important to not run the coverage tests with --parallel, it messes up the coverage # calculation! diff --git a/.github/workflows/postgresql_settings.py b/.github/workflows/postgresql_settings.py index c12927af3a..9f5e927ec3 100644 --- a/.github/workflows/postgresql_settings.py +++ b/.github/workflows/postgresql_settings.py @@ -42,11 +42,10 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "evennia", - "USER": "evennia", - "PASSWORD": "password", - "HOST": "localhost", - "PORT": "", # use default - "TEST": {"NAME": "default"}, + "USER": "postgresql", # evennia + "PASSWORD": "postgres", + "HOST": os.environ.get("POSTGRES_HOST", "localhost"), + "PORT": os.environ.get("POSTGRES_PORT", "5432"), } } From 9c7fcac0d62ea7cc0cc1174af82636b6f52d557d Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 18:07:32 +0200 Subject: [PATCH 084/112] Fix in action --- .github/workflows/github_action_test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index 34f1ea9ad7..d8b93afb2a 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -43,7 +43,7 @@ jobs: --health-cmd pg_isready --health-interval 10s --health-timeout 5s - --health-retries 5s + --health-retries 5 ports: - 5432:5432 From fba1359d914763ac1e3ea3ce0940a6b9275ac674 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 18:10:21 +0200 Subject: [PATCH 085/112] Another fix of postgres auth --- .github/workflows/postgresql_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/postgresql_settings.py b/.github/workflows/postgresql_settings.py index 9f5e927ec3..2aaa453fe5 100644 --- a/.github/workflows/postgresql_settings.py +++ b/.github/workflows/postgresql_settings.py @@ -42,7 +42,7 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "evennia", - "USER": "postgresql", # evennia + "USER": "postgres", # evennia "PASSWORD": "postgres", "HOST": os.environ.get("POSTGRES_HOST", "localhost"), "PORT": os.environ.get("POSTGRES_PORT", "5432"), From 50417d89706c0c958a9dad4a3f0fd9c792032030 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 18:30:03 +0200 Subject: [PATCH 086/112] Testing to clean up mysql action too --- .../workflows/github_action_test_suite.yml | 34 ++++++++++++++----- .github/workflows/mysql_settings.py | 28 +++++++-------- .github/workflows/postgresql_settings.py | 4 +-- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index d8b93afb2a..80beeb827e 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -36,9 +36,9 @@ jobs: postgres: image: postgres:12 env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres + POSTGRES_USER: evennia POSTGRES_DB: evennia + POSTGRES_PASSWORD: evennia options: >- --health-cmd pg_isready --health-interval 10s @@ -47,15 +47,31 @@ jobs: ports: - 5432:5432 + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root_password + MYSQL_DATABASE: evennia + MYSQL_USER: evennia + MYSQL_PASSWORD: evennia + options: >- + --health-cmd "mysqladmin ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + --charset "utf8mb4" + --init_command "set collation_connection=utf8mb4_unicode_ci" + ports: + - 3306:3306 + steps: - uses: actions/checkout@v3 - - name: Set up database (${{ matrix.TESTING_DB }}) - if: matrix.TESTING_DB != 'postgresql' - uses: ./.github/actions/setup-database - with: - database: ${{ matrix.TESTING_DB }} - timeout-minutes: 5 + # - name: Set up database (${{ matrix.TESTING_DB }}) + # uses: ./.github/actions/setup-database + # with: + # database: ${{ matrix.TESTING_DB }} + # timeout-minutes: 5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -93,6 +109,8 @@ jobs: env: POSTGRES_HOST: localhost POSTGRES_PORT: 5432 + MYSQL_HOST: 127.0.0.1 + MYSQL_PORT: 3306 # OBS - it's important to not run the coverage tests with --parallel, it messes up the coverage # calculation! diff --git a/.github/workflows/mysql_settings.py b/.github/workflows/mysql_settings.py index b5525dcd74..ff9d317af8 100644 --- a/.github/workflows/mysql_settings.py +++ b/.github/workflows/mysql_settings.py @@ -43,20 +43,20 @@ DATABASES = { "ENGINE": "django.db.backends.mysql", "NAME": "evennia", "USER": "evennia", - "PASSWORD": "password", - "HOST": "127.0.0.1", - "PORT": "", # use default port - "OPTIONS": { - "charset": "utf8mb4", - "init_command": "set collation_connection=utf8mb4_unicode_ci", - }, - "TEST": { - "NAME": "evennia", - "OPTIONS": { - "charset": "utf8mb4", - "init_command": "set collation_connection=utf8mb4_unicode_ci", - }, - }, + "PASSWORD": "evennia", + "HOST": os.environ.get("MYSQL_HOST", "127.0.0.1"), + "PORT": os.environ.get("MYSQL_PORT", "3306"), +# "OPTIONS": { +# "charset": "utf8mb4", +# "init_command": "set collation_connection=utf8mb4_unicode_ci", +# }, +# "TEST": { +# "NAME": "evennia", +# "OPTIONS": { +# "charset": "utf8mb4", +# "init_command": "set collation_connection=utf8mb4_unicode_ci", +# }, +# }, } } diff --git a/.github/workflows/postgresql_settings.py b/.github/workflows/postgresql_settings.py index 2aaa453fe5..2e5ad4c4e6 100644 --- a/.github/workflows/postgresql_settings.py +++ b/.github/workflows/postgresql_settings.py @@ -42,8 +42,8 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "evennia", - "USER": "postgres", # evennia - "PASSWORD": "postgres", + "USER": "evennia", + "PASSWORD": "evennia", "HOST": os.environ.get("POSTGRES_HOST", "localhost"), "PORT": os.environ.get("POSTGRES_PORT", "5432"), } From d54886e9e4b5308598f6784d69dfbdd85ea6ad2b Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 18:47:49 +0200 Subject: [PATCH 087/112] Testing to streamline mysql setup more --- .github/workflows/github_action_test_suite.yml | 4 ++-- .github/workflows/mysql_options.cnf | 9 +++++++++ .github/workflows/mysql_settings.py | 8 ++++---- 3 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/mysql_options.cnf diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index 80beeb827e..09730d56c5 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -59,10 +59,10 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - --charset "utf8mb4" - --init_command "set collation_connection=utf8mb4_unicode_ci" ports: - 3306:3306 + volumes: + - ${{ github.workspace }}/.github/workflows/mysql_options.cnf:/etc/mysql/conf.d/custom.cnf steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/mysql_options.cnf b/.github/workflows/mysql_options.cnf new file mode 100644 index 0000000000..69175ab0da --- /dev/null +++ b/.github/workflows/mysql_options.cnf @@ -0,0 +1,9 @@ +[mysqld] +character-set-server = utf8mb4 +collation-server = utf9mb4_unicode_ci + +[mysql] +default-character-set = utf8mb4 + +[client] +default-character-set = utf8mb4 diff --git a/.github/workflows/mysql_settings.py b/.github/workflows/mysql_settings.py index ff9d317af8..975e490c1b 100644 --- a/.github/workflows/mysql_settings.py +++ b/.github/workflows/mysql_settings.py @@ -46,10 +46,10 @@ DATABASES = { "PASSWORD": "evennia", "HOST": os.environ.get("MYSQL_HOST", "127.0.0.1"), "PORT": os.environ.get("MYSQL_PORT", "3306"), -# "OPTIONS": { -# "charset": "utf8mb4", -# "init_command": "set collation_connection=utf8mb4_unicode_ci", -# }, + "OPTIONS": { + "charset": "utf8mb4", + # "init_command": "set collation_connection=utf8mb4_unicode_ci", + }, # "TEST": { # "NAME": "evennia", # "OPTIONS": { From 6b984a4faa723faa6dab48b1ae67f15f45d2980e Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 18:57:40 +0200 Subject: [PATCH 088/112] Removing mysql volume and set better perm --- .github/workflows/github_action_test_suite.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index 09730d56c5..27cfa9be9f 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -61,12 +61,16 @@ jobs: --health-retries 5 ports: - 3306:3306 - volumes: - - ${{ github.workspace }}/.github/workflows/mysql_options.cnf:/etc/mysql/conf.d/custom.cnf steps: - uses: actions/checkout@v3 + - name: Copy custom MySQL option file + if: ${{ matrix.TESTING_DB == 'mysql' }} + run: | + sudo cp ${{ github.workspace }}/.github/workflows/mysql_options.cnf /etc/mysql/custom.cnf + sudo chown -R mysql:mysql /etc/mysql/custom.cnf + # - name: Set up database (${{ matrix.TESTING_DB }}) # uses: ./.github/actions/setup-database # with: From bce5fb0521db24ae394fb5788d7812d98451e583 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 19:08:27 +0200 Subject: [PATCH 089/112] Experimenting with other encodings for mysql --- .github/workflows/github_action_test_suite.yml | 2 +- .github/workflows/mysql_options.cnf | 8 ++++---- .github/workflows/mysql_settings.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index 27cfa9be9f..39f8b82495 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -65,7 +65,7 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Copy custom MySQL option file + - name: Set MySQL custom options if: ${{ matrix.TESTING_DB == 'mysql' }} run: | sudo cp ${{ github.workspace }}/.github/workflows/mysql_options.cnf /etc/mysql/custom.cnf diff --git a/.github/workflows/mysql_options.cnf b/.github/workflows/mysql_options.cnf index 69175ab0da..7950cd69af 100644 --- a/.github/workflows/mysql_options.cnf +++ b/.github/workflows/mysql_options.cnf @@ -1,9 +1,9 @@ [mysqld] -character-set-server = utf8mb4 -collation-server = utf9mb4_unicode_ci +character-set-server = utf8 +collation-server = utf8_general_ci [mysql] -default-character-set = utf8mb4 +default-character-set = utf8 [client] -default-character-set = utf8mb4 +default-character-set = utf8 diff --git a/.github/workflows/mysql_settings.py b/.github/workflows/mysql_settings.py index 975e490c1b..16fdb74b9e 100644 --- a/.github/workflows/mysql_settings.py +++ b/.github/workflows/mysql_settings.py @@ -48,7 +48,7 @@ DATABASES = { "PORT": os.environ.get("MYSQL_PORT", "3306"), "OPTIONS": { "charset": "utf8mb4", - # "init_command": "set collation_connection=utf8mb4_unicode_ci", + "init_command": "set collation_connection=utf8mb4_unicode_ci", }, # "TEST": { # "NAME": "evennia", From 081364bb2f875bc4313f3f38858436d40239bcf4 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 19:17:13 +0200 Subject: [PATCH 090/112] More shenanigans to please mysql testing --- .github/workflows/mysql_settings.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/mysql_settings.py b/.github/workflows/mysql_settings.py index 16fdb74b9e..7ffa74c347 100644 --- a/.github/workflows/mysql_settings.py +++ b/.github/workflows/mysql_settings.py @@ -50,13 +50,13 @@ DATABASES = { "charset": "utf8mb4", "init_command": "set collation_connection=utf8mb4_unicode_ci", }, -# "TEST": { -# "NAME": "evennia", -# "OPTIONS": { -# "charset": "utf8mb4", -# "init_command": "set collation_connection=utf8mb4_unicode_ci", -# }, -# }, + "TEST": { + "NAME": "evennia", + "OPTIONS": { + "charset": "utf8mb4", + "init_command": "set collation_connection=utf8mb4_unicode_ci", + }, + }, } } From 5455979f87cf16677a9d7160267e1c85538e6185 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 19:27:17 +0200 Subject: [PATCH 091/112] Had a typo in the mysql option copying --- .github/workflows/github_action_test_suite.yml | 4 ++-- .github/workflows/mysql_settings.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index 39f8b82495..aff2006273 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -68,8 +68,8 @@ jobs: - name: Set MySQL custom options if: ${{ matrix.TESTING_DB == 'mysql' }} run: | - sudo cp ${{ github.workspace }}/.github/workflows/mysql_options.cnf /etc/mysql/custom.cnf - sudo chown -R mysql:mysql /etc/mysql/custom.cnf + sudo cp ${{ github.workspace }}/.github/workflows/mysql_options.cnf /etc/mysql/conf.d/custom.cnf + sudo chown -R mysql:mysql /etc/mysql/conf.d/custom.cnf # - name: Set up database (${{ matrix.TESTING_DB }}) # uses: ./.github/actions/setup-database diff --git a/.github/workflows/mysql_settings.py b/.github/workflows/mysql_settings.py index 7ffa74c347..7541fa04cc 100644 --- a/.github/workflows/mysql_settings.py +++ b/.github/workflows/mysql_settings.py @@ -48,15 +48,15 @@ DATABASES = { "PORT": os.environ.get("MYSQL_PORT", "3306"), "OPTIONS": { "charset": "utf8mb4", - "init_command": "set collation_connection=utf8mb4_unicode_ci", +# "init_command": "set collation_connection=utf8mb4_unicode_ci", }, - "TEST": { - "NAME": "evennia", - "OPTIONS": { - "charset": "utf8mb4", - "init_command": "set collation_connection=utf8mb4_unicode_ci", - }, - }, +# "TEST": { +# "NAME": "evennia", +# "OPTIONS": { +# "charset": "utf8mb4", +# "init_command": "set collation_connection=utf8mb4_unicode_ci", +# }, +# }, } } From 4e2fa1bde0667b3390ca92bbff71a54105cbb432 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 19:37:35 +0200 Subject: [PATCH 092/112] More mysql adjustments --- .github/workflows/mysql_settings.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/mysql_settings.py b/.github/workflows/mysql_settings.py index 7541fa04cc..7ffa74c347 100644 --- a/.github/workflows/mysql_settings.py +++ b/.github/workflows/mysql_settings.py @@ -48,15 +48,15 @@ DATABASES = { "PORT": os.environ.get("MYSQL_PORT", "3306"), "OPTIONS": { "charset": "utf8mb4", -# "init_command": "set collation_connection=utf8mb4_unicode_ci", + "init_command": "set collation_connection=utf8mb4_unicode_ci", }, -# "TEST": { -# "NAME": "evennia", -# "OPTIONS": { -# "charset": "utf8mb4", -# "init_command": "set collation_connection=utf8mb4_unicode_ci", -# }, -# }, + "TEST": { + "NAME": "evennia", + "OPTIONS": { + "charset": "utf8mb4", + "init_command": "set collation_connection=utf8mb4_unicode_ci", + }, + }, } } From 79291e315ad59e075d55a95933ec34e29168788d Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 19:45:57 +0200 Subject: [PATCH 093/112] Testing with utf8 encoding --- .github/workflows/mysql_settings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/mysql_settings.py b/.github/workflows/mysql_settings.py index 7ffa74c347..3ffc8f1b7e 100644 --- a/.github/workflows/mysql_settings.py +++ b/.github/workflows/mysql_settings.py @@ -47,14 +47,14 @@ DATABASES = { "HOST": os.environ.get("MYSQL_HOST", "127.0.0.1"), "PORT": os.environ.get("MYSQL_PORT", "3306"), "OPTIONS": { - "charset": "utf8mb4", - "init_command": "set collation_connection=utf8mb4_unicode_ci", + "charset": "utf8", #"utf8mb4", + "init_command": "set collation_connection=utf8_unicode_ci", }, "TEST": { "NAME": "evennia", "OPTIONS": { - "charset": "utf8mb4", - "init_command": "set collation_connection=utf8mb4_unicode_ci", + "charset": "utf8", # "utf8mb4", + "init_command": "set collation_connection=utf8_unicode_ci", }, }, } From d1fdae493d98b932a414a91ab6efef01b8e8cc69 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 19:53:28 +0200 Subject: [PATCH 094/112] Testing barracuda to extend mysql table sizes --- .github/workflows/mysql_options.cnf | 11 +++++++---- .github/workflows/mysql_settings.py | 8 ++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/mysql_options.cnf b/.github/workflows/mysql_options.cnf index 7950cd69af..78711ebd9b 100644 --- a/.github/workflows/mysql_options.cnf +++ b/.github/workflows/mysql_options.cnf @@ -1,9 +1,12 @@ [mysqld] -character-set-server = utf8 -collation-server = utf8_general_ci +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci +innodb_large_prefix = 1 +innodb_file_format = Barracuda +innodb_file_per_table = 1 [mysql] -default-character-set = utf8 +default-character-set = utf8mb4 [client] -default-character-set = utf8 +default-character-set = utf8mb4 diff --git a/.github/workflows/mysql_settings.py b/.github/workflows/mysql_settings.py index 3ffc8f1b7e..7ffa74c347 100644 --- a/.github/workflows/mysql_settings.py +++ b/.github/workflows/mysql_settings.py @@ -47,14 +47,14 @@ DATABASES = { "HOST": os.environ.get("MYSQL_HOST", "127.0.0.1"), "PORT": os.environ.get("MYSQL_PORT", "3306"), "OPTIONS": { - "charset": "utf8", #"utf8mb4", - "init_command": "set collation_connection=utf8_unicode_ci", + "charset": "utf8mb4", + "init_command": "set collation_connection=utf8mb4_unicode_ci", }, "TEST": { "NAME": "evennia", "OPTIONS": { - "charset": "utf8", # "utf8mb4", - "init_command": "set collation_connection=utf8_unicode_ci", + "charset": "utf8mb4", + "init_command": "set collation_connection=utf8mb4_unicode_ci", }, }, } From e292903ddac427470fa9efb9faa3a87245119698 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 20:06:57 +0200 Subject: [PATCH 095/112] Another attempt to make mysql see the config --- .github/workflows/github_action_test_suite.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index aff2006273..e615cc8d7c 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -59,17 +59,19 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + --defaults-extra-file=/etc/mysql/conf.d/custom.cnf ports: - 3306:3306 steps: - uses: actions/checkout@v3 - - name: Set MySQL custom options + - name: Reboot MySQL with custom options if: ${{ matrix.TESTING_DB == 'mysql' }} run: | sudo cp ${{ github.workspace }}/.github/workflows/mysql_options.cnf /etc/mysql/conf.d/custom.cnf sudo chown -R mysql:mysql /etc/mysql/conf.d/custom.cnf + sudo service mysql restart # - name: Set up database (${{ matrix.TESTING_DB }}) # uses: ./.github/actions/setup-database From eaec0d5d009a44a399f539a70410c2ffa27b5a32 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 20:08:32 +0200 Subject: [PATCH 096/112] Another attempt --- .github/workflows/github_action_test_suite.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index e615cc8d7c..cafb5ba6f2 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -59,7 +59,6 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - --defaults-extra-file=/etc/mysql/conf.d/custom.cnf ports: - 3306:3306 From fadbbaa4a3dea6670613cfbd29d5c59b68a505c0 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 20:13:25 +0200 Subject: [PATCH 097/112] Trying to pass options directly --- .github/workflows/github_action_test_suite.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index cafb5ba6f2..9ae02bd26e 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -59,18 +59,19 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + --character-set-server-utf8mb4 + --collation-server-utf8mb4_unicode_ci ports: - 3306:3306 steps: - uses: actions/checkout@v3 - - name: Reboot MySQL with custom options - if: ${{ matrix.TESTING_DB == 'mysql' }} - run: | - sudo cp ${{ github.workspace }}/.github/workflows/mysql_options.cnf /etc/mysql/conf.d/custom.cnf - sudo chown -R mysql:mysql /etc/mysql/conf.d/custom.cnf - sudo service mysql restart + # - name: Reboot MySQL with custom options + # if: ${{ matrix.TESTING_DB == 'mysql' }} + # run: | + # sudo cp ${{ github.workspace }}/.github/workflows/mysql_options.cnf /etc/mysql/conf.d/custom.cnf + # sudo chown -R mysql:mysql /etc/mysql/conf.d/custom.cnf # - name: Set up database (${{ matrix.TESTING_DB }}) # uses: ./.github/actions/setup-database From ea2b259c79bf6bef91f80f4cab25a50125361573 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 20:15:38 +0200 Subject: [PATCH 098/112] Yet another attempt --- .github/workflows/github_action_test_suite.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index 9ae02bd26e..ded08d764c 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -59,8 +59,8 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - --character-set-server-utf8mb4 - --collation-server-utf8mb4_unicode_ci + --character-set-server "utf8mb4" + --collation-server "utf8mb4_unicode_ci" ports: - 3306:3306 From fa99218c31aedb1b381fc8b1a9dab60aa811872a Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 20:22:25 +0200 Subject: [PATCH 099/112] Testing sending all through options --- .../workflows/github_action_test_suite.yml | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index ded08d764c..750973d0db 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -40,10 +40,10 @@ jobs: POSTGRES_DB: evennia POSTGRES_PASSWORD: evennia options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-cmd=pg_isready + --health-interval=10s + --health-timeout=5s + --health-retries=5 ports: - 5432:5432 @@ -55,12 +55,15 @@ jobs: MYSQL_USER: evennia MYSQL_PASSWORD: evennia options: >- - --health-cmd "mysqladmin ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - --character-set-server "utf8mb4" - --collation-server "utf8mb4_unicode_ci" + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + --character-set-server=utf8mb4 + --collation-server=utf8mb4_unicode_ci + --innodb-large-prefix=1 + --innodb-file-format=Barracuda + --innodb-file-per-table=1 ports: - 3306:3306 From e5eccb046cc5e24f9f88cc7411c46ec48c37d290 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 20:37:50 +0200 Subject: [PATCH 100/112] Reverting to old mysql test suite --- .github/actions/setup-database/action.yml | 40 ++++++++-------- .../workflows/github_action_test_suite.yml | 47 +++++-------------- 2 files changed, 31 insertions(+), 56 deletions(-) diff --git a/.github/actions/setup-database/action.yml b/.github/actions/setup-database/action.yml index d5491525de..96c7da4022 100644 --- a/.github/actions/setup-database/action.yml +++ b/.github/actions/setup-database/action.yml @@ -19,25 +19,25 @@ runs: using: "composite" steps: - - name: Set up PostgreSQL server - if: ${{ inputs.database == 'postgresql' }} - uses: harmon758/postgresql-action@v1 - with: - postgresql version: "12" - postgresql db: "evennia" - postgresql user: "evennia" - postgresql password: "password" + # - name: Set up PostgreSQL server + # if: ${{ inputs.database == 'postgresql' }} + # uses: harmon758/postgresql-action@v1 + # with: + # postgresql version: "12" + # postgresql db: "evennia" + # postgresql user: "evennia" + # postgresql password: "password" - - name: Wait for PostgreSQL to activate - if: ${{ inputs.database == 'postgresql' }} - run: | - while ! pg_isready -h 127.0.0.1 -q >/dev/null 2>&1 - do - sleep 1 - echo -n . - done - echo - shell: bash + # - name: Wait for PostgreSQL to activate + # if: ${{ inputs.database == 'postgresql' }} + # run: | + # while ! pg_isready -h 127.0.0.1 -q >/dev/null 2>&1 + # do + # sleep 1 + # echo -n . + # done + # echo + # shell: bash - name: Set up MySQL server if: ${{ inputs.database == 'mysql' }} @@ -50,7 +50,7 @@ runs: collation server: "utf8_general_ci" mysql database: "evennia" mysql user: "evennia" - mysql password: "password" + mysql password: "evennia" mysql root password: root_password - name: Wait for MySQL to activate @@ -68,7 +68,7 @@ runs: if: ${{ inputs.database == 'mysql' }} run: | cat <- - --health-cmd=pg_isready - --health-interval=10s - --health-timeout=5s - --health-retries=5 + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 ports: - 5432:5432 - mysql: - image: mysql:8.0 - env: - MYSQL_ROOT_PASSWORD: root_password - MYSQL_DATABASE: evennia - MYSQL_USER: evennia - MYSQL_PASSWORD: evennia - options: >- - --health-cmd="mysqladmin ping" - --health-interval=10s - --health-timeout=5s - --health-retries=5 - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --innodb-large-prefix=1 - --innodb-file-format=Barracuda - --innodb-file-per-table=1 - ports: - - 3306:3306 - steps: - uses: actions/checkout@v3 - # - name: Reboot MySQL with custom options - # if: ${{ matrix.TESTING_DB == 'mysql' }} - # run: | - # sudo cp ${{ github.workspace }}/.github/workflows/mysql_options.cnf /etc/mysql/conf.d/custom.cnf - # sudo chown -R mysql:mysql /etc/mysql/conf.d/custom.cnf - - # - name: Set up database (${{ matrix.TESTING_DB }}) - # uses: ./.github/actions/setup-database - # with: - # database: ${{ matrix.TESTING_DB }} - # timeout-minutes: 5 + - name: Set up MySQL + if: ${{ matrix.TESTING_DB == 'mysql' }} + uses: ./.github/actions/setup-database + with: + database: ${{ matrix.TESTING_DB }} + timeout-minutes: 5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 From 6e4ca9c98e20185bfd0b63231bf36745c1dbeb54 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 21:09:20 +0200 Subject: [PATCH 101/112] Testing unit test execution with more verbosity --- .github/workflows/github_action_test_suite.yml | 4 +++- .github/workflows/mysql_options.cnf | 12 ------------ 2 files changed, 3 insertions(+), 13 deletions(-) delete mode 100644 .github/workflows/mysql_options.cnf diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index a62e88caf9..bd0c153607 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -89,7 +89,9 @@ jobs: if: ${{ ! matrix.coverage-test }} working-directory: testing_mygame run: | - evennia test ${{ env.UNIT_TEST_SETTINGS }} evennia + echo "::group::Running tests ..." + evennia test ${{ env.UNIT_TEST_SETTINGS }} evennia -v 2 --parallel + echo "::endgroup::" env: POSTGRES_HOST: localhost POSTGRES_PORT: 5432 diff --git a/.github/workflows/mysql_options.cnf b/.github/workflows/mysql_options.cnf deleted file mode 100644 index 78711ebd9b..0000000000 --- a/.github/workflows/mysql_options.cnf +++ /dev/null @@ -1,12 +0,0 @@ -[mysqld] -character-set-server = utf8mb4 -collation-server = utf8mb4_unicode_ci -innodb_large_prefix = 1 -innodb_file_format = Barracuda -innodb_file_per_table = 1 - -[mysql] -default-character-set = utf8mb4 - -[client] -default-character-set = utf8mb4 From 9e83b42a91d6cef8be7804c81973461a627d4039 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 21:14:19 +0200 Subject: [PATCH 102/112] More testing --- .github/workflows/github_action_test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index bd0c153607..82bd796325 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -90,7 +90,7 @@ jobs: working-directory: testing_mygame run: | echo "::group::Running tests ..." - evennia test ${{ env.UNIT_TEST_SETTINGS }} evennia -v 2 --parallel + evennia test ${{ env.UNIT_TEST_SETTINGS }} evennia -v 2 echo "::endgroup::" env: POSTGRES_HOST: localhost From 71df0d6656959b750742f78ffdafaefcbb81b1cc Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 21:18:47 +0200 Subject: [PATCH 103/112] Use actually correct syntax --- .github/workflows/github_action_test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index 82bd796325..79a6402ac8 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -90,7 +90,7 @@ jobs: working-directory: testing_mygame run: | echo "::group::Running tests ..." - evennia test ${{ env.UNIT_TEST_SETTINGS }} evennia -v 2 + evennia test ${{ env.UNIT_TEST_SETTINGS }} -v 2 --parallel evennia echo "::endgroup::" env: POSTGRES_HOST: localhost From 0f8a9489a8af1125aefb96a1a2d7bef47494c7ee Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 21:22:28 +0200 Subject: [PATCH 104/112] Testing parallel again --- .github/workflows/github_action_test_suite.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index 79a6402ac8..5f6f78c997 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -29,7 +29,7 @@ jobs: timeout-minutes: 40 env: - UNIT_TEST_SETTINGS: "--settings=settings --keepdb --timing" + UNIT_TEST_SETTINGS: "--settings=settings --keepdb --timing --parallel" COVERAGE_TEST_SETTINGS: "--settings=settings --timing" services: @@ -90,7 +90,7 @@ jobs: working-directory: testing_mygame run: | echo "::group::Running tests ..." - evennia test ${{ env.UNIT_TEST_SETTINGS }} -v 2 --parallel evennia + evennia test ${{ env.UNIT_TEST_SETTINGS }} evennia echo "::endgroup::" env: POSTGRES_HOST: localhost From cb65d4b2168d5071017b5ee539e085e8139ff823 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 21:28:35 +0200 Subject: [PATCH 105/112] Setting auto parallel mode --- .github/workflows/github_action_test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index 5f6f78c997..d428aa4ea3 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -29,7 +29,7 @@ jobs: timeout-minutes: 40 env: - UNIT_TEST_SETTINGS: "--settings=settings --keepdb --timing --parallel" + UNIT_TEST_SETTINGS: "--settings=settings --keepdb --timing --parallel auto" COVERAGE_TEST_SETTINGS: "--settings=settings --timing" services: From 856780b8a0dc8f513ecd09051425a4e216310a0c Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 21:32:07 +0200 Subject: [PATCH 106/112] Testing to remove the keepdb arg --- .github/workflows/github_action_test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index d428aa4ea3..90e80b4e42 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -29,7 +29,7 @@ jobs: timeout-minutes: 40 env: - UNIT_TEST_SETTINGS: "--settings=settings --keepdb --timing --parallel auto" + UNIT_TEST_SETTINGS: "--settings=settings --timing --parallel auto" COVERAGE_TEST_SETTINGS: "--settings=settings --timing" services: From 762761a01b7e8aeec392f1ed8503bcea7485557f Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 22:00:52 +0200 Subject: [PATCH 107/112] Testing shrinking the matrix --- .github/actions/setup-database/action.yml | 36 +++++++++---------- .../workflows/github_action_test_suite.yml | 21 ++--------- .github/workflows/mysql_settings.py | 14 ++++---- 3 files changed, 27 insertions(+), 44 deletions(-) diff --git a/.github/actions/setup-database/action.yml b/.github/actions/setup-database/action.yml index 96c7da4022..e6bd65f5de 100644 --- a/.github/actions/setup-database/action.yml +++ b/.github/actions/setup-database/action.yml @@ -19,25 +19,25 @@ runs: using: "composite" steps: - # - name: Set up PostgreSQL server - # if: ${{ inputs.database == 'postgresql' }} - # uses: harmon758/postgresql-action@v1 - # with: - # postgresql version: "12" - # postgresql db: "evennia" - # postgresql user: "evennia" - # postgresql password: "password" + - name: Set up PostgreSQL server + if: ${{ inputs.database == 'postgresql' }} + uses: harmon758/postgresql-action@v1 + with: + postgresql version: "12" + postgresql db: "evennia" + postgresql user: "evennia" + postgresql password: "password" - # - name: Wait for PostgreSQL to activate - # if: ${{ inputs.database == 'postgresql' }} - # run: | - # while ! pg_isready -h 127.0.0.1 -q >/dev/null 2>&1 - # do - # sleep 1 - # echo -n . - # done - # echo - # shell: bash + - name: Wait for PostgreSQL to activate + if: ${{ inputs.database == 'postgresql' }} + run: | + while ! pg_isready -h 127.0.0.1 -q >/dev/null 2>&1 + do + sleep 1 + echo -n . + done + echo + shell: bash - name: Set up MySQL server if: ${{ inputs.database == 'mysql' }} diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index 90e80b4e42..20b313e531 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -19,7 +19,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.12"] TESTING_DB: ["sqlite3", "postgresql", "mysql"] include: - python-version: "3.10" @@ -32,25 +32,10 @@ jobs: UNIT_TEST_SETTINGS: "--settings=settings --timing --parallel auto" COVERAGE_TEST_SETTINGS: "--settings=settings --timing" - services: - postgres: - image: postgres:12 - env: - POSTGRES_USER: evennia - POSTGRES_DB: evennia - POSTGRES_PASSWORD: evennia - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - steps: - uses: actions/checkout@v3 - - name: Set up MySQL + - name: Set up {{ matrix.TESTING_DB }} database if: ${{ matrix.TESTING_DB == 'mysql' }} uses: ./.github/actions/setup-database with: @@ -89,9 +74,7 @@ jobs: if: ${{ ! matrix.coverage-test }} working-directory: testing_mygame run: | - echo "::group::Running tests ..." evennia test ${{ env.UNIT_TEST_SETTINGS }} evennia - echo "::endgroup::" env: POSTGRES_HOST: localhost POSTGRES_PORT: 5432 diff --git a/.github/workflows/mysql_settings.py b/.github/workflows/mysql_settings.py index 7ffa74c347..16fdb74b9e 100644 --- a/.github/workflows/mysql_settings.py +++ b/.github/workflows/mysql_settings.py @@ -50,13 +50,13 @@ DATABASES = { "charset": "utf8mb4", "init_command": "set collation_connection=utf8mb4_unicode_ci", }, - "TEST": { - "NAME": "evennia", - "OPTIONS": { - "charset": "utf8mb4", - "init_command": "set collation_connection=utf8mb4_unicode_ci", - }, - }, +# "TEST": { +# "NAME": "evennia", +# "OPTIONS": { +# "charset": "utf8mb4", +# "init_command": "set collation_connection=utf8mb4_unicode_ci", +# }, +# }, } } From 90cd9e8308b881ae5b466905b246582b665b0134 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 22:05:54 +0200 Subject: [PATCH 108/112] Fix configs --- .github/actions/setup-database/action.yml | 2 +- .github/workflows/github_action_test_suite.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup-database/action.yml b/.github/actions/setup-database/action.yml index e6bd65f5de..efcf35c8f1 100644 --- a/.github/actions/setup-database/action.yml +++ b/.github/actions/setup-database/action.yml @@ -26,7 +26,7 @@ runs: postgresql version: "12" postgresql db: "evennia" postgresql user: "evennia" - postgresql password: "password" + postgresql password: "evennia" - name: Wait for PostgreSQL to activate if: ${{ inputs.database == 'postgresql' }} diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index 20b313e531..5b5932b269 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -29,7 +29,7 @@ jobs: timeout-minutes: 40 env: - UNIT_TEST_SETTINGS: "--settings=settings --timing --parallel auto" + UNIT_TEST_SETTINGS: "--settings=settings --keepdb --timing --parallel auto" COVERAGE_TEST_SETTINGS: "--settings=settings --timing" steps: From 609b646e45edd65a17f16e9ae24d0e513700c746 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 22:13:40 +0200 Subject: [PATCH 109/112] Revert CI experiments --- .github/actions/setup-database/action.yml | 6 ++--- .../workflows/github_action_test_suite.yml | 14 +++------- .github/workflows/mysql_settings.py | 26 +++++++++---------- .github/workflows/postgresql_settings.py | 7 ++--- 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/.github/actions/setup-database/action.yml b/.github/actions/setup-database/action.yml index efcf35c8f1..d5491525de 100644 --- a/.github/actions/setup-database/action.yml +++ b/.github/actions/setup-database/action.yml @@ -26,7 +26,7 @@ runs: postgresql version: "12" postgresql db: "evennia" postgresql user: "evennia" - postgresql password: "evennia" + postgresql password: "password" - name: Wait for PostgreSQL to activate if: ${{ inputs.database == 'postgresql' }} @@ -50,7 +50,7 @@ runs: collation server: "utf8_general_ci" mysql database: "evennia" mysql user: "evennia" - mysql password: "evennia" + mysql password: "password" mysql root password: root_password - name: Wait for MySQL to activate @@ -68,7 +68,7 @@ runs: if: ${{ inputs.database == 'mysql' }} run: | cat < Date: Thu, 27 Jun 2024 22:37:43 +0200 Subject: [PATCH 110/112] Evennia 4.2.0 minor release --- CHANGELOG.md | 4 +++- docs/source/Coding/Changelog.md | 4 +++- docs/source/Contribs/Contrib-RPSystem.md | 8 +++++++- docs/source/index.md | 2 +- evennia/VERSION.txt | 2 +- pyproject.toml | 2 +- 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43f6d0ad1d..6f8c305a7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog -## Main branch +## Evennia 4.2.0 + +June 27, 2024 - [Feature][pull3470]: New `exit_order` kwarg to `DefaultObject.get_display_exits` to easier customize the order in which diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 43f6d0ad1d..6f8c305a7f 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -1,6 +1,8 @@ # Changelog -## Main branch +## Evennia 4.2.0 + +June 27, 2024 - [Feature][pull3470]: New `exit_order` kwarg to `DefaultObject.get_display_exits` to easier customize the order in which diff --git a/docs/source/Contribs/Contrib-RPSystem.md b/docs/source/Contribs/Contrib-RPSystem.md index 0be85deeb4..3502f298e0 100644 --- a/docs/source/Contribs/Contrib-RPSystem.md +++ b/docs/source/Contribs/Contrib-RPSystem.md @@ -73,8 +73,14 @@ class Room(ContribRPRoom): # ... ``` +You need to set up Evennia to use the RPsystem's form to separate +between sdescs (`3-tall`) to make it compatible with how the rest of Evennia +separates between other multi-matches of searches/commands: -You will then need to reload the server and potentially force-reload + SEARCH_MULTIMATCH_REGEX = r"(?P[0-9]+)-(?P[^-]*)(?P.*)" + SEARCH_MULTIMATCH_TEMPLATE = " {number}-{name}{aliases}{info}\n" + +Finally, you will then need to reload the server and potentially force-reload your objects, if you originally created them without this. Example for your character: diff --git a/docs/source/index.md b/docs/source/index.md index 897a181933..a1ab64fa93 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -1,6 +1,6 @@ # Evennia Documentation -This is the manual of [Evennia](https://www.evennia.com), the open source Python `MU*` creation system. Use the Search bar on the left to find or discover interesting articles. This manual was last updated June 27, 2024, see the [Evennia Changelog](Coding/Changelog.md). Latest released Evennia version is 4.1.1. +This is the manual of [Evennia](https://www.evennia.com), the open source Python `MU*` creation system. Use the Search bar on the left to find or discover interesting articles. This manual was last updated June 27, 2024, see the [Evennia Changelog](Coding/Changelog.md). Latest released Evennia version is 4.2.0. - [Introduction](./Evennia-Introduction.md) - what is this Evennia thing? - [Evennia in Pictures](./Evennia-In-Pictures.md) - a visual overview of Evennia diff --git a/evennia/VERSION.txt b/evennia/VERSION.txt index 627a3f43a6..6aba2b245a 100644 --- a/evennia/VERSION.txt +++ b/evennia/VERSION.txt @@ -1 +1 @@ -4.1.1 +4.2.0 diff --git a/pyproject.toml b/pyproject.toml index 7c507aada5..3463aff417 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evennia" -version = "4.1.1" +version = "4.2.0" maintainers = [{ name = "Griatch", email = "griatch@gmail.com" }] description = "A full-featured toolkit and server for text-based multiplayer games (MUDs, MU*, etc)." requires-python = ">=3.10" From 9ddaabc60b7a2c0997edea931008fcaf5286124f Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 22:50:27 +0200 Subject: [PATCH 111/112] Fix typo in changelog --- CHANGELOG.md | 2 +- docs/source/Coding/Changelog.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f8c305a7f..2687383269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,7 @@ June 27, 2024 template properly (InspectorCaracal) - [Fix][pull3545]: Fix fallback issue in cmdhandler for local-object cmdsets (InspectorCaracal) - [Fix][pull3554]: Fix/readd custom `ic` command to the `character_creator` contrib (InspectorCaracal) -- [Fix][pull3466]: Make sure the `website/base.html` website base is targeted +- [Fix][pull3566]: Make sure the `website/base.html` website base is targeted explicitly so it doesn't get overridden by same file name elsewhere in app (InspectorCaracal) - [fix][issue3387]: Update all game template doc strings to be more up-to-date (Griatch) diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 6f8c305a7f..2687383269 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -50,7 +50,7 @@ June 27, 2024 template properly (InspectorCaracal) - [Fix][pull3545]: Fix fallback issue in cmdhandler for local-object cmdsets (InspectorCaracal) - [Fix][pull3554]: Fix/readd custom `ic` command to the `character_creator` contrib (InspectorCaracal) -- [Fix][pull3466]: Make sure the `website/base.html` website base is targeted +- [Fix][pull3566]: Make sure the `website/base.html` website base is targeted explicitly so it doesn't get overridden by same file name elsewhere in app (InspectorCaracal) - [fix][issue3387]: Update all game template doc strings to be more up-to-date (Griatch) From aeb0cf6854ce65f78c5a0be995019656cb8100ad Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jun 2024 23:59:17 +0200 Subject: [PATCH 112/112] Update to latest GH checkout action --- .github/workflows/github_action_test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github_action_test_suite.yml b/.github/workflows/github_action_test_suite.yml index c05faa95a5..2bcb6c0153 100644 --- a/.github/workflows/github_action_test_suite.yml +++ b/.github/workflows/github_action_test_suite.yml @@ -33,7 +33,7 @@ jobs: COVERAGE_TEST_SETTINGS: "--settings=settings --timing" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up database (${{ matrix.TESTING_DB }}) uses: ./.github/actions/setup-database