diff --git a/evennia/utils/olc/__init__.py b/evennia/utils/olc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/evennia/utils/olc/olc.py b/evennia/utils/olc/olc.py new file mode 100644 index 000000000..5e16bd8b2 --- /dev/null +++ b/evennia/utils/olc/olc.py @@ -0,0 +1,148 @@ +""" +OLC - On-Line Creation + +This module is the core of the Evennia online creation helper system. +This is a resource intended for players with build privileges. + +While the OLC command can be used to start the OLC "from the top", the +system is also intended to be plugged in to enhance existing build commands +with a more menu-like building style. + +Functionality: + +- Prototype management: Allows to create and edit Prototype +dictionaries. Can store such Prototypes on the Builder Player as an Attribute +or centrally on a central store that all builders can fetch prototypes from. +- Creates a new entity either from an existing prototype or by creating the +prototype on the fly for the sake of that single object (the prototype can +then also be saved for future use). +- Recording of session, for performing a series of recorded build actions in sequence. +Stored so as to be possible to reproduce. +- Export of objects created in recording mode to a batchcode file (Immortals only). + + +""" + +from collections import OrderedDict +from time import time +from evennia.utils.evmenu import EvMenu +from evennia.commands.command import Command + + +# OLC settings +_SHOW_PROMPT = True # settings.OLC_SHOW_PROMPT +_DEFAULT_PROMPT = "" # settings.OLC_DEFAULT_PROMPT +_LEN_HISTORY = 10 # settings.OLC_HISTORY_LENGTH + + +# OLC Session + +def _new_session(): + + """ + This generates an empty olcsession structure, which is used to hold state + information in the olc but which can also be pickled. + + Returns: + olcsession (dict): An empty OLCSession. + + Notes: + This is a customized dict which the Attribute system will + understand how to pickle and depickle since it provides + iteration. + """ + return { + # header info + "caller": None, # the current user of this session + "modified": time.now(), # last time this olcsession was active + "db_model": None, # currently unused, ObjectDB for now + "prompt_template": _DEFAULT_PROMPT, # prompt display + "olcfields": OrderedDict(), # registered OLCFields. Order matters + "prototype_key": "", # current active prototype key + } + + +def _update_prompt(osession): + """ + Update the OLC status prompt. + + Returns: + prompt (str): The prompt based on the + prompt template, populated with + the olcsession state. + + """ + return "" + + +def search_entity(osession, query): + """ + Perform a query for a specified entity. Which type of entity is determined by the osession + state. + + Args: + query (str): This is a string, a #dbref or an extended search + + """ + osession['db_model'].__class__. + + + + +def display_prototype(osession): + """ + Display prototype fields according to the order of the registered olcfields. + + """ + # TODO: Simple one column display to begin with - make multi-column later + pkey = osession['prototype_key'] + outtxt = ["=== {pkey} ===".format(pkey=pkey)] + for field in osession['olcfields'].values(): + fname, flabel, fvalue = field.name, field.label, field.display() + outtxt.append(" {fieldname} ({label}): {value}".format(fieldname=fname, + label=flabel, value=fvalue)) + return '\n'.join(outtxt) + + +def display_field_value(osession, fieldname): + """ + Display info about a specific field. + """ + field = osession['olcfields'].get(fieldname, None) + if field: + return "{fieldname}: {value}".format(fieldname=field.name, value=field.display()) + + +# Access function + +def OLC(caller, target=None, startnode=None): + """ + This function is a common entry-point into the OLC menu system. It is used + by Evennia systems to jump into the different possible start points of the + OLC menu tree depending on what info is already available. + + Args: + caller (Object or Player): The one using the olc. + target (Object, optional): Object to operate on, if any is known. + startnode (str, optional): Where in the menu tree to start. If unset, + will be decided by whether target is given or not. + + """ + startnode = startnode or (target and "node_edit_top") or "node_top" + EvMenu(caller, "evennia.utils.olc.olc_menu", startnode=startnode, target=target) + + +class CmdOLC(Command): + """ + Test OLC + + Usage: + olc [target] + + Starts the olc to create a new object or to modify an existing one. + + """ + key = "olc" + def func(self): + OLC(self.caller, target=self.args) + diff --git a/evennia/utils/olc/olc_fields.py b/evennia/utils/olc/olc_fields.py new file mode 100644 index 000000000..376d54f63 --- /dev/null +++ b/evennia/utils/olc/olc_fields.py @@ -0,0 +1,291 @@ +""" +OLC fields describe how to edit and display a specific piece of data of a prototype within the OLC system. + +The OLC system imports and adds these field classes to its prototype manipulation pages in order to +know what data to read and how to display it. + +""" +from collections import deque +# from django.conf import settings + +_OLC_VALIDATION_ERROR = """ +Error storing data in {fieldname}: + {value} +The reported error was + {error} +""" + +_LEN_HISTORY = 10 # settings.OLC_HISTORY_LENGTH + + +class OLCField(object): + """ + This is the parent for all OLC fields. This docstring acts + as the help text for the field. + + """ + # name of this field, for error reporting + key = "Empty field" + # if this field must have a value different than None + required = False + # used for displaying extra info in the OLC + label = "Empty field" + # initial value of field if not given + initial = None + # actions available on this field. Available actions + # are replace, edit, append, remove, clear, help + actions = ['replace', 'edit', 'remove', 'clear', 'help'] + + def __init__(self, olcsession): + self.olcsession = olcsession + self._value_history = deque([self.initial], _LEN_HISTORY) + self._history_pos = 0 + self._has_changed = False + + def __repr__(self): + return self.display() + + # storing data to the field in a history-aware way + @property + def value(self): + return self._value_history[self._history_pos] + + @value.setter + def value(self, value): + """ + Update field value by updating the history. + """ + original_value = value + try: + value = self.validate(value) + except Exception as err: + errtext = _OLC_VALIDATION_ERROR.format(fieldname=self.key, value=original_value, error=err) + self.olcsession.caller.msg(errtext) + return + if (self._value_history and isinstance(value, (basestring, bool, int, float)) and + self._value_history[0] == value): + # don't change/update history if re-adding the same thing + return + else: + self._has_changed = True + self._history_pos = 0 + self._value_history.appendleft(value) + + @value.deleter + def value(self): + self.history_pos = 0 + self._value_history.appendleft(self.initial) + + def history(self, step): + """ + Change history position. + + Args: + step (int): Step in the history stack. Positive movement + means moving futher back in history (with a maximum + of `settings.OLC_HISTORY_LENGTH`, negative steps + moves towards recent history (with 0 being the latest + value). + + """ + self._history_pos = min(len(self.value_history)-1, max(0, self._history_pos + step)) + + def has_changed(self): + """ + Check if this field has changed. + + Returns: + changed (bool): If the field changed or not. + + """ + return bool(self._has_changed) + + # overloadable methods + + def from_entity(self, entity, **kwargs): + """ + Populate this field from an entity. + + Args: + entity (any): An object to use for + populating this field (like an Object). + """ + pass + + def to_prototype(self, prototype): + """ + Store this field value in a prototype. + + Args: + prototype (dict): The prototype dict + to update with the value of this field. + """ + pass + + def validate(self, value, **kwargs): + """ + Validate/preprocess data to store in this field. + + Args: + value (any): An input value to + validate + + Kwargs: + any (any): Optional info to send to field. + + Returns: + validated_value (any): The value, correctly + validated and/or processed to store in this field. + + Raises: + Exception: If the field was given an + invalid value to validate. + + """ + return str(value) + + def display(self): + """ + How to display the field contents in the OLC display. + + """ + return self.value + + +# OLCFields for all the standard model properties +# key, location, destination, home, aliases, +# permissions, tags, attributes +# ... + + +class OLCKeyField(OLCField): + """ + The name (key) of the object is its main identifier, used + throughout listings even if may not always be visible to + the end user. + """ + key = 'Name' + required = True + label = "The object's name" + + def from_entity(self, entity, **kwargs): + self.value = entity.db_key + + def to_prototype(self, prototype): + prototype['key'] = self.value + + +class OLCLocationField(OLCField): + """ + An object's location is usually a Room but could be any + other in-game entity. By convention, Rooms themselves have + a None location. Objects are otherwise only placed in a + None location to take them out of the game. + """ + key = 'Location' + required = False + label = "The object's current location" + + def validate(self, value): + return self.olcsession.search_by_string(value) + + def from_entity(self, entity, **kwargs): + self.value = entity.db_location + + def to_prototype(self, prototype): + prototype['location'] = self.value + + +class OLCHomeField(OLCField): + """ + An object's home location acts as a fallback when various + extreme situations occur. An example is when a location is + deleted - all its content (except exits) are then not deleted + but are moved to each object's home location. + """ + key = 'Home' + required = True + label = "The object's home location" + + def validate(self, value): + return self.olcsession.search_by_string(value) + + def from_entity(self, entity, **kwargs): + self.value = entity.db_home + + def to_prototype(self, prototype): + prototype['home'] = self.value + +class OLCDestinationField(OLCField): + """ + An object's destination is usually not set unless the object + represents an exit between game locations. If set, the + destination should be set to the location you get to when + passing through this exit. + + """ + key = 'Destination' + required = False + label = "The object's (usually exit's) destination" + + def validate(self, value): + return self.olcsession.search_by_string(value) + + def from_entity(self, entity, **kwargs): + self.value = entity.db_destination + + def to_prototype(self, prototype): + prototype['destination'] = self.value + + +class OLCAliasField(OLCField): + """ + Specify as a comma-separated list. Use quotes around the + alias if the alias itself contains a comma. + + Aliases are alternate names for an object. An alias is just + as fast to search for as a key and two objects are assumed + to have the same name is *either* their name or any of their + aliases match. + + """ + key = 'Aliases' + required = False + label = "The object's alternative name or names" + actions = OLCField.actions + ['append'] + + def validate(self, value): + return split_by_comma(value) + + def from_entity(self, entity, **kwargs): + self.value = list(entity.db_aliases.all()) + + def to_prototype(self, prototype): + prototype['aliases'] = self.value + + +class OLCTagField(OLCField): + """ + Specify as a comma-separated list of tagname or tagname:category. + + Aliases are alternate names for an object. An alias is just + as fast to search for as a key and two objects are assumed + to have the same name is *either* their name or any of their + aliases match. + + """ + key = 'Aliases' + required = False + label = "The object's (usually exit's) destination" + actions = OLCField.actions + ['append'] + + def validate(self, value): + return [tagstr.split(':', 1) if ':' in tagstr else (tagstr, None) + for tagstr in split_by_comma(value)] + + def from_entity(self, entity, **kwargs): + self.value = entity.tags.all(return_key_and_category=True) + + def to_prototype(self, prototype): + prototype['tags'] = self.value + diff --git a/evennia/utils/olc/olc_menutree.py b/evennia/utils/olc/olc_menutree.py new file mode 100644 index 000000000..a55078294 --- /dev/null +++ b/evennia/utils/olc/olc_menutree.py @@ -0,0 +1,83 @@ +""" +This describes the menu structure/logic of the OLC system editor, using the EvMenu subsystem. The +various nodes are modular and will when possible make use of the various utilities of the OLC rather +than hard-coding things in each node. + +Menu structure: + + start: + new object + edit object + manage prototypes + export session to batchcode file (immortals only) + + new/edit object: + Protoype + Typeclass + Key + Location + Destination + PErmissions + LOcks + Attributes + TAgs + Scripts + + create/update object + copy object + save prototype + save/delete object + update existing objects + + manage prototypes + list prototype + search prototype + import prototype (from global store) + + export session + +""" + +def node_top(caller, raw_input): + # top level node + # links to edit, manage, export + text = """OnLine Creation System""" + options = ({"key": ("|yN|new", "new", "n"), + "desc": "New object", + "goto": "node_new_top", + "exec": _obj_to_prototype}, + {"key": ("|yE|ndit", "edit", "e", "m"), + "desc": "Edit existing object", + "goto": "node_edit_top", + "exec": _obj_to_prototype}, + {"key": ("|yP|nrototype", "prototype", "manage", "p", "m"), + "desc": "Manage prototypes", + "goto": "node_prototype_top"}, + {"key": ("E|yx|nport", "export", "x"), + "desc": "Export to prototypes", + "goto": "node_prototype_top"}, + {"key": ("|yQ|nuit", "quit", "q"), + "desc": "Quit OLC", + "goto": "node_quit"},) + return text, options + +def node_quit(caller, raw_input): + return 'Exiting.', None + +def node_new_top(caller, raw_input): + pass + +def node_edit_top(caller, raw_input): + # edit top level + text = """Edit object""" + + +def node_prototype_top(caller, raw_input): + # manage prototypes + pass + + +def node_export_top(caller, raw_input): + # export top level + pass + diff --git a/evennia/utils/olc/olc_utils.py b/evennia/utils/olc/olc_utils.py new file mode 100644 index 000000000..68bfd3ac9 --- /dev/null +++ b/evennia/utils/olc/olc_utils.py @@ -0,0 +1,13 @@ +""" +Miscellaneous utilities for the OLC system. + +""" +import csv + + +def search_by_string(olcsession, query): + pass + + +def split_by_comma(string): + return csv.reader([string], skipinitialspace=True)