Resolve merge conflicts

This commit is contained in:
Griatch 2020-10-08 23:35:31 +02:00
commit 90a1a0cba8
35 changed files with 2766 additions and 973 deletions

View file

@ -2,7 +2,7 @@
## Evennia 1.0 (2019-) (develop branch, WIP)
- new `drop:holds()` lock default to limit dropping nonsensical things. Access check
- New `drop:holds()` lock default to limit dropping nonsensical things. Access check
defaults to True for backwards-compatibility in 0.9, will be False in 1.0
- REST API allows you external access to db objects through HTTP requests (Tehom)
- `Object.normalize_name` and `.validate_name` added to (by default) enforce latinify
@ -20,10 +20,10 @@
- Change default multimatch syntax from 1-obj, 2-obj to obj-1, obj-2.
- Make `object.search` support 'stacks=0' keyword - if ``>0``, the method will return
N identical matches instead of triggering a multi-match error.
- Add `tags.has()` method for checking if an object has a tag or tags (PR by ChrisLR)
### Already in master
- Renamed Tutorial classes "Weapon" and "WeaponRack" to "TutorialWeapon" and
"TutorialWeaponRack" to prevent collisions with classes in mygame
### Evennia 0.95 (master)
- `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False
- `py` command now reroutes stdout to output results in-game client. `py`
without arguments starts a full interactive Python console.
@ -92,7 +92,14 @@ without arguments starts a full interactive Python console.
pagination (e.g. to create EvTables for every page instead of splittine one table)
- Using `EvMore pagination`, dramatically improves performance of `spawn/list` and `scripts` listings
(100x speed increase for displaying 1000+ prototypes/scripts).
- `EvMenu` now uses the more logically named `.ndb._evmenu` instead of `.ndb._menutree` to store itself.
Both still work for backward compatibility, but `_menutree` is deprecated.
- `EvMenu.msg(txt)` added as a central place to send text to the user, makes it easier to override.
Default `EvMenu.msg` sends with OOB type="menu" for use with OOB and webclient pane-redirects.
- New EvMenu templating system for quickly building simpler EvMenus without as much code.
- Renamed Tutorial classes "Weapon" and "WeaponRack" to "TutorialWeapon" and
"TutorialWeaponRack" to prevent collisions with classes in mygame
## Evennia 0.9 (2018-2019)

View file

@ -109,19 +109,15 @@ from the server and display them as inline HTML.
* `notifications.js` Defines onText. Generates browser notification events for each new message
while the tab is hidden.
* `oob.js` Defines onSend. Allows the user to test/send Out Of Band json messages to the server.
* `options.js` Defines most callbacks. Provides a popup-based UI to coordinate options settings with
the server.
* `options2.js` Defines most callbacks. Provides a goldenlayout-based version of the
options/settings tab. Integrates with other plugins via the custom onOptionsUI callback.
* `popups.js` Provides default popups/Dialog UI for other plugins to use.
* `splithandler.js` Defines onText. Provides an older, less-flexible alternative to goldenlayout for
multi-window UI to automatically separate out screen real-estate by type of message.
* `options.js` Defines most callbacks. Provides a popup-based UI to coordinate options settings with the server.
* `options2.js` Defines most callbacks. Provides a goldenlayout-based version of the options/settings tab. Integrates with other plugins via the custom onOptionsUI callback.
* `popups.js` Provides default popups/Dialog UI for other plugins to use.
# Writing your own Plugins
So, you love the functionality of the webclient, but your game has specific types of text that need
to be separated out into their own space, visually. There are two plugins to help with this. The
Goldenlayout plugin framework, and the older Splithandler framework.
So, you love the functionality of the webclient, but your game has specific
types of text that need to be separated out into their own space, visually.
The Goldenlayout plugin framework can help with this.
## GoldenLayout
@ -258,87 +254,4 @@ window.plugin_handler.add("myplugin", myplugin);
```
You can then add "mycomponent" to an item's componentName in your goldenlayout_default_config.js.
Make sure to stop your server, evennia collectstatic, and restart your server. Then make sure to
clear your browser cache before loading the webclient page.
## Older Splithandler
The splithandler.js plugin provides a means to do this, but you don't want to have to force every
player to set up their own layout every time they use the client.
Let's create a `mygame/web/static_overrides/webclient/js/plugins/layout.js` plugin!
First up, follow the directions in Customizing the Web Client section above to override the
base.html.
Next, add the new plugin to your copy of base.html:
```
<script src={% static "webclient/js/plugins/layout.js" %} language="javascript"
type="text/javascript"></script>
```
Remember, plugins are load-order dependent, so make sure the new `<script>` tag comes after the
splithandler.js
And finally create the layout.js file and add the minimum skeleton of a plugin to it:
```
// my new plugin
var my_plugin = (function () {
let init = function () {
console.log("myplugin! Hello World!");
}
return {
init: init,
}
})();
plugin_handler.add("myplugin", my_plugin);
```
Now, `evennia stop`, `evennia collectstatic`, and `evennia start` and then load the webclient up in
your browser.
Enable developer options and look in the console, and you should see the message 'myplugin! Hello
World!'
Since our layout.js plugin is going to use the splithandler, let's enhance this by adding a check to
make sure the splithandler.js plugin has been loaded:
change the above init function to:
```
let init = function () {
let splithandler = plugins['splithandler'];
if( splithandler ) {
console.log("MyPlugin initialized");
} else {
alert('MyPlugin requires the splithandler.js plugin. Please contact the game maintainer to
correct this');
}
}
```
And finally, the splithandler.js provides provides two functions to cut up the screen real-estate:
`dynamic_split( pane_name_to_cut_apart, direction_of_split, new_pane_name1, new_pane_name2,
text_flow_pane1, text_flow_pane2, array_of_split_percentages )`
and
`set_pane_types( pane_to_set, array_of_known_message_types_to_assign)`
In this case, we'll cut it into 3 panes, 1 bigger, two smaller, and assign 'help' messages to the
top-right pane:
```
let init = function () {
let splithandler = plugins['splithandler'];
if( splithandler ) {
splithandler.dynamic_split("main","horizontal","left","right","linefeed","linefeed",[50,50]);
splithandler.dynamic_split("right","vertical","help","misc","replace","replace",[50,50]);
splithandler.set_pane_types('help', ['help']);
console.log("MyPlugin initialized");
} else {
alert('MyPlugin requires the splithandler.js plugin. Please contact the game maintainer to
correct this');
}
}
```
`evennia stop`, `evennia collectstatic`, and `evennia start` once more, and force-reload your
browser page to clear any cached version. You should now have a nicely split layout.
Make sure to stop your server, evennia collectstatic, and restart your server. Then make sure to clear your browser cache before loading the webclient page.

View file

@ -7,7 +7,7 @@
2020-06-12 22:36:53. There are known conversion issues and missing links.
This will slowly be ironed out as this is developed.
For now you are best off using the original wiki, or the less changing v0.9.1
For now you are best off using the original wiki, or the less changing v0.9.5
of these docs. You have been warned.
```
@ -36,4 +36,4 @@ This is the manual of [Evennia](http://www.evennia.com), the open source Python
- [Links](./Links) - useful links
- [Table of Contents](./toc) - an alphabetical listing of all regular documentation pages
Want to help improve the docs? See the page on [Contributing to the docs](./Contributing-Docs)!
Want to help improve the docs? See the page on [Contributing to the docs](./Contributing-Docs)!

View file

@ -1262,7 +1262,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
]
except Exception:
logger.log_trace()
now = timezone.localtime()
if settings.USE_TZ:
now = timezone.localtime()
else:
now = timezone.now()
now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, now.day, now.hour, now.minute)
if _MUDINFO_CHANNEL:
_MUDINFO_CHANNEL.tempmsg(f"[{_MUDINFO_CHANNEL.key}, {now}]: {message}")

View file

@ -494,6 +494,11 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
cmdset = None
for cset in (cset for cset in local_obj_cmdsets if cset):
cset.duplicates = cset.old_duplicates
# important - this syncs the CmdSetHandler's .current field with the
# true current cmdset!
if cmdset:
caller.cmdset.current = cmdset
returnValue(cmdset)
except ErrorReported:
raise

View file

@ -106,9 +106,9 @@ class CmdSet(object, metaclass=_CmdSetMeta):
commands preference.
duplicates - determines what happens when two sets of equal
priority merge. Default has the first of them in the
priority merge (only). Defaults to None and has the first of them in the
merger (i.e. A above) automatically taking
precedence. But if allow_duplicates is true, the
precedence. But if `duplicates` is true, the
result will be a merger with more than one of each
name match. This will usually lead to the account
receiving a multiple-match error higher up the road,
@ -119,6 +119,16 @@ class CmdSet(object, metaclass=_CmdSetMeta):
select which ball to kick ... Allowing duplicates
only makes sense for Union and Intersect, the setting
is ignored for the other mergetypes.
Note that the `duplicates` flag is *not* propagated in
a cmdset merger. So `A + B = C` will result in
a cmdset with duplicate commands, but C.duplicates will
be `None`. For duplication to apply to a whole cmdset
stack merge, _all_ cmdsets in the stack must have
`.duplicates=True` set.
Finally, if a final cmdset has `.duplicates=None` (the normal
unless created alone with another value), the cmdhandler
will assume True for object-based cmdsets and False for
all other. This is usually the most intuitive outcome.
key_mergetype (dict) - allows the cmdset to define a unique
mergetype for particular cmdsets. Format is
@ -144,14 +154,27 @@ class CmdSet(object, metaclass=_CmdSetMeta):
mergetype = "Union"
priority = 0
# These flags, if set to None, will allow "pass-through" of lower-prio settings
# of True/False. If set to True/False, will override lower-prio settings.
# These flags, if set to None should be interpreted as 'I don't care' and,
# will allow "pass-through" even of lower-prio cmdsets' explicitly True/False
# options. If this is set to True/False however, priority matters.
no_exits = None
no_objs = None
no_channels = None
# same as above, but if left at None in the final merged set, the
# cmdhandler will auto-assume True for Objects and stay False for all
# other entities.
# The .duplicates setting does not propagate and since duplicates can only happen
# on same-prio cmdsets, there is no concept of passthrough on `None`.
# The merger of two cmdsets always return in a cmdset with `duplicates=None`
# (even if the result may have duplicated commands).
# If a final cmdset has `duplicates=None` (normal, unless the cmdset is
# created on its own with the flag set), the cmdhandler will auto-assume it to be
# True for Object-based cmdsets and stay None/False for all other entities.
#
# Example:
# A and C has .duplicates=True, B has .duplicates=None (or False)
# B + A = BA, where BA will have duplicate cmds, but BA.duplicates = None
# BA + C = BAC, where BAC will have more duplication, but BAC.duplicates = None
#
# Basically, for the `.duplicate` setting to survive throughout a
# merge-stack, every cmdset in the stack must have `duplicates` set explicitly.
duplicates = None
permanent = False
@ -334,7 +357,15 @@ class CmdSet(object, metaclass=_CmdSetMeta):
commands (str): Representation of commands in Cmdset.
"""
return ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)])
perm = "perm" if self.permanent else "non-perm"
options = ", ".join([
"{}:{}".format(opt, "T" if getattr(self, opt) else "F")
for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
if getattr(self, opt) is not None
])
options = (", " + options) if options else ""
return f"<CmdSet {self.key}, {self.mergetype}, {perm}, prio {self.priority}{options}>: " + ", ".join(
[str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)])
def __iter__(self):
"""
@ -401,12 +432,15 @@ class CmdSet(object, metaclass=_CmdSetMeta):
# pass through options whenever they are set, unless the merging or higher-prio
# set changes the setting (i.e. has a non-None value). We don't pass through
# the duplicates setting; that is per-merge
# the duplicates setting; that is per-merge; the resulting .duplicates value
# is always None (so merging cmdsets must all have explicit values if wanting
# to cause duplicates).
cmdset_c.no_channels = (
self.no_channels if cmdset_a.no_channels is None else cmdset_a.no_channels
)
cmdset_c.no_exits = self.no_exits if cmdset_a.no_exits is None else cmdset_a.no_exits
cmdset_c.no_objs = self.no_objs if cmdset_a.no_objs is None else cmdset_a.no_objs
cmdset_c.duplicates = None
else:
# B higher priority than A
@ -428,12 +462,15 @@ class CmdSet(object, metaclass=_CmdSetMeta):
# pass through options whenever they are set, unless the higher-prio
# set changes the setting (i.e. has a non-None value). We don't pass through
# the duplicates setting; that is per-merge
# the duplicates setting; that is per-merge; the resulting .duplicates value#
# is always None (so merging cmdsets must all have explicit values if wanting
# to cause duplicates).
cmdset_c.no_channels = (
cmdset_a.no_channels if self.no_channels is None else self.no_channels
)
cmdset_c.no_exits = cmdset_a.no_exits if self.no_exits is None else self.no_exits
cmdset_c.no_objs = cmdset_a.no_objs if self.no_objs is None else self.no_objs
cmdset_c.duplicates = None
# we store actual_mergetype since key_mergetypes
# might be different from the main mergetype.

View file

@ -293,7 +293,10 @@ class CmdSetHandler(object):
# the id of the "merged" current cmdset for easy access.
self.key = None
# this holds the "merged" current command set
# this holds the "merged" current command set. Note that while the .update
# method updates this field in order to have it synced when operating on
# cmdsets in-code, when the game runs, this field is kept up-to-date by
# the cmdsethandler's get_and_merge_cmdsets!
self.current = None
# this holds a history of CommandSets
self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)]
@ -311,27 +314,13 @@ class CmdSetHandler(object):
Display current commands
"""
string = ""
strings = ["<CmdSetHandler> stack:"]
mergelist = []
if len(self.cmdset_stack) > 1:
# We have more than one cmdset in stack; list them all
for snum, cmdset in enumerate(self.cmdset_stack):
mergetype = self.mergetype_stack[snum]
permstring = "non-perm"
if cmdset.permanent:
permstring = "perm"
if mergetype != cmdset.mergetype:
mergetype = "%s^" % (mergetype)
string += "\n %i: <%s (%s, prio %i, %s)>: %s" % (
snum,
cmdset.key,
mergetype,
cmdset.priority,
permstring,
cmdset,
)
mergelist.append(str(snum))
string += "\n"
mergelist.append(str(snum + 1))
strings.append(f" {snum + 1}: {cmdset}")
# Display the currently active cmdset, limited by self.obj's permissions
mergetype = self.mergetype_stack[-1]
@ -339,27 +328,15 @@ class CmdSetHandler(object):
merged_on = self.cmdset_stack[-2].key
mergetype = _("custom {mergetype} on cmdset '{cmdset}'")
mergetype = mergetype.format(mergetype=mergetype, cmdset=merged_on)
if mergelist:
tmpstring = _(" <Merged {mergelist} {mergetype}, prio {prio}>: {current}")
string += tmpstring.format(
mergelist="+".join(mergelist),
mergetype=mergetype,
prio=self.current.priority,
current=self.current,
)
# current is a result of mergers
mergelist="+".join(mergelist)
strings.append(f" <Merged {mergelist}>: {self.current}")
else:
permstring = "non-perm"
if self.current.permanent:
permstring = "perm"
tmpstring = _(" <{key} ({mergetype}, prio {prio}, {permstring})>:\n {keylist}")
string += tmpstring.format(
key=self.current.key,
mergetype=mergetype,
prio=self.current.priority,
permstring=permstring,
keylist=", ".join(cmd.key for cmd in sorted(self.current, key=lambda o: o.key)),
)
return string.strip()
# current is a single cmdset
strings.append(" " + str(self.current))
return "\n".join(strings).rstrip()
def _import_cmdset(self, cmdset_path, emit_to_obj=None):
"""
@ -381,12 +358,22 @@ class CmdSetHandler(object):
def update(self, init_mode=False):
"""
Re-adds all sets in the handler to have an updated current
set.
Args:
init_mode (bool, optional): Used automatically right after
this handler was created; it imports all permanent cmdsets
from the database.
Notes:
This method is necessary in order to always have a `.current`
cmdset when working with the cmdsethandler in code. But the
CmdSetHandler doesn't (cannot) consider external cmdsets and game
state. This means that the .current calculated from this method
will likely not match the true current cmdset as determined at
run-time by `cmdhandler.get_and_merge_cmdsets()`. So in a running
game the responsibility of keeping `.current` upt-to-date belongs
to the central `cmdhandler.get_and_merge_cmdsets()`!
"""
if init_mode:
# reimport all permanent cmdsets

View file

@ -147,6 +147,9 @@ class Command(object, metaclass=CommandMeta):
arg_regex - (optional) raw string regex defining how the argument part of
the command should look in order to match for this command
(e.g. must it be a space between cmdname and arg?)
auto_help_display_key - (optional) if given, this replaces the string shown
in the auto-help listing. This is particularly useful for system-commands
whose actual key is not really meaningful.
(Note that if auto_help is on, this initial string is also used by the
system to create the help entry for the command, so it's a good idea to

View file

@ -2429,13 +2429,13 @@ class CmdExamine(ObjManipCommand):
)
return output
def format_output(self, obj, avail_cmdset):
def format_output(self, obj, current_cmdset):
"""
Helper function that creates a nice report about an object.
Args:
obj (any): Object to analyze.
avail_cmdset (CmdSet): Current cmdset for object.
current_cmdset (CmdSet): Current cmdset for object.
Returns:
str: The formatted string.
@ -2513,15 +2513,36 @@ class CmdExamine(ObjManipCommand):
# cmdsets
if not (len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"):
# all() returns a 'stack', so make a copy to sort.
def _format_options(cmdset):
"""helper for cmdset-option display"""
def _truefalse(string, value):
if value is None:
return ""
if value:
return f"{string}: T"
return f"{string}: F"
options = ", ".join(
_truefalse(opt, getattr(cmdset, opt))
for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
if getattr(cmdset, opt) is not None
)
options = ", " + options if options else ""
return options
# cmdset stored on us
stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority, reverse=True)
output["Stored Cmdset(s)"] = "\n " + "\n ".join(
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority})"
for cmdset in stored_cmdsets
if cmdset.key != "_EMPTY_CMDSET"
)
stored = []
for cmdset in stored_cmdsets:
if cmdset.key == "_EMPTY_CMDSET":
continue
options = _format_options(cmdset)
stored.append(
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority}{options})")
output["Stored Cmdset(s)"] = "\n " + "\n ".join(stored)
# this gets all components of the currently merged set
all_cmdsets = [(cmdset.key, cmdset) for cmdset in avail_cmdset.merged_from]
all_cmdsets = [(cmdset.key, cmdset) for cmdset in current_cmdset.merged_from]
# we always at least try to add account- and session sets since these are ignored
# if we merge on the object level.
if hasattr(obj, "account") and obj.account:
@ -2551,15 +2572,24 @@ class CmdExamine(ObjManipCommand):
pass
all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()]
all_cmdsets.sort(key=lambda x: x.priority, reverse=True)
output["Merged Cmdset(s)"] = "\n " + "\n ".join(
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype} prio {cmdset.priority})"
for cmdset in all_cmdsets
)
# list the commands available to this object
avail_cmdset = sorted([cmd.key for cmd in avail_cmdset if cmd.access(obj, "cmd")])
cmdsetstr = "\n" + utils.fill(", ".join(avail_cmdset), indent=2)
# the resulting merged cmdset
options = _format_options(current_cmdset)
merged = [
f"<Current merged cmdset> ({current_cmdset.mergetype} prio {current_cmdset.priority}{options})"]
# the merge stack
for cmdset in all_cmdsets:
options = _format_options(cmdset)
merged.append(
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype} prio {cmdset.priority}{options})")
output["Merged Cmdset(s)"] = "\n " + "\n ".join(merged)
# list the commands available to this object
current_commands = sorted([cmd.key for cmd in current_cmdset if cmd.access(obj, "cmd")])
cmdsetstr = "\n" + utils.fill(", ".join(current_commands), indent=2)
output[f"Commands available to {obj.key} (result of Merged CmdSets)"] = str(cmdsetstr)
# scripts
if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all():
output["Scripts"] = "\n " + f"{obj.scripts}"

View file

@ -808,17 +808,49 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
lastpages = pages[-number:]
else:
lastpages = pages
template = "|w%s|n |c%s|n to |c%s|n: %s"
lastpages = "\n ".join(
template
% (
utils.datetime_format(page.date_created),
",".join(obj.key for obj in page.senders),
"|n,|c ".join([obj.name for obj in page.receivers]),
page.message,
to_template = "|w{date}{clr} {sender}|nto{clr}{receiver}|n:> {message}"
from_template = "|w{date}{clr} {receiver}|nfrom{clr}{sender}|n:< {message}"
listing = []
prev_selfsend = False
for page in lastpages:
multi_send = len(page.senders) > 1
multi_recv = len(page.receivers) > 1
sending = self.caller in page.senders
# self-messages all look like sends, so we assume they always
# come in close pairs and treat the second of the pair as the recv.
selfsend = sending and self.caller in page.receivers
if selfsend:
if prev_selfsend:
# this is actually a receive of a self-message
sending = False
prev_selfsend = False
else:
prev_selfsend = True
clr = "|c" if sending else "|g"
sender = f"|n,{clr}".join(obj.key for obj in page.senders)
receiver = f"|n,{clr}".join([obj.name for obj in page.receivers])
if sending:
template = to_template
sender = f"{sender} " if multi_send else ""
receiver = f" {receiver}" if multi_recv else f" {receiver}"
else:
template = from_template
receiver = f"{receiver} " if multi_recv else ""
sender = f" {sender} " if multi_send else f" {sender}"
listing.append(
template.format(
date=utils.datetime_format(page.date_created),
clr=clr,
sender=sender,
receiver=receiver,
message=page.message,
)
)
for page in lastpages
)
lastpages = "\n ".join(listing)
if lastpages:
string = "Your latest pages:\n %s" % lastpages

View file

@ -378,10 +378,12 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
if not items:
string = "You are not carrying anything."
else:
from evennia.utils.ansi import raw as raw_ansi
table = self.styled_table(border="header")
for item in items:
table.add_row("|C%s|n" % item.name, item.db.desc or "")
string = "|wYou are carrying:\n%s" % table
table.add_row(f"|C{item.name}|n",
"{}|n".format(utils.crop(raw_ansi(item.db.desc), width=50) or ""))
string = f"|wYou are carrying:\n{table}"
self.caller.msg(string)

View file

@ -294,10 +294,12 @@ class CmdHelp(Command):
hdict_topic = defaultdict(list)
# create the dictionaries {category:[topic, topic ...]} required by format_help_list
# Filter commands that should be reached by the help
# system, but not be displayed in the table.
# system, but not be displayed in the table, or be displayed differently.
for cmd in all_cmds:
if self.should_list_cmd(cmd, caller):
hdict_cmd[cmd.help_category].append(cmd.key)
key = (cmd.auto_help_display_key
if hasattr(cmd, "auto_help_display_key") else cmd.key)
hdict_cmd[cmd.help_category].append(key)
[hdict_topic[topic.help_category].append(topic.key) for topic in all_topics]
# report back
self.msg_help(self.format_help_list(hdict_cmd, hdict_topic))
@ -308,7 +310,6 @@ class CmdHelp(Command):
for match_query in [f"{query}~1", f"{query}*"]:
# We first do an exact word-match followed by a start-by query
matches, suggestions = help_search_with_index(
match_query, entries, suggestion_maxnum=self.suggestion_maxnum
)

View file

@ -984,7 +984,8 @@ class TestBuilding(CommandTest):
self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was set to Room")
def test_list_cmdsets(self):
self.call(building.CmdListCmdSets(), "", "<DefaultCharacter (Union, prio 0, perm)>:")
self.call(building.CmdListCmdSets(), "",
"<CmdSetHandler> stack:\n <CmdSet DefaultCharacter, Union, perm, prio 0>:")
self.call(building.CmdListCmdSets(), "NotFound", "Could not find 'NotFound'")
def test_typeclass(self):

View file

@ -194,27 +194,71 @@ class TestCmdSetMergers(TestCase):
self.assertEqual(len(cmdset_f.commands), 4)
self.assertTrue(all(True for cmd in cmdset_f.commands if cmd.from_cmdset == "A"))
def test_option_transfer(self):
"Test transfer of cmdset options"
class TestOptionTransferTrue(TestCase):
"""
Test cmdset-merge transfer of the cmdset-special options
(no_exits/channels/objs/duplicates etc)
cmdset A has all True options
"""
def setUp(self):
super().setUp()
self.cmdset_a = _CmdSetA()
self.cmdset_b = _CmdSetB()
self.cmdset_c = _CmdSetC()
self.cmdset_d = _CmdSetD()
self.cmdset_a.priority = 0
self.cmdset_b.priority = 0
self.cmdset_c.priority = 0
self.cmdset_d.priority = 0
self.cmdset_a.no_exits = True
self.cmdset_a.no_objs = True
self.cmdset_a.no_channels = True
self.cmdset_a.duplicates = True
def test_option_transfer__reverse_sameprio_passthrough(self):
"""
A has all True options, merges last (normal reverse merge), same prio.
The options should pass through to F since none of the other cmdsets
care to change the setting from their default None.
Since A.duplicates = True, the final result is an union of duplicate
pairs (8 commands total).
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
# the options should pass through since none of the other cmdsets care
# to change the setting from None.
a.no_exits = True
a.no_objs = True
a.no_channels = True
a.duplicates = True
cmdset_f = d + c + b + a # reverse, same-prio
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertTrue(cmdset_f.duplicates)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 8)
def test_option_transfer__forward_sameprio_passthrough(self):
"""
A has all True options, merges first (forward merge), same prio. This
should pass those options through since the other all have options set
to None. The exception is `duplicates` since that is determined by
the two last mergers in the chain both being True.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
cmdset_f = a + b + c + d # forward, same-prio
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertFalse(cmdset_f.duplicates)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__reverse_highprio_passthrough(self):
"""
A has all True options, merges last (normal reverse merge) with the
highest prio. This should also pass through.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 1
c.priority = 0
@ -223,14 +267,35 @@ class TestCmdSetMergers(TestCase):
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertTrue(cmdset_f.duplicates)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__forward_highprio_passthrough(self):
"""
A has all True options, merges first (forward merge). This is a bit
synthetic since it will never happen in practice, but logic should
still make it pass through.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 1
c.priority = 0
d.priority = -1
cmdset_f = a + b + c + d # forward, A top priority. This never happens in practice.
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertTrue(cmdset_f.duplicates)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__reverse_lowprio_passthrough(self):
"""
A has all True options, merges last (normal reverse merge) with the lowest
prio. This never happens (it would always merge first) but logic should hold
and pass through since the other cmdsets have None.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = -1
b.priority = 0
c.priority = 1
@ -239,32 +304,678 @@ class TestCmdSetMergers(TestCase):
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertFalse(cmdset_f.duplicates)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__forward_lowprio_passthrough(self):
"""
A has all True options, merges first (forward merge) with lowest prio. This
is the normal behavior for a low-prio cmdset. Passthrough should happen.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = -1
b.priority = 0
c.priority = 1
d.priority = 2
cmdset_f = a + b + c + d # forward, A low prio
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertFalse(cmdset_f.duplicates)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__reverse_highprio_block_passthrough(self):
"""
A has all True options, other cmdsets has False. A merges last with high
prio. A should retain its option values and override the others
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 1
c.priority = 0
d.priority = -1
c.no_exits = False
b.no_objs = False
d.duplicates = False
# higher-prio sets will change the option up the chain
cmdset_f = d + c + b + a # reverse, high prio
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__forward_highprio_block_passthrough(self):
"""
A has all True options, other cmdsets has False. A merges last with high
prio. This situation should never happen, but logic should hold - the highest
prio's options should survive the merge process.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 1
c.priority = 0
d.priority = -1
c.no_exits = False
b.no_channels = False
b.no_objs = False
d.duplicates = False
# higher-prio sets will change the option up the chain
cmdset_f = a + b + c + d # forward, high prio, never happens
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__forward_lowprio_block(self):
"""
A has all True options, other cmdsets has False. A merges last with low
prio. This should result in its values being blocked and come out False.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = -1
b.priority = 0
c.priority = 1
d.priority = 2
c.no_exits = False
c.no_channels = False
b.no_objs = False
d.duplicates = False
# higher-prio sets will change the option up the chain
cmdset_f = a + b + c + d # forward, A low prio
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__forward_lowprio_block_partial(self):
"""
A has all True options, other cmdsets has False excet C which has a None
for `no_channels`. A merges last with low
prio. This should result in its values being blocked and come out False
except for no_channels which passes through.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = -1
b.priority = 0
c.priority = 1
d.priority = 2
c.no_exits = False
c.no_channels = None # passthrough
b.no_objs = False
d.duplicates = False
# higher-prio sets will change the option up the chain
cmdset_f = a + b + c + d # forward, A low prio
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertFalse(cmdset_f.duplicates)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
a.priority = 0
b.priority = 0
def test_option_transfer__reverse_highprio_sameprio_order_last(self):
"""
A has all True options and highest prio, D has False and lowest prio,
others are passthrough. B has the same prio as A, with passthrough.
Since A is merged last, this should give prio to A's options
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 2
c.priority = 0
d.priority = 0
d.priority = -1
d.no_channels = False
d.no_exits = False
d.no_objs = None
d.duplicates = False
# higher-prio sets will change the option up the chain
cmdset_f = d + c + b + a # reverse, A same prio, merged after b
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 8)
def test_option_transfer__reverse_highprio_sameprio_order_first(self):
"""
A has all True options and highest prio, D has False and lowest prio,
others are passthrough. B has the same prio as A, with passthrough.
While B, with None-values, is merged after A, A's options should have
replaced those of D at that point, and since B has passthrough the
final result should contain A's True options.
Note that despite A having duplicates=True, there is no duplication in
the DB + A merger since they have different priorities.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 2
c.priority = 0
d.priority = -1
d.no_channels = False
d.no_exits = False
d.no_objs = False
d.duplicates = False
# higher-prio sets will change the option up the chain
cmdset_f = d + c + a + b # reverse, A same prio, merged before b
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__reverse_lowprio_block(self):
"""
A has all True options, other cmdsets has False. A merges last with low
prio. This usually doesn't happen- it should merge last. But logic should
hold and the low-prio cmdset's values should be blocked and come out False.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = -1
b.priority = 0
c.priority = 1
d.priority = 2
c.no_exits = False
d.no_channels = False
b.no_objs = False
d.duplicates = False
# higher-prio sets will change the option up the chain
cmdset_f = d + c + b + a # reverse, A low prio, never happens
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
class TestOptionTransferFalse(TestCase):
"""
Test cmdset-merge transfer of the cmdset-special options
(no_exits/channels/objs/duplicates etc)
cmdset A has all False options
"""
def setUp(self):
super().setUp()
self.cmdset_a = _CmdSetA()
self.cmdset_b = _CmdSetB()
self.cmdset_c = _CmdSetC()
self.cmdset_d = _CmdSetD()
self.cmdset_a.priority = 0
self.cmdset_b.priority = 0
self.cmdset_c.priority = 0
self.cmdset_d.priority = 0
self.cmdset_a.no_exits = False
self.cmdset_a.no_objs = False
self.cmdset_a.no_channels = False
self.cmdset_a.duplicates = False
def test_option_transfer__reverse_sameprio_passthrough(self):
"""
A has all False options, merges last (normal reverse merge), same prio.
The options should pass through to F since none of the other cmdsets
care to change the setting from their default None.
Since A has duplicates=False, the result is a unique union of 4 cmds.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
cmdset_f = d + c + b + a # reverse, same-prio
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__forward_sameprio_passthrough(self):
"""
A has all False options, merges first (forward merge), same prio. This
should pass those options through since the other all have options set
to None. The exception is `duplicates` since that is determined by
the two last mergers in the chain both being .
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
cmdset_f = a + b + c + d # forward, same-prio
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__reverse_highprio_passthrough(self):
"""
A has all False options, merges last (normal reverse merge) with the
highest prio. This should also pass through.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 1
c.priority = 0
d.priority = -1
cmdset_f = d + c + b + a # reverse, A top priority
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__forward_highprio_passthrough(self):
"""
A has all False options, merges first (forward merge). This is a bit
synthetic since it will never happen in practice, but logic should
still make it pass through.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 1
c.priority = 0
d.priority = -1
cmdset_f = a + b + c + d # forward, A top priority. This never happens in practice.
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__reverse_lowprio_passthrough(self):
"""
A has all False options, merges last (normal reverse merge) with the lowest
prio. This never happens (it would always merge first) but logic should hold
and pass through since the other cmdsets have None.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = -1
b.priority = 0
c.priority = 1
d.priority = 2
cmdset_f = d + c + b + a # reverse, A low prio. This never happens in practice.
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__forward_lowprio_passthrough(self):
"""
A has all False options, merges first (forward merge) with lowest prio. This
is the normal behavior for a low-prio cmdset. Passthrough should happen.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = -1
b.priority = 0
c.priority = 1
d.priority = 2
cmdset_f = a + b + c + d # forward, A low prio
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__reverse_highprio_block_passthrough(self):
"""
A has all False options, other cmdsets has True. A merges last with high
prio. A should retain its option values and override the others
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 1
c.priority = 0
d.priority = -1
c.no_exits = True
b.no_objs = True
d.duplicates = True
# higher-prio sets will change the option up the chain
cmdset_f = d + c + b + a # reverse, high prio
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__forward_highprio_block_passthrough(self):
"""
A has all False options, other cmdsets has True. A merges last with high
prio. This situation should never happen, but logic should hold - the highest
prio's options should survive the merge process.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 1
c.priority = 0
d.priority = -1
c.no_exits = True
b.no_channels = True
b.no_objs = True
d.duplicates = True
# higher-prio sets will change the option up the chain
cmdset_f = a + b + c + d # forward, high prio, never happens
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__forward_lowprio_block(self):
"""
A has all False options, other cmdsets has True. A merges last with low
prio. This should result in its values being blocked and come out False.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = -1
b.priority = 0
c.priority = 1
d.priority = 2
c.no_exits = True
c.no_channels = True
b.no_objs = True
d.duplicates = True
# higher-prio sets will change the option up the chain
cmdset_f = a + b + c + d # forward, A low prio
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__forward_lowprio_block_partial(self):
"""
A has all False options, other cmdsets has True excet C which has a None
for `no_channels`. A merges last with low
prio. This should result in its values being blocked and come out True
except for no_channels which passes through.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = -1
b.priority = 0
c.priority = 1
d.priority = 2
c.no_exits = True
c.no_channels = None # passthrough
b.no_objs = True
d.duplicates = True
# higher-prio sets will change the option up the chain
cmdset_f = a + b + c + d # forward, A low prio
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__reverse_sameprio_order_last(self):
"""
A has all False options and highest prio, D has True and lowest prio,
others are passthrough. B has the same prio as A, with passthrough.
Since A is merged last, this should give prio to A's False options
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 2
c.priority = 0
d.priority = -1
d.no_channels = True
d.no_exits = True
d.no_objs = True
d.duplicates = False
# higher-prio sets will change the option up the chain
cmdset_f = d + c + b + a # reverse, A high prio, merged after b
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__reverse_sameprio_order_first(self):
"""
A has all False options and highest prio, D has True and lowest prio,
others are passthrough. B has the same prio as A, with passthrough.
While B, with None-values, is merged after A, A's options should have
replaced those of D at that point, and since B has passthrough the
final result should contain A's False options.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 2
c.priority = 0
d.priority = -1
d.no_channels = True
d.no_exits = True
d.no_objs = True
d.duplicates = False
# higher-prio sets will change the option up the chain
cmdset_f = d + c + a + b # reverse, A high prio, merged before b
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
def test_option_transfer__reverse_lowprio_block(self):
"""
A has all False options, other cmdsets has True. A merges last with low
prio. This usually doesn't happen- it should merge last. But logic should
hold and the low-prio cmdset's values should be blocked and come out True.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = -1
b.priority = 0
c.priority = 1
d.priority = 2
c.no_exits = True
d.no_channels = True
b.no_objs = True
d.duplicates = True
# higher-prio sets will change the option up the chain
cmdset_f = d + c + b + a # reverse, A low prio, never happens
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
class TestDuplicateBehavior(TestCase):
"""
Test behavior of .duplicate option, which is a bit special in that it
doesn't propagate.
`A.duplicates=True` for all tests.
"""
def setUp(self):
super().setUp()
self.cmdset_a = _CmdSetA()
self.cmdset_b = _CmdSetB()
self.cmdset_c = _CmdSetC()
self.cmdset_d = _CmdSetD()
self.cmdset_a.priority = 0
self.cmdset_b.priority = 0
self.cmdset_c.priority = 0
self.cmdset_d.priority = 0
self.cmdset_a.duplicates = True
def test_reverse_sameprio_duplicate(self):
"""
Test of `duplicates` transfer which does not propagate. Only
A has duplicates=True.
D + B = DB (no duplication, DB.duplication=None)
DB + C = DBC (no duplication, DBC.duplication=None)
DBC + A = final (duplication, final.duplication=None)
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
cmdset_f = d + b + c + a # two last mergers duplicates=True
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 8)
def test_reverse_sameprio_duplicate(self):
"""
Test of `duplicates` transfer, which does not propagate.
C.duplication=True
D + B = DB (no duplication, DB.duplication=None)
DB + C = DBC (duplication, DBC.duplication=None)
DBC + A = final (duplication, final.duplication=None)
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
c.duplicates = True
cmdset_f = d + b + c + a # two last mergers duplicates=True
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 10)
def test_forward_sameprio_duplicate(self):
"""
Test of `duplicates` transfer which does not propagate.
C.duplication=True, merges later than A
D + B = DB (no duplication, DB.duplication=None)
DB + A = DBA (duplication, DBA.duplication=None)
DBA + C = final (duplication, final.duplication=None)
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
c.duplicates = True
cmdset_f = d + b + a + c # two last mergers duplicates=True
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 10)
def test_reverse_sameprio_duplicate_reverse(self):
"""
Test of `duplicates` transfer which does not propagate.
C.duplication=False (explicit), merges before A. This behavior is the
same as if C.duplication=None, since A merges later and takes
precedence.
D + B = DB (no duplication, DB.duplication=None)
DB + C = DBC (no duplication, DBC.duplication=None)
DBC + A = final (duplication, final.duplication=None)
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
c.duplicates = False
cmdset_f = d + b + c + a # a merges last, takes precedence
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 8)
def test_reverse_sameprio_duplicate_forward(self):
"""
Test of `duplicates` transfer which does not propagate.
C.duplication=False (explicit), merges after A. This just means
only A causes duplicates, earlier in the chain.
D + B = DB (no duplication, DB.duplication=None)
DB + A = DBA (duplication, DBA.duplication=None)
DBA + C = final (no duplication, final.duplication=None)
Note that DBA has 8 cmds due to A merging onto DB with duplication,
but since C merges onto this with no duplication, the union will hold
6 commands, since C has two commands that replaces the 4 duplicates
with uniques copies from C.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
c.duplicates = False
cmdset_f = d + b + a + c # a merges before c
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 6)
class TestOptionTransferReplace(TestCase):
"""
Test option transfer through more complex merge types.
"""
def setUp(self):
super().setUp()
self.cmdset_a = _CmdSetA()
self.cmdset_b = _CmdSetB()
self.cmdset_c = _CmdSetC()
self.cmdset_d = _CmdSetD()
self.cmdset_a.priority = 0
self.cmdset_b.priority = 0
self.cmdset_c.priority = 0
self.cmdset_d.priority = 0
self.cmdset_a.no_exits = True
self.cmdset_a.no_objs = True
self.cmdset_a.no_channels = True
self.cmdset_a.duplicates = True
def test_option_transfer__replace_reverse_highprio(self):
"""
A has all options True and highest priority. C has them False and is
Replace-type.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.priority = 2
b.priority = 2
c.priority = 0
c.mergetype = "Replace"
c.no_channels = False
c.no_exits = False
c.no_objs = False
c.duplicates = False
d.priority = -1
cmdset_f = d + c + b + a # reverse, A high prio, C Replace
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 7)
def test_option_transfer__replace_reverse_highprio_from_false(self):
"""
Inverse of previous test: A has all options False and highest priority.
C has them True and is Replace-type.
"""
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
a.no_exits = False
a.no_objs = False
a.no_channels = False
a.duplicates = False
a.priority = 2
b.priority = 2
c.priority = 0
c.mergetype = "Replace"
c.no_channels = True
c.no_exits = True
c.no_objs = True
c.duplicates = True
d.priority = -1
cmdset_f = d + c + b + a # reverse, A high prio, C Replace
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
self.assertIsNone(cmdset_f.duplicates)
self.assertEqual(len(cmdset_f.commands), 4)
# test cmdhandler functions

View file

@ -1,6 +1,6 @@
from evennia.utils.test_resources import EvenniaTest
from evennia import DefaultChannel
from evennia.utils.create import create_message
from evennia.utils.test_resources import EvenniaTest
class ObjectCreationTest(EvenniaTest):
@ -16,3 +16,34 @@ class ObjectCreationTest(EvenniaTest):
msg = create_message("peewee herman", "heh-heh!", header="mail time!")
self.assertTrue(msg)
self.assertEqual(str(msg), "peewee herman->: heh-heh!")
class ChannelWholistTests(EvenniaTest):
def setUp(self):
super().setUp()
self.default_channel, _ = DefaultChannel.create("coffeetalk", description="A place to talk about coffee.")
self.default_channel.connect(self.obj1)
def test_wholist_shows_subscribed_objects(self):
expected = "Obj"
result = self.default_channel.wholist
self.assertEqual(expected, result)
def test_wholist_shows_none_when_empty(self):
# No one hates dogs
empty_channel, _ = DefaultChannel.create("doghaters", description="A place where dog haters unite.")
expected = "<None>"
result = empty_channel.wholist
self.assertEqual(expected, result)
def test_wholist_does_not_show_muted_objects(self):
self.default_channel.mute(self.obj2)
expected = "Obj"
result = self.default_channel.wholist
self.assertEqual(expected, result)
def test_wholist_shows_connected_object_as_bold(self):
self.default_channel.connect(self.char1)
expected = "Obj, |wChar|n"
result = self.default_channel.wholist
self.assertEqual(expected, result)

View file

@ -104,36 +104,34 @@ tutorial
# ... and describe it.
#
@desc
|gWelcome to the Evennia tutorial!|n
|gWelcome to the Evennia tutorial-world!|n
This small quest shows some examples of Evennia usage.
|gDo you want help with how to play? Write |yintro|g to get an introduction to
Evennia and the basics of playing!|n
To get into the mood of this miniature quest, imagine you are an adventurer
out to find fame and fortune. You have heard rumours of an old castle ruin by
the coast. In its depth a warrior princess was buried together with her
powerful magical weapon - a valuable prize, if it's true. Of course this is a
chance to adventure that you cannot turn down!
You reach the coast in the midst of a raging thunderstorm. With wind and rain
screaming in your face you stand where the moor meet the sea along a high,
rocky coast ...
Try '|yintro|n' for usage help. During the quest, write '|ytutorial|n' to get
behind-the-scenes help anywhere, and '|ygive up|n' to abandon the quest.
|gwrite 'begin' to start your quest!|n
The following tutorial consists of a small single-player quest
area. The various rooms are designed to show off some of the power
and possibilities of the Evennia mud creation system. At any time
during this tutorial you can use the |wtutorial|n (or |wtut|n)
command to get some background info about the room or certain objects
to see what is going on "behind the scenes".
To get into the mood of this miniature quest, imagine you are an
adventurer out to find fame and fortune. You have heard rumours of an
old castle ruin by the coast. In its depth a warrior princess was
buried together with her powerful magical weapon - a valuable prize,
if it's true. Of course this is a chance to adventure that you
cannot turn down!
You reach the coast in the midst of a raging thunderstorm. With wind
and rain screaming in your face you stand where the moor meet the sea
along a high, rocky coast ...
|g(write 'start' or 'begin' to start the tutorial. Try 'tutorial'
to get behind-the-scenes help anywhere.)|n
#
# Show that the tutorial command works ...
#
@set here/tutorial_info =
You just tried the tutorial command. Use it in various rooms to see
You just tried the |wtutorial|G command. Use it in various rooms to see
what's technically going on and what you could try in each room. The
intro room assigns some properties to your character, like a simple
"health" property used when fighting. Other rooms and puzzles might do
@ -294,14 +292,14 @@ start
on the sign.
# Set a climbable object for discovering a hidden exit
#
@create/drop gnarled old trees;tree;trees;gnarled : tutorial_world.objects.TutorialClimbable
@create/drop gnarled old tree;tree;trees;gnarled : tutorial_world.objects.TutorialClimbable
#
@desc trees = Only the sturdiest of trees survive at the edge of the
moor. A small group of huddling black things has dug in near the
@desc tree = Only the sturdiest of trees survive at the edge of the
moor. A small huddling black thing has dug in near the
cliff edge, eternally pummeled by wind and salt to become an integral
part of the gloomy scenery.
#
@lock trees = get:false()
@lock tree = get:false()
#
@set trees/get_err_msg =
The group of old trees have withstood the eternal wind for hundreds
@ -475,7 +473,7 @@ north
# regular exits back to the cliff, that is handled by the bridge
# typeclass itself.
#
@dig The old bridge;bridge;tut#05
@dig The old bridge;bridge;east;e;tut#05
: tutorial_world.rooms.BridgeRoom
= old bridge;east;e;bridge;hangbridge
#

View file

@ -0,0 +1,782 @@
"""
Intro menu / game tutor
Evennia contrib - Griatch 2020
This contrib is an intro-menu for general MUD and evennia usage using the
EvMenu menu-templating system.
EvMenu templating is a way to create a menu using a string-format instead
of creating all nodes manually. Of course, for full functionality one must
still create the goto-callbacks.
"""
from evennia import create_object
from evennia import CmdSet
from evennia.utils.evmenu import parse_menu_template, EvMenu
# Goto callbacks and helper resources for the menu
def do_nothing(caller, raw_string, **kwargs):
"""
Re-runs the current node
"""
return None
def send_testing_tagged(caller, raw_string, **kwargs):
"""
Test to send a message to a pane tagged with 'testing' in the webclient.
"""
caller.msg(
(
"This is a message tagged with 'testing' and "
"should appear in the pane you selected!\n "
f"You wrote: '{raw_string}'",
{"type": "testing"},
)
)
return None
# Resources for the first help-command demo
class DemoCommandSetHelp(CmdSet):
"""
Demo the help command
"""
key = "Help Demo Set"
priority = 2
def at_cmdset_creation(self):
from evennia import default_cmds
self.add(default_cmds.CmdHelp())
def goto_command_demo_help(caller, raw_string, **kwargs):
"Sets things up before going to the help-demo node"
_maintain_demo_room(caller, delete=True)
caller.cmdset.remove(DemoCommandSetRoom)
caller.cmdset.remove(DemoCommandSetComms)
caller.cmdset.add(DemoCommandSetHelp) # TODO - make persistent
return kwargs.get("gotonode") or "command_demo_help"
# Resources for the comms demo
class DemoCommandSetComms(CmdSet):
"""
Demo communications
"""
key = "Color Demo Set"
priority = 2
no_exits = True
no_objs = True
def at_cmdset_creation(self):
from evennia import default_cmds
self.add(default_cmds.CmdHelp())
self.add(default_cmds.CmdSay())
self.add(default_cmds.CmdPose())
self.add(default_cmds.CmdPage())
self.add(default_cmds.CmdColorTest())
def goto_command_demo_comms(caller, raw_string, **kwargs):
"""
Setup and go to the color demo node.
"""
caller.cmdset.remove(DemoCommandSetHelp)
caller.cmdset.remove(DemoCommandSetRoom)
caller.cmdset.add(DemoCommandSetComms)
return kwargs.get("gotonode") or "comms_demo_start"
# Resources for the room demo
_ROOM_DESC = """
This is a small and comfortable wood cabin. Bright sunlight is shining in
through the windows.
Use |ylook sign|n or |yl sign|n to examine the wooden sign nailed to the wall.
"""
_SIGN_DESC = """
The small sign reads:
Good! Now try '|ylook small|n'.
... You'll get a multi-match error! There are two things that 'small' could
refer to here - the 'small wooden sign' or the 'small, cozy cabin' itself. You will
get a list of the possibilities.
You could either tell Evennia which one you wanted by picking a unique part
of their name (like '|ylook cozy|n') or use the number in the list to pick
the one you want, like this:
|ylook 2-small|n
As long as what you write is uniquely identifying you can be lazy and not
write the full name of the thing you want to look at. Try '|ylook bo|n',
'|yl co|n' or '|yl 1-sm|n'!
... Oh, and if you see database-ids like (#1245) by the name of objects,
it's because you are playing with Builder-privileges or higher. Regular
players will not see the numbers.
Next try |ylook door|n.
"""
_DOOR_DESC_OUT = """
This is a solid wooden door leading to the outside of the cabin. Some
text is written on it:
This is an |wexit|n. An exit is often named by its compass-direction like
|weast|n, |wwest|n, |wnorthwest|n and so on, but it could be named
anything, like this door. To use the exit, you just write its name. So by
writing |ydoor|n you will leave the cabin.
"""
_DOOR_DESC_IN = """
This is a solid wooden door leading to the inside of the cabin. On
are some carved text:
This exit leads back into the cabin. An exit is just like any object,
so while has a name, it can also have aliases. To get back inside
you can both write |ydoor|n but also |yin|n.
"""
_MEADOW_DESC = """
This is a lush meadow, just outside a cozy cabin. It's surrounded
by trees and sunlight filters down from a clear blue sky.
There is a |wstone|n here. Try looking at it!
"""
_STONE_DESC = """
This is a fist-sized stone covered in runes:
To pick me up, use
|yget stone|n
You can see what you carry with the |yinventory|n (|yi|n).
To drop me again, just write
|ydrop stone|n
Use |ynext|n when you are done exploring and want to
continue with the tutorial.
"""
def _maintain_demo_room(caller, delete=False):
"""
Handle the creation/cleanup of demo assets. We store them
on the character and clean them when leaving the menu later.
"""
# this is a tuple (room, obj)
roomdata = caller.db.tutorial_world_demo_room_data
if delete:
if roomdata:
# we delete directly for simplicity. We need to delete
# in specific order to avoid deleting rooms moves
# its contents to their default home-location
prev_loc, room1, sign, room2, stone, door_out, door_in = roomdata
caller.location = prev_loc
sign.delete()
stone.delete()
door_out.delete()
door_in.delete()
room1.delete()
room2.delete()
del caller.db.tutorial_world_demo_room_data
elif not roomdata:
# create and describe the cabin and box
room1 = create_object("evennia.objects.objects.DefaultRoom", key="A small, cozy cabin")
room1.db.desc = _ROOM_DESC.lstrip()
sign = create_object(
"evennia.objects.objects.DefaultObject", key="small wooden sign", location=room1
)
sign.db.desc = _SIGN_DESC.strip()
sign.locks.add("get:false()")
sign.db.get_err_msg = "The sign is nailed to the wall. It's not budging."
# create and describe the meadow and stone
room2 = create_object("evennia.objects.objects.DefaultRoom", key="A lush summer meadow")
room2.db.desc = _MEADOW_DESC.lstrip()
stone = create_object(
"evennia.objects.objects.DefaultObject", key="carved stone", location=room2
)
stone.db.desc = _STONE_DESC.strip()
# make the linking exits
door_out = create_object(
"evennia.objects.objects.DefaultExit",
key="Door",
location=room1,
destination=room2,
locks=["get:false()"],
)
door_out.db.desc = _DOOR_DESC_OUT.strip()
door_in = create_object(
"evennia.objects.objects.DefaultExit",
key="entrance to the cabin",
aliases=["door", "in", "entrance"],
location=room2,
destination=room1,
locks=["get:false()"],
)
door_in.db.desc = _DOOR_DESC_IN.strip()
# store references for easy removal later
caller.db.tutorial_world_demo_room_data = (
caller.location,
room1,
sign,
room2,
stone,
door_out,
door_in,
)
# move caller into room
caller.location = room1
class DemoCommandSetRoom(CmdSet):
"""
Demo some general in-game commands command.
"""
key = "Room Demo Set"
priority = 2
no_exits = False
no_objs = False
def at_cmdset_creation(self):
from evennia import default_cmds
self.add(default_cmds.CmdHelp())
self.add(default_cmds.CmdLook())
self.add(default_cmds.CmdGet())
self.add(default_cmds.CmdDrop())
self.add(default_cmds.CmdInventory())
self.add(default_cmds.CmdExamine())
self.add(default_cmds.CmdPy())
def goto_command_demo_room(caller, raw_string, **kwargs):
"""
Setup and go to the demo-room node. Generates a little 2-room environment
for testing out some commands.
"""
_maintain_demo_room(caller)
caller.cmdset.remove(DemoCommandSetHelp)
caller.cmdset.remove(DemoCommandSetComms)
caller.cmdset.add(DemoCommandSetRoom)
return "command_demo_room"
def goto_cleanup_cmdsets(caller, raw_strings, **kwargs):
"""
Cleanup all cmdsets.
"""
caller.cmdset.remove(DemoCommandSetHelp)
caller.cmdset.remove(DemoCommandSetComms)
caller.cmdset.remove(DemoCommandSetRoom)
return kwargs.get("gotonode")
# register all callables that can be used in the menu template
GOTO_CALLABLES = {
"send_testing_tagged": send_testing_tagged,
"do_nothing": do_nothing,
"goto_command_demo_help": goto_command_demo_help,
"goto_command_demo_comms": goto_command_demo_comms,
"goto_command_demo_room": goto_command_demo_room,
"goto_cleanup_cmdsets": goto_cleanup_cmdsets,
}
# Main menu definition
MENU_TEMPLATE = """
## NODE start
|g** Evennia introduction wizard **|n
If you feel lost you can learn some of the basics of how to play a text-based
game here. You can also learn a little about the system and how to find more
help. You can exit this tutorial-wizard at any time by entering '|yq|n' or '|yquit|n'.
Press |y<return>|n or write |ynext|n to step forward. Or select a number to jump to.
## OPTIONS
1 (next);1;next;n: What is a MUD/MU*? -> about_muds
2: About Evennia -> about_evennia
3: Using the webclient -> using webclient
4: The help command -> goto_command_demo_help()
5: Communicating with others -> goto_command_demo_help(gotonode='talk on channels')
6: Using colors -> goto_command_demo_comms(gotonode='testing_colors')
7: Moving and exploring -> goto_command_demo_room()
8: Conclusions & next steps-> conclusions
>: about_muds
# ---------------------------------------------------------------------------------
## NODE about_muds
|g** About MUDs **|n
The term '|wMUD|n' stands for Multi-user-Dungeon or -Dimension. A MUD is
primarily played by inserting text |wcommands|n and getting text back.
MUDS were the |wprecursors|n to graphical MMORPG-style games like World of
Warcraft. While not as mainstream as they once were, comparing a text-game to a
graphical game is like comparing a book to a movie - it's just a different
experience altogether.
MUDs are |wdifferent|n from Interactive Fiction (IF) in that they are multiplayer
and usually has a consistent game world with many stories and protagonists
acting at the same time.
Like there are many different styles of graphical MMOs, there are |wmany
variations|n of MUDs: They can be slow-paced or fast. They can cover fantasy,
sci-fi, horror or other genres. They can allow PvP or not and be casual or
hardcore, strategic, tactical, turn-based or play in real-time.
Whereas 'MUD' is arguably the most well-known term, there are other terms
centered around particular game engines - such as MUSH, MOO, MUX, MUCK, LPMuds,
ROMs, Diku and others. Many people that played MUDs in the past used one of
these existing families of text game-servers, whether they knew it or not.
|cEvennia|n is a newer text game engine designed to emulate almost any existing
gaming style you like and possibly any new ones you can come up with!
## OPTIONS
next;n: About Evennia -> about_evennia
back to start;back;start;t: start
>: about_evennia
# ---------------------------------------------------------------------------------
## NODE about_evennia
|g** About Evennia **|n
|cEvennia|n is a Python game engine for creating multiplayer online text-games
(aka MUDs, MUSHes, MUX, MOOs...). It is open-source and |wfree to use|n, also for
commercial projects (BSD license).
Out of the box, Evennia provides a |wfull, if empty game|n. Whereas you can play
via traditional telnet MUD-clients, the server runs your game's website and
offers a |wHTML5 webclient|n so that people can play your game in their browser
without downloading anything extra.
Evennia deliberately |wdoes not|n hard-code any game-specific things like
combat-systems, races, skills, etc. They would not match what just you wanted
anyway! Whereas we do have optional contribs with many examples, most of our
users use them as inspiration to make their own thing.
Evennia is developed entirely in |wPython|n, using modern developer practices.
The advantage of text is that even a solo developer or small team can
realistically make a competitive multiplayer game (as compared to a graphical
MMORPG which is one of the most expensive game types in existence to develop).
Many also use Evennia as a |wfun way to learn Python|n!
## OPTIONS
next;n: Using the webclient -> using webclient
back;b: About MUDs -> about_muds
>: using webclient
# ---------------------------------------------------------------------------------
## NODE using webclient
|g** Using the Webclient **|n
|RNote: This is only relevant if you use Evennia's HTML5 web client. If you use a
third-party (telnet) mud-client, you can skip this section.|n
Evennia's web client is (for a local install) found by pointing your browser to
|yhttp://localhost:4001/webclient|n
For a live example, the public Evennia demo can be found at
|yhttps://demo.evennia.com/webclient|n
The web client starts out having two panes - the input-pane for entering commands
and the main window.
- Use |y<Return>|n (or click the arrow on the right) to send your input.
- Use |yCtrl + <up/down-arrow>|n to step back and forth in your command-history.
- Use |yCtrl + <Return>|n to add a new line to your input without sending.
(Cmd instead of Ctrl-key on Macs)
There is also some |wextra|n info to learn about customizing the webclient.
## OPTIONS
extra: Customizing the webclient -> customizing the webclient
next;n: Playing the game -> goto_command_demo_help()
back;b: About Evennia -> about_evennia
back to start;start: start
>: goto_command_demo_help()
# ---------------------------------------------------------------------------------
# this is a dead-end 'leaf' of the menu
## NODE customizing the webclient
|g** Extra hints on customizing the Webclient **|n
|y1)|n The panes of the webclient can be resized and you can create additional panes.
- Press the little plus (|w+|n) sign in the top left and a new tab will appear.
- Click and drag the tab and pull it far to the right and release when it creates two
panes next to each other.
|y2)|n You can have certain server output only appear in certain panes.
- In your new rightmost pane, click the diamond () symbol at the top.
- Unselect everything and make sure to select "testing".
- Click the diamond again so the menu closes.
- Next, write "|ytest Hello world!|n". A test-text should appear in your rightmost pane!
|y3)|n You can customize general webclient settings by pressing the cogwheel in the upper
left corner. It allows to change things like font and if the client should play sound.
The "message routing" allows for rerouting text matching a certain regular expression (regex)
to a web client pane with a specific tag that you set yourself.
|y4)|n Close the right-hand pane with the |wX|n in the rop right corner.
## OPTIONS
back;b: using webclient
> test *: send tagged message to new pane -> send_testing_tagged()
>: using webclient
# ---------------------------------------------------------------------------------
# we get here via goto_command_demo_help()
## NODE command_demo_help
|g** Playing the game **|n
Evennia has about |w90 default commands|n. They include useful administration/building
commands and a few limited "in-game" commands to serve as examples. They are intended
to be changed, extended and modified as you please.
First to try is |yhelp|n. This lists all commands |wcurrently|n available to you.
Use |yhelp <topic>|n to get specific help. Try |yhelp help|n to get help on using
the help command. For your game you could add help about your game, lore, rules etc
as well.
At the moment you only have |whelp|n and some |wChannel Names|n (the '<menu commands>'
is just a placeholder to indicate you are using this menu).
We'll add more commands as we get to them in this tutorial - but we'll only
cover a small handful. Once you exit you'll find a lot more! Now let's try
those channels ...
## OPTIONS
next;n: Talk on Channels -> talk on channels
back;b: Using the webclient -> goto_cleanup_cmdsets(gotonode='using webclient')
back to start;start: start
>: talk on channels
# ---------------------------------------------------------------------------------
## NODE talk on channels
|g** Talk on Channels **|n
|wChannels|n are like in-game chatrooms. The |wChannel Names|n help-category
holds the names of the channels available to you right now. One such channel is
|wpublic|n. Use |yhelp public|n to see how to use it. Try it:
|ypublic Hello World!|n
This will send a message to the |wpublic|n channel where everyone on that
channel can see it. If someone else is on your server, you may get a reply!
Evennia can link its in-game channels to external chat networks. This allows
you to talk with people not actually logged into the game. For
example, the online Evennia-demo links its |wpublic|n channel to the #evennia
IRC support channel.
## OPTIONS
next;n: Talk to people in-game -> goto_command_demo_comms()
back;b: Finding help -> goto_command_demo_help()
back to start;start: start
>: goto_command_demo_comms()
# ---------------------------------------------------------------------------------
# we get here via goto_command_demo_comms()
## NODE comms_demo_start
|g** Talk to people in-game **|n
You can also chat with people inside the game. If you try |yhelp|n now you'll
find you have a few more commands available for trying this out.
|ysay Hello there!|n
|y'Hello there!|n
|wsay|n is used to talk to people in the same location you are. Everyone in the
room will see what you have to say. A single quote |y'|n is a convenient shortcut.
|ypose smiles|n
|y:smiles|n
|wpose|n (or |wemote|n) describes what you do to those nearby. This is a very simple
command by default, but it can be extended to much more complex parsing in order to
include other people/objects in the emote, reference things by a short-description etc.
## OPTIONS
next;n: Paging people -> paging_people
back;b: Talk on Channels -> goto_command_demo_help(gotonode='talk on channels')
back to start;start: start
>: paging_people
# ---------------------------------------------------------------------------------
## NODE paging_people
|g** Paging people **|n
Halfway between talking on a |wChannel|n and chatting in your current location
with |wsay|n and |wpose|n, you can also |wpage|n people. This is like a private
message only they can see.
|ypage <name> = Hello there!
page <name1>, <name2> = Hello both of you!|n
If you are alone on the server, put your own name as |w<name>|n to test it and
page yourself. Write just |ypage|n to see your latest pages. This will also show
you if anyone paged you while you were offline.
(By the way - depending on which games you are used to, you may think that the
use of |y=|n above is strange. This is a MUSH/MUX-style of syntax. For your own
game you can change the |wpose|n command to work however you prefer).
## OPTIONS
next;n: Using colors -> testing_colors
back;b: Talk to people in-game -> comms_demo_start
back to start;start: start
>: testing_colors
# ---------------------------------------------------------------------------------
## NODE testing_colors
|g** U|rs|yi|gn|wg |c|yc|wo|rl|bo|gr|cs |g**|n
You can add color in your text by the help of tags. However, remember that not
everyone will see your colors - it depends on their client (and some use
screenreaders). Using color can also make text harder to read. So use it
sparingly.
To start coloring something |rred|n, add a ||r (red) marker and then
end with ||n (to go back to neutral/no-color):
|ysay This is a ||rred||n text!
say This is a ||Rdark red||n text!|n
You can also change the background:
|ysay This is a ||[x||bblue text on a light-grey background!|n
There are 16 base colors and as many background colors (called ANSI colors). Some
clients also supports so-called Xterm256 which gives a total of 256 colors. These are
given as |w||rgb|n, where r, g, b are the components of red, green and blue from 0-5:
|ysay This is ||050solid green!|n
|ysay This is ||520an orange color!|n
|ysay This is ||[005||555white on bright blue background!|n
If you don't see the expected colors from the above examples, it's because your
client does not support it - try out the Evennia webclient instead. To see all
color codes printed, try
|ycolor ansi
|ycolor xterm
## OPTIONS
next;n: Moving and Exploring -> goto_command_demo_room()
back;b: Paging people -> goto_command_demo_comms(gotonode='paging_people')
back to start;start: start
>: goto_command_demo_room()
# ---------------------------------------------------------------------------------
# we get here via goto_command_demo_room()
## NODE command_demo_room
|gMoving and Exploring|n
For exploring the game, a very important command is '|ylook|n'. It's also
abbreviated '|yl|n' since it's used so much. Looking displays/redisplays your
current location. You can also use it to look closer at items in the world. So
far in this tutorial, using 'look' would just redisplay the menu.
Try |ylook|n now. You have been quietly transported to a sunny cabin to look
around in. Explore a little and use |ynext|n when you are done.
## OPTIONS
next;n: Conclusions -> conclusions
back;b: Channel commands -> goto_command_demo_comms(gotonode='testing_colors')
back to start;start: start
>: conclusions
# ---------------------------------------------------------------------------------
## NODE conclusions
|gConclusions|n
That concludes this little quick-intro to using the base game commands of
Evennia. With this you should be able to continue exploring and also find help
if you get stuck!
Write |ynext|n to end this wizard and continue to the tutorial-world quest!
If you want there is also some |wextra|n info for where to go beyond that.
## OPTIONS
extra: Where to go next -> post scriptum
next;next;n: End -> end
back;b: Moving and Exploring -> goto_command_demo_room()
back to start;start: start
>: end
# ---------------------------------------------------------------------------------
## NODE post scriptum
|gWhere to next?|n
After playing through the tutorial-world quest, if you aim to make a game with
Evennia you are wise to take a look at the |wEvennia documentation|n at
|yhttps://github.com/evennia/evennia/wiki|n
- You can start by trying to build some stuff by following the |wBuilder quick-start|n:
|yhttps://github.com/evennia/evennia/wiki/Building-Quickstart|n
- The tutorial-world may or may not be your cup of tea, but it does show off
several |wuseful tools|n of Evennia. You may want to check out how it works:
|yhttps://github.com/evennia/evennia/wiki/Tutorial-World-Introduction|n
- You can then continue looking through the |wTutorials|n and pick one that
fits your level of understanding.
|yhttps://github.com/evennia/evennia/wiki/Tutorials|n
- Make sure to |wjoin our forum|n and connect to our |wsupport chat|n! The
Evennia community is very active and friendly and no question is too simple.
You will often quickly get help. You can everything you need linked from
|yhttp://www.evennia.com|n
# ---------------------------------------------------------------------------------
## OPTIONS
back: conclusions
>: conclusions
## NODE end
|gGood luck!|n
"""
# -------------------------------------------------------------------------------------------
#
# EvMenu implementation and access function
#
# -------------------------------------------------------------------------------------------
class TutorialEvMenu(EvMenu):
"""
Custom EvMenu for displaying the intro-menu
"""
def close_menu(self):
"""Custom cleanup actions when closing menu"""
self.caller.cmdset.remove(DemoCommandSetHelp)
self.caller.cmdset.remove(DemoCommandSetRoom)
self.caller.cmdset.remove(DemoCommandSetComms)
_maintain_demo_room(self.caller, delete=True)
super().close_menu()
def options_formatter(self, optionslist):
navigation_keys = ("next", "back", "back to start")
other = []
navigation = []
for key, desc in optionslist:
if key in navigation_keys:
desc = f" ({desc})" if desc else ""
navigation.append(f"|lc{key}|lt|w{key}|n|le{desc}")
else:
other.append((key, desc))
navigation = (
(" " + " |W|||n ".join(navigation) + " |W|||n " + "|wQ|Wuit|n") if navigation else ""
)
other = super().options_formatter(other)
sep = "\n\n" if navigation and other else ""
return f"{navigation}{sep}{other}"
def init_menu(caller):
"""
Call to initialize the menu.
"""
menutree = parse_menu_template(caller, MENU_TEMPLATE, GOTO_CALLABLES)
TutorialEvMenu(caller, menutree)

View file

@ -22,7 +22,7 @@ TutorialWeaponRack
import random
from evennia import DefaultObject, DefaultExit, Command, CmdSet
from evennia.utils import search, delay
from evennia.utils import search, delay, dedent
from evennia.prototypes.spawner import spawn
# -------------------------------------------------------------
@ -647,7 +647,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
"""called when the object is first created."""
super().at_object_creation()
self.aliases.add(["secret passage", "passage", "crack", "opening", "secret door"])
self.aliases.add(["secret passage", "passage", "crack", "opening", "secret"])
# starting root positions. H1/H2 are the horizontally hanging roots,
# V1/V2 the vertically hanging ones. Each can have three positions:
@ -688,6 +688,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
# start a 45 second timer before closing again. We store the deferred so it can be
# killed in unittesting.
self.deferred = delay(45, self.reset)
return True
def _translate_position(self, root, ipos):
"""Translates the position into words"""
@ -740,7 +741,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
"The wall is old and covered with roots that here and there have permeated the stone. "
"The roots (or whatever they are - some of them are covered in small nondescript flowers) "
"crisscross the wall, making it hard to clearly see its stony surface. Maybe you could "
"try to |wshift|n or |wmove|n them.\n"
"try to |wshift|n or |wmove|n them (like '|wshift red up|n').\n"
]
# display the root positions to help with the puzzle
for key, pos in self.db.root_pos.items():
@ -833,6 +834,7 @@ class CmdAttack(Command):
"stab",
"slash",
"chop",
"bash",
"parry",
"defend",
]
@ -875,7 +877,7 @@ class CmdAttack(Command):
tstring = "%s stabs at you with %s. " % (self.caller.key, self.obj.key)
ostring = "%s stabs at %s with %s. " % (self.caller.key, target.key, self.obj.key)
self.caller.db.combat_parry_mode = False
elif cmdstring in ("slash", "chop"):
elif cmdstring in ("slash", "chop", "bash"):
hit = float(self.obj.db.hit) # un modified due to slash
damage = self.obj.db.damage # un modified due to slash
string = "You slash with %s. " % self.obj.key
@ -1150,7 +1152,14 @@ class TutorialWeaponRack(TutorialObject):
self.db.rack_id = "weaponrack_1"
# these are prototype names from the prototype
# dictionary above.
self.db.get_weapon_msg = "You find |c%s|n."
self.db.get_weapon_msg = dedent(
"""
You find |c%s|n. While carrying this weapon, these actions are available:
|wstab/thrust/pierce <target>|n - poke at the enemy. More damage but harder to hit.
|wslash/chop/bash <target>|n - swipe at the enemy. Less damage but easier to hit.
|wdefend/parry|n - protect yourself and make yourself harder to hit.)
""").strip()
self.db.no_more_weapons_msg = "you find nothing else of use."
self.db.available_weapons = ["knife", "dagger", "sword", "club"]

View file

@ -69,12 +69,14 @@ class CmdTutorial(Command):
target = caller.search(self.args.strip())
if not target:
return
helptext = target.db.tutorial_info
if helptext:
caller.msg("|G%s|n" % helptext)
else:
caller.msg("|RSorry, there is no tutorial help available here.|n")
helptext = target.db.tutorial_info or ""
if helptext:
helptext = f" |G{helptext}|n"
else:
helptext = " |RSorry, there is no tutorial help available here.|n"
helptext += "\n\n (Write 'give up' if you want to abandon your quest.)"
caller.msg(helptext)
# for the @detail command we inherit from MuxCommand, since
# we want to make use of MuxCommand's pre-parsing of '=' in the
@ -200,6 +202,26 @@ class CmdTutorialLook(default_cmds.CmdLook):
looking_at_obj.at_desc(looker=caller)
return
class CmdTutorialGiveUp(default_cmds.MuxCommand):
"""
Give up the tutorial-world quest and return to Limbo, the start room of the
server.
"""
key = "give up"
aliases = ['abort']
def func(self):
outro_room = OutroRoom.objects.all()
if outro_room:
outro_room = outro_room[0]
else:
self.caller.msg("That didn't work (seems like a bug). "
"Try to use the |wteleport|n command instead.")
return
self.caller.move_to(outro_room)
class TutorialRoomCmdSet(CmdSet):
"""
@ -216,6 +238,7 @@ class TutorialRoomCmdSet(CmdSet):
self.add(CmdTutorial())
self.add(CmdTutorialSetDetail())
self.add(CmdTutorialLook())
self.add(CmdTutorialGiveUp())
class TutorialRoom(DefaultRoom):
@ -362,6 +385,31 @@ SUPERUSER_WARNING = (
#
# -------------------------------------------------------------
class CmdEvenniaIntro(Command):
"""
Start the Evennia intro wizard.
Usage:
intro
"""
key = "intro"
def func(self):
from .intro_menu import init_menu
# quell also superusers
if self.caller.account:
self.caller.account.execute_cmd("quell")
self.caller.msg("(Auto-quelling)")
init_menu(self.caller)
class CmdSetEvenniaIntro(CmdSet):
key = "Evennia Intro StartSet"
def at_cmdset_creation(self):
self.add(CmdEvenniaIntro())
class IntroRoom(TutorialRoom):
"""
@ -381,6 +429,7 @@ class IntroRoom(TutorialRoom):
"This assigns the health Attribute to "
"the account."
)
self.cmdset.add(CmdSetEvenniaIntro, permanent=True)
def at_object_receive(self, character, source_location):
"""
@ -396,8 +445,12 @@ class IntroRoom(TutorialRoom):
if character.is_superuser:
string = "-" * 78 + SUPERUSER_WARNING + "-" * 78
character.msg("|r%s|n" % string.format(name=character.key, quell="|w@quell|r"))
character.msg("|r%s|n" % string.format(name=character.key, quell="|wquell|r"))
else:
# quell user
if character.account:
character.account.execute_cmd("quell")
character.msg("(Auto-quelling while in tutorial-world)")
# -------------------------------------------------------------
#
@ -617,7 +670,7 @@ class BridgeCmdSet(CmdSet):
"""This groups the bridge commands. We will store it on the room."""
key = "Bridge commands"
priority = 1 # this gives it precedence over the normal look/help commands.
priority = 2 # this gives it precedence over the normal look/help commands.
def at_cmdset_creation(self):
"""Called at first cmdset creation"""
@ -679,7 +732,7 @@ class BridgeRoom(WeatherRoom):
self.db.east_exit = "gate"
self.db.fall_exit = "cliffledge"
# add the cmdset on the room.
self.cmdset.add_default(BridgeCmdSet)
self.cmdset.add(BridgeCmdSet, permanent=True)
# since the default Character's at_look() will access the room's
# return_description (this skips the cmdset) when
# first entering it, we need to explicitly turn off the room
@ -1108,3 +1161,8 @@ class OutroRoom(TutorialRoom):
if obj.typeclass_path.startswith("evennia.contrib.tutorial_world"):
obj.delete()
character.tags.clear(category="tutorial_world")
def at_object_leave(self, character, destination):
if character.account:
character.account.execute_cmd("unquell")

View file

@ -1981,12 +1981,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# whisper mode
msg_type = "whisper"
msg_self = (
'{self} whisper to {all_receivers}, "{speech}"' if msg_self is True else msg_self
'{self} whisper to {all_receivers}, "|n{speech}|n"' if msg_self is True else msg_self
)
msg_receivers = msg_receivers or '{object} whispers: "{speech}"'
msg_receivers = msg_receivers or '{object} whispers: "|n{speech}|n"'
msg_location = None
else:
msg_self = '{self} say, "{speech}"' if msg_self is True else msg_self
msg_self = '{self} say, "|n{speech}|n"' if msg_self is True else msg_self
msg_location = msg_location or '{object} says, "{speech}"'
msg_receivers = msg_receivers or message

View file

@ -9,7 +9,7 @@ There main function is `spawn(*prototype)`, where the `prototype`
is a dictionary like this:
```python
from evennia.prototypes import prototypes
from evennia.prototypes import prototypes, spawner
prot = {
"prototype_key": "goblin",
@ -22,7 +22,10 @@ prot = {
"tags": ["mob", "evil", ('greenskin','mob')]
"attrs": [("weapon", "sword")]
}
# spawn something with the prototype
goblin = spawner.spawn(prot)
# make this into a db-saved prototype (optional)
prot = prototypes.create_prototype(prot)
```
@ -82,13 +85,13 @@ import random
{
"prototype_key": "goblin_wizard",
"prototype_parent": GOBLIN,
"prototype_parent": "GOBLIN",
"key": "goblin wizard",
"spells": ["fire ball", "lighting bolt"]
}
GOBLIN_ARCHER = {
"prototype_parent": GOBLIN,
"prototype_parent": "GOBLIN",
"key": "goblin archer",
"attack_skill": (random, (5, 10))"
"attacks": ["short bow"]
@ -104,7 +107,7 @@ ARCHWIZARD = {
GOBLIN_ARCHWIZARD = {
"key" : "goblin archwizard"
"prototype_parent": (GOBLIN_WIZARD, ARCHWIZARD),
"prototype_parent": ("GOBLIN_WIZARD", "ARCHWIZARD"),
}
```

View file

@ -29,7 +29,7 @@ LIMBO_DESC = _(
"""
Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if you need
help, want to contribute, report issues or just join the community.
As Account #1 you can create a demo/tutorial area with |w@batchcommand tutorial_world.build|n.
As Account #1 you can create a demo/tutorial area with '|wbatchcommand tutorial_world.build|n'.
"""
)

View file

@ -397,7 +397,7 @@ if WEBSERVER_ENABLED:
w_interface = WEBSOCKET_CLIENT_INTERFACE
w_ifacestr = ""
if w_interface not in ("0.0.0.0", "::") or len(WEBSERVER_INTERFACES) > 1:
w_ifacestr = "-%s" % interface
w_ifacestr = "-%s" % w_interface
port = WEBSOCKET_CLIENT_PORT
class Websocket(WebSocketServerFactory):

View file

@ -239,16 +239,16 @@ class PortalSessionHandler(SessionHandler):
def server_connect(self, protocol_path="", config=dict()):
"""
Called by server to force the initialization of a new protocol
instance. Server wants this instance to get a unique sessid
and to be connected back as normal. This is used to initiate
irc/rss etc connections.
instance. Server wants this instance to get a unique sessid and to be
connected back as normal. This is used to initiate irc/rss etc
connections.
Args:
protocol_path (st): Full python path to the class factory
protocol_path (str): Full python path to the class factory
for the protocol used, eg
'evennia.server.portal.irc.IRCClientFactory'
config (dict): Dictionary of configuration options, fed as
`**kwargs` to protocol class' __init__ method.
`**kwarg` to protocol class `__init__` method.
Raises:
RuntimeError: If The correct factory class is not found.

View file

@ -20,9 +20,7 @@ This implements the following telnet OOB communication protocols:
- GMCP (Generic Mud Communication Protocol) as per
http://www.ironrealms.com/rapture/manual/files/FeatGMCP-txt.html#Generic_MUD_Communication_Protocol%28GMCP%29
Following the lead of KaVir's protocol snippet, we first check if
client supports MSDP and if not, we fallback to GMCP with a MSDP
header where applicable.
----
"""
import re

View file

@ -182,6 +182,16 @@ class AjaxWebClient(resource.Resource):
csessid = self.get_client_sessid(request)
remote_addr = request.getClientIP()
if remote_addr in settings.UPSTREAM_IPS and request.getHeader("x-forwarded-for"):
addresses = [x.strip() for x in request.getHeader("x-forwarded-for").split(",")]
addresses.reverse()
for addr in addresses:
if addr not in settings.UPSTREAM_IPS:
remote_addr = addr
break
host_string = "%s (%s:%s)" % (
_SERVERNAME,
request.getRequestHostname(),

View file

@ -52,6 +52,8 @@ TIMESTEP with a chance given by CHANCE_OF_ACTION by in the order given
(no randomness) and allows for setting up a more complex chain of
commands (such as creating an account and logging in).
----
"""
# Dummy runner settings

View file

@ -57,7 +57,7 @@ both one or two arguments interchangeably. It also accepts nodes
that takes `**kwargs`.
The menu tree itself is available on the caller as
`caller.ndb._menutree`. This makes it a convenient place to store
`caller.ndb._evmenu`. This makes it a convenient place to store
temporary state variables between nodes, since this NAttribute is
deleted when the menu is exited.
@ -165,11 +165,114 @@ your default cmdset. Run it with this module, like `testmenu evennia.utils.evmen
----
## Menu generation from template string
In evmenu.py is a helper function `parse_menu_template` that parses a
template-string and outputs a menu-tree dictionary suitable to pass into
EvMenu:
::
menutree = evmenu.parse_menu_template(caller, menu_template, goto_callables)
EvMenu(caller, menutree)
For maximum flexibility you can inject normally-created nodes in the menu tree
before passing it to EvMenu. If that's not needed, you can also create a menu
in one step with:
::
evmenu.template2menu(caller, menu_template, goto_callables)
The `goto_callables` is a mapping `{"funcname": callable, ...}`, where each
callable must be a module-global function on the form
`funcname(caller, raw_string, **kwargs)` (like any goto-callable). The
`menu_template` is a multi-line string on the following form:
::
## node start
This is the text of the start node.
The text area can have multiple lines, line breaks etc.
Each option below is one of these forms
key: desc -> gotostr_or_func
key: gotostr_or_func
>: gotostr_or_func
> glob/regex: gotostr_or_func
## options
# comments are only allowed from beginning of line.
# Indenting is not necessary, but good for readability
1: Option number 1 -> node1
2: Option number 2 -> node2
next: This steps next -> go_back()
# the -> can be ignored if there is no desc
back: go_back(from_node=start)
abort: abort
## node node1
Text for Node1. Enter a message!
<return> to go back.
## options
# Starting the option-line with >
# allows to perform different actions depending on
# what is inserted.
# this catches everything starting with foo
> foo*: handle_foo_message()
# regex are also allowed (this catches number inputs)
> [0-9]+?: handle_numbers()
# this catches the empty return
>: start
# this catches everything else
> *: handle_message(from_node=node1)
## node node2
Text for Node2. Just go back.
## options
>: start
# node abort
This exits the menu since there is no `## options` section.
Each menu node is defined by a `# node <name>` containing the text of the node,
followed by `## options` Also `## NODE` and `## OPTIONS` work. No python code
logics is allowed in the template, this code is not evaluated but parsed. More
advanced dynamic usage requires a full node-function (which can be added to the
generated dict, as said).
Adding `(..)` to a goto treats it as a callable and it must then be included in
the `goto_callable` mapping. Only named keywords (or no args at all) are
allowed, these will be added to the `**kwargs` going into the callable. Quoting
strings is only needed if wanting to pass strippable spaces, otherwise the
key:values will be converted to strings/numbers with literal_eval before passed
into the callable.
The `> ` option takes a glob or regex to perform different actions depending on user
input. Make sure to sort these in increasing order of generality since they
will be tested in sequence.
"""
import random
import re
import inspect
from ast import literal_eval
from fnmatch import fnmatch
from inspect import isfunction, getargspec
from django.conf import settings
from evennia import Command, CmdSet
@ -179,6 +282,9 @@ from evennia.utils.ansi import strip_ansi
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
from evennia.commands import cmdhandler
# i18n
from django.utils.translation import gettext as _
# read from protocol NAWS later?
_MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
@ -189,11 +295,10 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
# Return messages
# i18n
from django.utils.translation import gettext as _
_ERR_NOT_IMPLEMENTED = _(
"Menu node '{nodename}' is either not implemented or " "caused an error. Make another choice."
"Menu node '{nodename}' is either not implemented or caused an error. "
"Make another choice or try 'q' to abort."
)
_ERR_GENERAL = _("Error in menu node '{nodename}'.")
_ERR_NO_OPTION_DESC = _("No description.")
@ -227,6 +332,19 @@ class EvMenuError(RuntimeError):
pass
class EvMenuGotoAbortMessage(RuntimeError):
"""
This can be raised by a goto-callable to abort the goto flow. The message
stored with the executable will be sent to the caller who will remain on
the current node. This can be used to pass single-line returns without
re-running the entire node with text and options.
Example:
raise EvMenuGotoMessage("That makes no sense.")
"""
# -------------------------------------------------------------
#
# Menu command and command set
@ -243,6 +361,10 @@ class CmdEvMenuNode(Command):
aliases = [_CMD_NOMATCH]
locks = "cmd:all()"
help_category = "Menu"
auto_help_display_key = "<menu commands>"
def get_help(self):
return "Menu commands are explained within the menu."
def func(self):
"""
@ -271,28 +393,28 @@ class CmdEvMenuNode(Command):
caller = self.caller
# we store Session on the menu since this can be hard to
# get in multisession environemtns if caller is an Account.
menu = caller.ndb._menutree
menu = caller.ndb._evmenu
if not menu:
if _restore(caller):
return
orig_caller = caller
caller = caller.account if hasattr(caller, "account") else None
menu = caller.ndb._menutree if caller else None
menu = caller.ndb._evmenu if caller else None
if not menu:
if caller and _restore(caller):
return
caller = self.session
menu = caller.ndb._menutree
menu = caller.ndb._evmenu
if not menu:
# can't restore from a session
err = "Menu object not found as %s.ndb._menutree!" % orig_caller
err = "Menu object not found as %s.ndb._evmenu!" % orig_caller
orig_caller.msg(
err
) # don't give the session as a kwarg here, direct to original
raise EvMenuError(err)
# we must do this after the caller with the menu has been correctly identified since it
# can be either Account, Object or Session (in the latter case this info will be superfluous).
caller.ndb._menutree._session = self.session
caller.ndb._evmenu._session = self.session
# we have a menu, use it.
menu.parse_input(self.raw_string)
@ -324,7 +446,7 @@ class EvMenuCmdSet(CmdSet):
# -------------------------------------------------------------
class EvMenu(object):
class EvMenu:
"""
This object represents an operational menu. It is initialized from
a menufile.py instruction.
@ -425,9 +547,9 @@ class EvMenu(object):
EvMenuError: If the start/end node is not found in menu tree.
Notes:
While running, the menu is stored on the caller as `caller.ndb._menutree`. Also
While running, the menu is stored on the caller as `caller.ndb._evmenu`. Also
the current Session (from the Command, so this is still valid in multisession
environments) is available through `caller.ndb._menutree._session`. The `_menutree`
environments) is available through `caller.ndb._evmenu._session`. The `_evmenu`
property is a good one for storing intermediary data on between nodes since it
will be automatically deleted when the menu closes.
@ -478,7 +600,7 @@ class EvMenu(object):
self.test_nodetext = ""
# assign kwargs as initialization vars on ourselves.
if set(
reserved_clash = set(
(
"_startnode",
"_menutree",
@ -492,22 +614,26 @@ class EvMenu(object):
"cmdset_mergetype",
"auto_quit",
)
).intersection(set(kwargs.keys())):
).intersection(set(kwargs.keys()))
if reserved_clash:
raise RuntimeError(
"One or more of the EvMenu `**kwargs` is reserved by EvMenu for internal use."
f"One or more of the EvMenu `**kwargs` ({list(reserved_clash)}) is reserved by EvMenu for internal use."
)
for key, val in kwargs.items():
setattr(self, key, val)
if self.caller.ndb._menutree:
if self.caller.ndb._evmenu:
# an evmenu already exists - we try to close it cleanly. Note that this will
# not fire the previous menu's end node.
try:
self.caller.ndb._menutree.close_menu()
self.caller.ndb._evmenu.close_menu()
except Exception:
pass
# store ourself on the object
self.caller.ndb._evmenu = self
# DEPRECATED - for backwards-compatibility
self.caller.ndb._menutree = self
if persistent:
@ -527,7 +653,7 @@ class EvMenu(object):
caller.attributes.add("_menutree_saved", (self.__class__, (menudata,), calldict))
caller.attributes.add("_menutree_saved_startnode", (startnode, startnode_input))
except Exception as err:
caller.msg(_ERROR_PERSISTENT_SAVING.format(error=err), session=self._session)
self.msg(_ERROR_PERSISTENT_SAVING.format(error=err))
logger.log_trace(_TRACE_PERSISTENT_SAVING)
persistent = False
@ -537,11 +663,19 @@ class EvMenu(object):
menu_cmdset.priority = int(cmdset_priority)
self.caller.cmdset.add(menu_cmdset, permanent=persistent)
reserved_startnode_kwargs = set(("nodename", "raw_string"))
startnode_kwargs = {}
if isinstance(startnode_input, (tuple, list)) and len(startnode_input) > 1:
startnode_input, startnode_kwargs = startnode_input[:2]
if not isinstance(startnode_kwargs, dict):
raise EvMenuError("startnode_input must be either a str or a tuple (str, dict).")
clashing_kwargs = reserved_startnode_kwargs.intersection(set(startnode_kwargs.keys()))
if clashing_kwargs:
raise RuntimeError(
f"Evmenu startnode_inputs includes kwargs {tuple(clashing_kwargs)} that "
"clashes with EvMenu's internal usage."
)
# start the menu
self.goto(self._startnode, startnode_input, **startnode_kwargs)
@ -631,7 +765,7 @@ class EvMenu(object):
ret = callback(self.caller)
except EvMenuError:
errmsg = _ERR_GENERAL.format(nodename=callback)
self.caller.msg(errmsg, self._session)
self.msg(errmsg)
logger.log_trace()
raise
@ -656,20 +790,21 @@ class EvMenu(object):
try:
node = self._menutree[nodename]
except KeyError:
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
self.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename))
raise EvMenuError
try:
kwargs["_current_nodename"] = nodename
ret = self._safe_call(node, raw_string, **kwargs)
if isinstance(ret, (tuple, list)) and len(ret) > 1:
nodetext, options = ret[:2]
else:
nodetext, options = ret, None
except KeyError:
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
self.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename))
logger.log_trace()
raise EvMenuError
except Exception:
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
self.msg(_ERR_GENERAL.format(nodename=nodename))
logger.log_trace()
raise
@ -679,8 +814,27 @@ class EvMenu(object):
return nodetext, options
def msg(self, txt):
"""
This is a central point for sending return texts to the caller. It
allows for a central point to add custom messaging when creating custom
EvMenu overrides.
Args:
txt (str): The text to send.
Notes:
By default this will send to the same session provided to EvMenu
(if `session` kwarg was provided to `EvMenu.__init__`). It will
also send it with a `type=menu` for the benefit of OOB/webclient.
"""
self.caller.msg(text=(txt, {"type": "menu"}), session=self._session)
def run_exec(self, nodename, raw_string, **kwargs):
"""
NOTE: This is deprecated. Use `goto` directly instead.
Run a function or node as a callback (with the 'exec' option key).
Args:
@ -723,7 +877,7 @@ class EvMenu(object):
ret, kwargs = ret[:2]
except EvMenuError as err:
errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string.rstrip(), err)
self.caller.msg("|r%s|n" % errmsg)
self.msg("|r%s|n" % errmsg)
logger.log_trace(errmsg)
return
@ -904,12 +1058,14 @@ class EvMenu(object):
# avoid multiple calls from different sources
self._quitting = True
self.caller.cmdset.remove(EvMenuCmdSet)
del self.caller.ndb._menutree
del self.caller.ndb._evmenu
if self._persistent:
self.caller.attributes.remove("_menutree_saved")
self.caller.attributes.remove("_menutree_saved_startnode")
if self.cmd_on_exit is not None:
self.cmd_on_exit(self.caller, self)
# special for template-generated menues
del self.caller.db._evmenu_template_contents
def print_debug_info(self, arg):
"""
@ -968,7 +1124,7 @@ class EvMenu(object):
)
+ "\n |y... END MENU DEBUG|n"
)
self.caller.msg(debugtxt)
self.msg(debugtxt)
def parse_input(self, raw_string):
"""
@ -985,30 +1141,35 @@ class EvMenu(object):
"""
cmd = strip_ansi(raw_string.strip().lower())
if cmd in self.options:
# this will take precedence over the default commands
# below
goto, goto_kwargs, execfunc, exec_kwargs = self.options[cmd]
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
elif self.auto_look and cmd in ("look", "l"):
self.display_nodetext()
elif self.auto_help and cmd in ("help", "h"):
self.display_helptext()
elif self.auto_quit and cmd in ("quit", "q", "exit"):
self.close_menu()
elif self.debug_mode and cmd.startswith("menudebug"):
self.print_debug_info(cmd[9:].strip())
elif self.default:
goto, goto_kwargs, execfunc, exec_kwargs = self.default
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
else:
self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session)
try:
if self.options and cmd in self.options:
# this will take precedence over the default commands
# below
goto, goto_kwargs, execfunc, exec_kwargs = self.options[cmd]
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
elif self.auto_look and cmd in ("look", "l"):
self.display_nodetext()
elif self.auto_help and cmd in ("help", "h"):
self.display_helptext()
elif self.auto_quit and cmd in ("quit", "q", "exit"):
self.close_menu()
elif self.debug_mode and cmd.startswith("menudebug"):
self.print_debug_info(cmd[9:].strip())
elif self.default:
goto, goto_kwargs, execfunc, exec_kwargs = self.default
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
else:
self.msg(_HELP_NO_OPTION_MATCH)
except EvMenuGotoAbortMessage as err:
# custom interrupt from inside a goto callable - print the message and
# stay on the current node.
self.msg(str(err))
def display_nodetext(self):
self.caller.msg(self.nodetext, session=self._session)
self.msg(self.nodetext)
def display_helptext(self):
self.caller.msg(self.helptext, session=self._session)
self.msg(self.helptext)
# formatters - override in a child class
@ -1460,219 +1621,288 @@ def get_input(caller, prompt, callback, session=None, *args, **kwargs):
# -------------------------------------------------------------
#
# test menu strucure and testing command
# Menu generation from menu template string
#
# -------------------------------------------------------------
_RE_NODE = re.compile(r"##\s*?NODE\s+?(?P<nodename>\S[\S\s]*?)$", re.I + re.M)
_RE_OPTIONS_SEP = re.compile(r"##\s*?OPTIONS\s*?$", re.I + re.M)
_RE_CALLABLE = re.compile(r"\S+?\(\)", re.I + re.M)
_RE_CALLABLE = re.compile(
r"(?P<funcname>\S+?)(?:\((?P<kwargs>[\S\s]+?)\)|\(\))", re.I + re.M
)
def _generate_goto(caller, **kwargs):
return kwargs.get("name", "test_dynamic_node"), {"name": "replaced!"}
_HELP_NO_OPTION_MATCH = _("Choose an option or try 'help'.")
_OPTION_INPUT_MARKER = ">"
_OPTION_ALIAS_MARKER = ";"
_OPTION_SEP_MARKER = ":"
_OPTION_CALL_MARKER = "->"
_OPTION_COMMENT_START = "#"
def test_start_node(caller):
menu = caller.ndb._menutree
text = """
This is an example menu.
# Input/option/goto handler functions that allows for dynamically generated
# nodes read from the menu template.
If you enter anything except the valid options, your input will be
recorded and you will be brought to a menu entry showing your
input.
def _process_callable(caller, goto, goto_callables, raw_string,
current_nodename, kwargs):
"""
Central helper for parsing a goto-callable (`funcname(**kwargs)`) out of
the right-hand-side of the template options and map this to an actual
callable registered with the template generator. This involves parsing the
func-name and running literal-eval on its kwargs.
Select options or use 'quit' to exit the menu.
"""
match = _RE_CALLABLE.match(goto)
if match:
gotofunc = match.group("funcname")
gotokwargs = match.group("kwargs") or ""
if gotofunc in goto_callables:
for kwarg in gotokwargs.split(","):
if kwarg and "=" in kwarg:
key, value = [part.strip() for part in kwarg.split("=", 1)]
if key in ("evmenu_goto", "evmenu_gotomap", "_current_nodename",
"evmenu_current_nodename", "evmenu_goto_callables"):
raise RuntimeError(
f"EvMenu template error: goto-callable '{goto}' uses a "
f"kwarg ({kwarg}) that is reserved for the EvMenu templating "
"system. Rename the kwarg.")
try:
key = literal_eval(key)
except ValueError:
pass
try:
value = literal_eval(value)
except ValueError:
pass
kwargs[key] = value
The menu was initialized with two variables: %s and %s.
""" % (
menu.testval,
menu.testval2,
)
goto = goto_callables[gotofunc](caller, raw_string, **kwargs)
if goto is None:
return goto, {"generated_nodename": current_nodename}
return goto, {"generated_nodename": goto}
options = (
{
"key": ("|yS|net", "s"),
"desc": "Set an attribute on yourself.",
"exec": lambda caller: caller.attributes.add("menuattrtest", "Test value"),
"goto": "test_set_node",
},
{
"key": ("|yL|nook", "l"),
"desc": "Look and see a custom message.",
"goto": "test_look_node",
},
{"key": ("|yV|niew", "v"), "desc": "View your own name", "goto": "test_view_node"},
{
"key": ("|yD|nynamic", "d"),
"desc": "Dynamic node",
"goto": (_generate_goto, {"name": "test_dynamic_node"}),
},
{
"key": ("|yQ|nuit", "quit", "q", "Q"),
"desc": "Quit this menu example.",
"goto": "test_end_node",
},
{"key": "_default", "goto": "test_displayinput_node"},
)
def _generated_goto_func(caller, raw_string, **kwargs):
"""
This rerouter handles normal direct goto func call matches.
key : ... -> goto_callable(**kwargs)
"""
goto = kwargs["evmenu_goto"]
goto_callables = kwargs["evmenu_goto_callables"]
current_nodename = kwargs["evmenu_current_nodename"]
return _process_callable(caller, goto, goto_callables, raw_string,
current_nodename, kwargs)
def _generated_input_goto_func(caller, raw_string, **kwargs):
"""
This goto-func acts as a rerouter for >-type line parsing (by acting as the
_default option). The patterns discovered in the menu maps to different
*actual* goto-funcs. We map to those here.
>pattern: ... -> goto_callable
"""
gotomap = kwargs["evmenu_gotomap"]
goto_callables = kwargs["evmenu_goto_callables"]
current_nodename = kwargs["evmenu_current_nodename"]
raw_string = raw_string.strip("\n") # strip is necessary to catch empty return
# start with glob patterns
for pattern, goto in gotomap.items():
if fnmatch(raw_string.lower(), pattern):
return _process_callable(caller, goto, goto_callables, raw_string,
current_nodename, kwargs)
# no glob pattern match; try regex
for pattern, goto in gotomap.items():
if pattern and re.match(pattern, raw_string.lower(), flags=re.I + re.M):
return _process_callable(caller, goto, goto_callables, raw_string,
current_nodename, kwargs)
# no match, show error
raise EvMenuGotoAbortMessage(_HELP_NO_OPTION_MATCH)
def _generated_node(caller, raw_string, **kwargs):
"""
Every node in the templated menu will be this node, but with dynamically
changing text/options. It must be a global function like this because
otherwise we could not make the templated-menu persistent.
"""
text, options = caller.db._evmenu_template_contents[kwargs["_current_nodename"]]
return text, options
def test_look_node(caller):
text = "This is a custom look location!"
options = {
"key": ("|yL|nook", "l"),
"desc": "Go back to the previous menu.",
"goto": "test_start_node",
}
return text, options
def parse_menu_template(caller, menu_template, goto_callables=None):
"""
Parse menu-template string. The main function of the EvMenu templating system.
Args:
caller (Object or Account): Entity using the menu.
menu_template (str): Menu described using the templating format.
goto_callables (dict, optional): Mapping between call-names and callables
on the form `callable(caller, raw_string, **kwargs)`. These are what is
available to use in the `menu_template` string.
def test_set_node(caller):
text = (
Returns:
dict: A `{"node": nodefunc}` menutree suitable to pass into EvMenu.
"""
def _validate_kwarg(goto, kwarg):
"""
The attribute 'menuattrtest' was set to
|w%s|n
(check it with examine after quitting the menu).
This node's has only one option, and one of its key aliases is the
string "_default", meaning it will catch any input, in this case
to return to the main menu. So you can e.g. press <return> to go
back now.
"""
% caller.db.menuattrtest, # optional help text for this node
Validate goto-callable kwarg is on correct form.
"""
This is the help entry for this node. It is created by returning
the node text as a tuple - the second string in that tuple will be
used as the help text.
""",
)
if not "=" in kwarg:
raise RuntimeError(
f"EvMenu template error: goto-callable '{goto}' has a "
f"non-kwarg argument ({kwarg}). All callables in the "
"template must have only keyword-arguments, or no "
"args at all.")
key, _ = [part.strip() for part in kwarg.split("=", 1)]
if key in ("evmenu_goto", "evmenu_gotomap", "_current_nodename",
"evmenu_current_nodename", "evmenu_goto_callables"):
raise RuntimeError(
f"EvMenu template error: goto-callable '{goto}' uses a "
f"kwarg ({kwarg}) that is reserved for the EvMenu templating "
"system. Rename the kwarg.")
options = {"key": ("back (default)", "_default"), "goto": "test_start_node"}
return text, options
def test_view_node(caller, **kwargs):
text = (
def _parse_options(nodename, optiontxt, goto_callables):
"""
Your name is |g%s|n!
click |lclook|lthere|le to trigger a look command under MXP.
This node's option has no explicit key (nor the "_default" key
set), and so gets assigned a number automatically. You can infact
-always- use numbers (1...N) to refer to listed options also if you
don't see a string option key (try it!).
"""
% caller.key
)
if kwargs.get("executed_from_dynamic_node", False):
# we are calling this node as a exec, skip return values
caller.msg("|gCalled from dynamic node:|n \n {}".format(text))
return
else:
options = {"desc": "back to main", "goto": "test_start_node"}
return text, options
def test_displayinput_node(caller, raw_string):
text = (
Parse option section into option dict.
"""
You entered the text:
options = []
optiontxt = optiontxt[0].strip() if optiontxt else ""
optionlist = [optline.strip() for optline in optiontxt.split("\n")]
inputparsemap = {}
"|w%s|n"
for inum, optline in enumerate(optionlist):
if optline.startswith(_OPTION_COMMENT_START) or _OPTION_SEP_MARKER not in optline:
# skip comments or invalid syntax
continue
key = ""
desc = ""
pattern = None
... which could now be handled or stored here in some way if this
was not just an example.
key, goto = [part.strip() for part in optline.split(_OPTION_SEP_MARKER, 1)]
This node has an option with a single alias "_default", which
makes it hidden from view. It catches all input (except the
in-menu help/quit commands) and will, in this case, bring you back
to the start node.
# desc -> goto
if _OPTION_CALL_MARKER in goto:
desc, goto = [part.strip() for part in goto.split(_OPTION_CALL_MARKER, 1)]
# validate callable
match = _RE_CALLABLE.match(goto)
if match:
kwargs = match.group("kwargs")
if kwargs:
for kwarg in kwargs.split(','):
_validate_kwarg(goto, kwarg)
# parse key [;aliases|pattern]
key = [part.strip() for part in key.split(_OPTION_ALIAS_MARKER)]
if not key:
# fall back to this being the Nth option
key = [f"{inum + 1}"]
main_key = key[0]
if main_key.startswith(_OPTION_INPUT_MARKER):
# if we have a pattern, build the arguments for _default later
pattern = main_key[len(_OPTION_INPUT_MARKER):].strip()
inputparsemap[pattern] = goto
else:
# a regular goto string/callable target
option = {
"key": key,
"goto": (
_generated_goto_func,
{
"evmenu_goto": goto,
"evmenu_current_nodename": nodename,
"evmenu_goto_callables": goto_callables,
},
),
}
if desc:
option["desc"] = desc
options.append(option)
if inputparsemap:
# if this exists we must create a _default entry too
options.append(
{
"key": "_default",
"goto": (
_generated_input_goto_func,
{
"evmenu_gotomap": inputparsemap,
"evmenu_current_nodename": nodename,
"evmenu_goto_callables": goto_callables,
},
),
}
)
return options
def _parse(caller, menu_template, goto_callables):
"""
Parse the menu string format into a node tree.
"""
nodetree = {}
splits = _RE_NODE.split(menu_template)
splits = splits[1:] if splits else []
# from evennia import set_trace;set_trace(term_size=(140,120))
content_map = {}
for node_ind in range(0, len(splits), 2):
nodename, nodetxt = splits[node_ind], splits[node_ind + 1]
text, *optiontxt = _RE_OPTIONS_SEP.split(nodetxt, maxsplit=2)
options = _parse_options(nodename, optiontxt, goto_callables)
content_map[nodename] = (text, options)
nodetree[nodename] = _generated_node
caller.db._evmenu_template_contents = content_map
return nodetree
return _parse(caller, menu_template, goto_callables)
def template2menu(
caller,
menu_template,
goto_callables=None,
startnode="start",
persistent=False,
**kwargs,
):
"""
% raw_string.rstrip()
)
options = {"key": "_default", "goto": "test_start_node"}
return text, options
Helper function to generate and start an EvMenu based on a menu template
string. This will internall call `parse_menu_template` and run a default
EvMenu with its results.
Args:
caller (Object or Account): The entity using the menu.
menu_template (str): The menu-template string describing the content
and structure of the menu. It can also be the python-path to, or a module
containing a `MENU_TEMPLATE` global variable with the template.
goto_callables (dict, optional): Mapping of callable-names to
module-global objects to reference by name in the menu-template.
Must be on the form `callable(caller, raw_string, **kwargs)`.
startnode (str, optional): The name of the startnode, if not 'start'.
persistent (bool, optional): If the generated menu should be persistent.
**kwargs: All kwargs will be passed into EvMenu.
def _test_call(caller, raw_input, **kwargs):
mode = kwargs.get("mode", "exec")
caller.msg(
"\n|y'{}' |n_test_call|y function called with\n "
'caller: |n{}\n |yraw_input: "|n{}|y" \n kwargs: |n{}\n'.format(
mode, caller, raw_input.rstrip(), kwargs
)
)
if mode == "exec":
kwargs = {"random": random.random()}
caller.msg("function modify kwargs to {}".format(kwargs))
else:
caller.msg("|ypassing function kwargs without modification.|n")
return "test_dynamic_node", kwargs
def test_dynamic_node(caller, **kwargs):
text = """
This is a dynamic node with input:
{}
""".format(
kwargs
)
options = (
{
"desc": "pass a new random number to this node",
"goto": ("test_dynamic_node", {"random": random.random()}),
},
{
"desc": "execute a func with kwargs",
"exec": (_test_call, {"mode": "exec", "test_random": random.random()}),
},
{"desc": "dynamic_goto", "goto": (_test_call, {"mode": "goto", "goto_input": "test"})},
{
"desc": "exec test_view_node with kwargs",
"exec": ("test_view_node", {"executed_from_dynamic_node": True}),
"goto": "test_dynamic_node",
},
{"desc": "back to main", "goto": "test_start_node"},
)
return text, options
def test_end_node(caller):
text = """
This is the end of the menu and since it has no options the menu
will exit here, followed by a call of the "look" command.
"""
return text, None
class CmdTestMenu(Command):
"""
Test menu
Usage:
testmenu <menumodule>
Starts a demo menu from a menu node definition module.
Returns:
EvMenu: The generated EvMenu.
"""
key = "testmenu"
def func(self):
if not self.args:
self.caller.msg("Usage: testmenu menumodule")
return
# start menu
EvMenu(
self.caller,
self.args.strip(),
startnode="test_start_node",
persistent=True,
cmdset_mergetype="Replace",
testval="val",
testval2="val2",
)
goto_callables = goto_callables or {}
menu_tree = parse_menu_template(caller, menu_template, goto_callables)
return EvMenu(
caller,
menu_tree,
persistent=persistent,
**kwargs,
)

View file

@ -0,0 +1,221 @@
# -------------------------------------------------------------
#
# test menu strucure and testing command
#
# -------------------------------------------------------------
import random
def _generate_goto(caller, **kwargs):
return kwargs.get("name", "test_dynamic_node"), {"name": "replaced!"}
def test_start_node(caller):
menu = caller.ndb._menutree
text = """
This is an example menu.
If you enter anything except the valid options, your input will be
recorded and you will be brought to a menu entry showing your
input.
Select options or use 'quit' to exit the menu.
The menu was initialized with two variables: %s and %s.
""" % (
menu.testval,
menu.testval2,
)
options = (
{
"key": ("|yS|net", "s"),
"desc": "Set an attribute on yourself.",
"exec": lambda caller: caller.attributes.add("menuattrtest", "Test value"),
"goto": "test_set_node",
},
{
"key": ("|yL|nook", "l"),
"desc": "Look and see a custom message.",
"goto": "test_look_node",
},
{"key": ("|yV|niew", "v"), "desc": "View your own name", "goto": "test_view_node"},
{
"key": ("|yD|nynamic", "d"),
"desc": "Dynamic node",
"goto": (_generate_goto, {"name": "test_dynamic_node"}),
},
{
"key": ("|yQ|nuit", "quit", "q", "Q"),
"desc": "Quit this menu example.",
"goto": "test_end_node",
},
{"key": "_default", "goto": "test_displayinput_node"},
)
return text, options
def test_look_node(caller):
text = "This is a custom look location!"
options = {
"key": ("|yL|nook", "l"),
"desc": "Go back to the previous menu.",
"goto": "test_start_node",
}
return text, options
def test_set_node(caller):
text = (
"""
The attribute 'menuattrtest' was set to
|w%s|n
(check it with examine after quitting the menu).
This node's has only one option, and one of its key aliases is the
string "_default", meaning it will catch any input, in this case
to return to the main menu. So you can e.g. press <return> to go
back now.
"""
% caller.db.menuattrtest, # optional help text for this node
"""
This is the help entry for this node. It is created by returning
the node text as a tuple - the second string in that tuple will be
used as the help text.
""",
)
options = {"key": ("back (default)", "_default"), "goto": "test_start_node"}
return text, options
def test_view_node(caller, **kwargs):
text = (
"""
Your name is |g%s|n!
click |lclook|lthere|le to trigger a look command under MXP.
This node's option has no explicit key (nor the "_default" key
set), and so gets assigned a number automatically. You can infact
-always- use numbers (1...N) to refer to listed options also if you
don't see a string option key (try it!).
"""
% caller.key
)
if kwargs.get("executed_from_dynamic_node", False):
# we are calling this node as a exec, skip return values
caller.msg("|gCalled from dynamic node:|n \n {}".format(text))
return
else:
options = {"desc": "back to main", "goto": "test_start_node"}
return text, options
def test_displayinput_node(caller, raw_string):
text = (
"""
You entered the text:
"|w%s|n"
... which could now be handled or stored here in some way if this
was not just an example.
This node has an option with a single alias "_default", which
makes it hidden from view. It catches all input (except the
in-menu help/quit commands) and will, in this case, bring you back
to the start node.
"""
% raw_string.rstrip()
)
options = {"key": "_default", "goto": "test_start_node"}
return text, options
def _test_call(caller, raw_input, **kwargs):
mode = kwargs.get("mode", "exec")
caller.msg(
"\n|y'{}' |n_test_call|y function called with\n "
'caller: |n{}\n |yraw_input: "|n{}|y" \n kwargs: |n{}\n'.format(
mode, caller, raw_input.rstrip(), kwargs
)
)
if mode == "exec":
kwargs = {"random": random.random()}
caller.msg("function modify kwargs to {}".format(kwargs))
else:
caller.msg("|ypassing function kwargs without modification.|n")
return "test_dynamic_node", kwargs
def test_dynamic_node(caller, **kwargs):
text = """
This is a dynamic node with input:
{}
""".format(
kwargs
)
options = (
{
"desc": "pass a new random number to this node",
"goto": ("test_dynamic_node", {"random": random.random()}),
},
{
"desc": "execute a func with kwargs",
"exec": (_test_call, {"mode": "exec", "test_random": random.random()}),
},
{"desc": "dynamic_goto", "goto": (_test_call, {"mode": "goto", "goto_input": "test"})},
{
"desc": "exec test_view_node with kwargs",
"exec": ("test_view_node", {"executed_from_dynamic_node": True}),
"goto": "test_dynamic_node",
},
{"desc": "back to main", "goto": "test_start_node"},
)
return text, options
def test_end_node(caller):
text = """
This is the end of the menu and since it has no options the menu
will exit here, followed by a call of the "look" command.
"""
return text, None
# class CmdTestMenu(Command):
# """
# Test menu
#
# Usage:
# testmenu <menumodule>
#
# Starts a demo menu from a menu node definition module.
#
# """
#
# key = "testmenu"
#
# def func(self):
#
# if not self.args:
# self.caller.msg("Usage: testmenu menumodule")
# return
# # start menu
# EvMenu(
# self.caller,
# self.args.strip(),
# startnode="test_start_node",
# persistent=True,
# cmdset_mergetype="Replace",
# testval="val",
# testval2="val2",
# )
#

View file

@ -18,7 +18,9 @@ To help debug the menu, turn on `debug_output`, which will print the traversal p
"""
import copy
from anything import Anything
from django.test import TestCase
from evennia.utils.test_resources import EvenniaTest
from evennia.utils import evmenu
from evennia.utils import ansi
from mock import MagicMock
@ -229,7 +231,7 @@ class TestEvMenu(TestCase):
class TestEvMenuExample(TestEvMenu):
menutree = "evennia.utils.evmenu"
menutree = "evennia.utils.tests.data.evmenu_example"
startnode = "test_start_node"
kwargs = {"testval": "val", "testval2": "val2"}
debug_output = False
@ -262,3 +264,79 @@ class TestEvMenuExample(TestEvMenu):
def test_kwargsave(self):
self.assertTrue(hasattr(self.menu, "testval"))
self.assertTrue(hasattr(self.menu, "testval2"))
def _callnode1(caller, raw_string, **kwargs):
return "node1"
def _callnode2(caller, raw_string, **kwargs):
return "node2"
class TestMenuTemplateParse(EvenniaTest):
"""Test menu templating helpers"""
def setUp(self):
super().setUp()
self.menu_template = """
## node start
Neque ea alias perferendis molestiae eligendi. Debitis exercitationem
exercitationem quas blanditiis quisquam officia ut. Fugit aut fugit enim quia
non. Earum et excepturi animi ex esse accusantium et. Id adipisci eos enim
ratione.
## options
1: first option -> node1
2: second option -> node2
next: node1
## node node1
Node 1
## options
fwd: node2
call1: callnode1()
call2: callnode2(foo=bar, bar=22, goo="another test")
>: start
## node node2
Text of node 2
## options
> foo*: node1
> [0-9]+?: node2
> back: start
"""
self.goto_callables = {"callnode1": _callnode1, "callnode2": _callnode2}
def test_parse_menu_template(self):
"""EvMenu template testing"""
menutree = evmenu.parse_menu_template(self.char1, self.menu_template,
self.goto_callables)
self.assertEqual(menutree, {"start": Anything, "node1": Anything, "node2": Anything})
def test_template2menu(self):
evmenu.template2menu(self.char1, self.menu_template, self.goto_callables)
def test_parse_menu_fail(self):
template = """
## NODE
Text
## OPTIONS
next: callnode2(invalid)
"""
with self.assertRaises(RuntimeError):
evmenu.parse_menu_template(self.char1, template, self.goto_callables)

View file

@ -6,7 +6,7 @@
let goldenlayout = (function () {
var myLayout;
var knownTypes = ["all", "untagged"];
var knownTypes = ["all", "untagged", "testing"];
var untagged = [];
var newTabConfig = {
@ -404,10 +404,12 @@ let goldenlayout = (function () {
//
// returns an array of pane divs that the given message should be sent to
//
var onText = function (args, kwargs) {
var routeMessage = function (args, kwargs) {
// If the message is not itself tagged, we"ll assume it
// should go into any panes with "all" and "untagged" set
var divArray = [];
var msgtype = "untagged";
if ( kwargs && "type" in kwargs ) {
@ -419,37 +421,47 @@ let goldenlayout = (function () {
}
}
let messageDelivered = false;
let components = myLayout.root.getItemsByType("component");
components.forEach( function (component) {
if( component.hasId("inputComponent") ) { return; } // ignore the input component
if( component.hasId("inputComponent") ) { return; } // ignore input components
let textDiv = component.container.getElement().children(".content");
let attrTypes = textDiv.attr("types");
let destDiv = component.container.getElement().children(".content");
let attrTypes = destDiv.attr("types");
let paneTypes = attrTypes ? attrTypes.split(" ") : [];
let updateMethod = textDiv.attr("updateMethod");
let txt = args[0];
// is this message type listed in this pane"s types (or is this pane catching "all")
if( paneTypes.includes(msgtype) || paneTypes.includes("all") ) {
routeMsg( textDiv, txt, updateMethod );
messageDelivered = true;
divArray.push(destDiv);
}
// is this pane catching "upmapped" messages?
// And is this message type listed in the untagged types array?
if( paneTypes.includes("untagged") && untagged.includes(msgtype) ) {
routeMsg( textDiv, txt, updateMethod );
messageDelivered = true;
divArray.push(destDiv);
}
});
if ( messageDelivered ) {
return true;
}
// unhandled message
return false;
return divArray;
}
//
//
var onText = function (args, kwargs) {
// are any panes set to receive this text message?
var divs = routeMessage(args, kwargs);
var msgHandled = false;
divs.forEach( function (div) {
let updateMethod = div.attr("updateMethod");
let txt = args[0];
// yes, so add this text message to the target div
routeMsg( div, txt, updateMethod );
msgHandled = true;
});
return msgHandled;
}
@ -536,6 +548,7 @@ let goldenlayout = (function () {
getGL: function () { return myLayout; },
addKnownType: addKnownType,
onTabCreate: onTabCreate,
routeMessage: routeMessage,
}
}());
window.plugin_handler.add("goldenlayout", goldenlayout);

View file

@ -1,53 +1,119 @@
/*
* Evennia example Webclient multimedia outputs plugin
*
* Evennia Webclient multimedia outputs plugin
* PLUGIN ORDER PREREQS:
* loaded after:
* webclient_gui.js
* option2.js
* loaded before:
*
* in evennia python code:
*
* To use, in evennia python code:
* target.msg( image="URL" )
* target.msg( audio="URL" )
* target.msg( video="URL" )
* or, if you prefer tagged routing:
* target.msg( image=("URL",{'type':'tag'}) )
*
*
* Note: users probably don't _want_ more than one pane to end up with multimedia tags...
* But to allow proper tagged message routing, this plugin doesn't explicitly deny it.
*/
let multimedia_plugin = (function () {
//
var image = function (args, kwargs) {
var mwin = $("#messagewindow");
mwin.append("<img src='"+ args[0] +"'/>");
mwin.scrollTop(mwin[0].scrollHeight);
let options = window.options;
if( !("mm_image" in options) || options["mm_image"] === false ) { return; }
var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
mwins.forEach( function (mwin) {
mwin.append("<img src='"+ args[0] +"'/>");
mwin.scrollTop(mwin[0].scrollHeight);
});
}
//
var audio = function (args, kwargs) {
let options = window.options;
if( !("mm_audio" in options) || options["mm_audio"] === false ) { return; }
// create an HTML5 audio control (only .mp3 is fully compatible with all major browsers)
var mwin = $("#messagewindow");
mwin.append("<audio controls='' autoplay='' style='height:17px;width:175px'>" +
"<source src='"+ args[0] +"'/>" +
"</audio>");
mwin.scrollTop(mwin[0].scrollHeight);
var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
mwins.forEach( function (mwin) {
mwin.append("<audio controls='' autoplay='' style='height:17px;width:175px'>" +
"<source src='"+ args[0] +"'/>" +
"</audio>");
mwin.scrollTop(mwin[0].scrollHeight);
});
}
//
var video = function (args, kwargs) {
let options = window.options;
if( !("mm_video" in options) || options["mm_video"] === false ) { return; }
// create an HTML5 video element (only h264 .mp4 is compatible with all major browsers)
var mwin = $("#messagewindow");
mwin.append("<video controls='' autoplay=''>" +
"<source src='"+ args[0] +"'/>" +
"</video>");
mwin.scrollTop(mwin[0].scrollHeight);
var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
mwins.forEach( function (mwin) {
mwin.append("<video controls='' autoplay=''>" +
"<source src='"+ args[0] +"'/>" +
"</video>");
mwin.scrollTop(mwin[0].scrollHeight);
});
}
//
var onOptionsUI = function (parentdiv) {
let options = window.options;
var checked;
checked = options["mm_image"] ? "checked='checked'" : "";
var mmImage = $( [ "<label>",
"<input type='checkbox' data-setting='mm_image' " + checked + "'>",
" Enable multimedia image (png/gif/etc) messages",
"</label>"
].join("") );
checked = options["mm_audio"] ? "checked='checked'" : "";
var mmAudio = $( [ "<label>",
"<input type='checkbox' data-setting='mm_audio' " + checked + "'>",
" Enable multimedia audio (mp3) messages",
"</label>"
].join("") );
checked = options["mm_video"] ? "checked='checked'" : "";
var mmVideo = $( [ "<label>",
"<input type='checkbox' data-setting='mm_video' " + checked + "'>",
" Enable multimedia video (h264 .mp4) messages",
"</label>"
].join("") );
mmImage.on("change", window.plugins["options2"].onOptionCheckboxChanged);
mmAudio.on("change", window.plugins["options2"].onOptionCheckboxChanged);
mmVideo.on("change", window.plugins["options2"].onOptionCheckboxChanged);
parentdiv.append(mmImage);
parentdiv.append(mmAudio);
parentdiv.append(mmVideo);
}
//
// Mandatory plugin init function
var init = function () {
Evennia = window.Evennia;
Evennia.emitter.on('image', image); // capture "image" commands
Evennia.emitter.on('audio', audio); // capture "audio" commands
Evennia.emitter.on('video', video); // capture "video" commands
let options = window.options;
options["mm_image"] = true;
options["mm_audio"] = true;
options["mm_video"] = true;
let Evennia = window.Evennia;
Evennia.emitter.on("image", image); // capture "image" commands
Evennia.emitter.on("audio", audio); // capture "audio" commands
Evennia.emitter.on("video", video); // capture "video" commands
console.log('Multimedia plugin initialized');
}
return {
init: init,
onOptionsUI: onOptionsUI,
}
})();
plugin_handler.add('multimedia_plugin', multimedia_plugin);
plugin_handler.add("multimedia_plugin", multimedia_plugin);

View file

@ -179,6 +179,7 @@ let options2 = (function () {
onLoggedIn: onLoggedIn,
onOptionsUI: onOptionsUI,
onPrompt: onPrompt,
onOptionCheckboxChanged: onOptionCheckboxChanged,
}
})();
window.plugin_handler.add("options2", options2);

View file

@ -1,438 +0,0 @@
/*
*
* Plugin to use split.js to create a basic windowed ui
*
*/
let splithandler_plugin = (function () {
var num_splits = 0;
var split_panes = {};
var backout_list = [];
var known_types = ['all', 'rest'];
// Exported Functions
//
// function to assign "Text types to catch" to a pane
var set_pane_types = function (splitpane, types) {
split_panes[splitpane]['types'] = types;
}
//
// Add buttons to the Evennia webcilent toolbar
function addToolbarButtons () {
var toolbar = $('#toolbar');
toolbar.append( $('<button id="splitbutton" type="button">&#x21f9;</button>') );
toolbar.append( $('<button id="panebutton" type="button">&#x2699;</button>') );
toolbar.append( $('<button id="undobutton" type="button">&#x21B6;</button>') );
$('#undobutton').hide();
}
function addSplitDialog () {
plugins['popups'].createDialog('splitdialog', 'Split Pane', '');
}
function addPaneDialog () {
plugins['popups'].createDialog('panedialog', 'Assign Pane Options', '');
}
//
// Handle resizing the InputField after a client resize event so that the splits dont get too big.
function resizeInputField () {
var wrapper = $("#inputform")
var input = $("#inputcontrol")
var prompt = $("#prompt")
input.height( wrapper.height() - (input.offset().top - wrapper.offset().top) );
}
//
// Handle resizing of client
function doWindowResize() {
var resizable = $("[data-update-append]");
var parents = resizable.closest(".split");
resizeInputField();
parents.animate({
scrollTop: parents.prop("scrollHeight")
}, 0);
}
//
// create a new UI split
var dynamic_split = function (splitpane, direction, pane_name1, pane_name2, update_method1, update_method2, sizes) {
// find the sub-div of the pane we are being asked to split
splitpanesub = splitpane + '-sub';
// create the new div stack to replace the sub-div with.
var first_div = $( '<div id="'+pane_name1+'" class="split split-'+direction+'" />' )
var first_sub = $( '<div id="'+pane_name1+'-sub" class="split-sub" />' )
var second_div = $( '<div id="'+pane_name2+'" class="split split-'+direction+'" />' )
var second_sub = $( '<div id="'+pane_name2+'-sub" class="split-sub" />' )
// check to see if this sub-pane contains anything
contents = $('#'+splitpanesub).contents();
if( contents ) {
// it does, so move it to the first new div-sub (TODO -- selectable between first/second?)
contents.appendTo(first_sub);
}
first_div.append( first_sub );
second_div.append( second_sub );
// update the split_panes array to remove this pane name, but store it for the backout stack
var backout_settings = split_panes[splitpane];
delete( split_panes[splitpane] );
// now vaporize the current split_N-sub placeholder and create two new panes.
$('#'+splitpane).append(first_div);
$('#'+splitpane).append(second_div);
$('#'+splitpane+'-sub').remove();
// And split
Split(['#'+pane_name1,'#'+pane_name2], {
direction: direction,
sizes: sizes,
gutterSize: 4,
minSize: [50,50],
});
// store our new split sub-divs for future splits/uses by the main UI.
split_panes[pane_name1] = { 'types': [], 'update_method': update_method1 };
split_panes[pane_name2] = { 'types': [], 'update_method': update_method2 };
// add our new split to the backout stack
backout_list.push( {'pane1': pane_name1, 'pane2': pane_name2, 'undo': backout_settings} );
$('#undobutton').show();
}
//
// Reverse the last UI split
var undo_split = function () {
// pop off the last split pair
var back = backout_list.pop();
if( !back ) {
return;
}
if( backout_list.length === 0 ) {
$('#undobutton').hide();
}
// Collect all the divs/subs in play
var pane1 = back['pane1'];
var pane2 = back['pane2'];
var pane1_sub = $('#'+pane1+'-sub');
var pane2_sub = $('#'+pane2+'-sub');
var pane1_parent = $('#'+pane1).parent();
var pane2_parent = $('#'+pane2).parent();
if( pane1_parent.attr('id') != pane2_parent.attr('id') ) {
// sanity check failed...somebody did something weird...bail out
console.log( pane1 );
console.log( pane2 );
console.log( pane1_parent );
console.log( pane2_parent );
return;
}
// create a new sub-pane in the panes parent
var parent_sub = $( '<div id="'+pane1_parent.attr('id')+'-sub" class="split-sub" />' )
// check to see if the special #messagewindow is in either of our sub-panes.
var msgwindow = pane1_sub.find('#messagewindow')
if( !msgwindow ) {
//didn't find it in pane 1, try pane 2
msgwindow = pane2_sub.find('#messagewindow')
}
if( msgwindow ) {
// It is, so collect all contents into it instead of our parent_sub div
// then move it to parent sub div, this allows future #messagewindow divs to flow properly
msgwindow.append( pane1_sub.contents() );
msgwindow.append( pane2_sub.contents() );
parent_sub.append( msgwindow );
} else {
//didn't find it, so move the contents of the two panes' sub-panes into the new sub-pane
parent_sub.append( pane1_sub.contents() );
parent_sub.append( pane2_sub.contents() );
}
// clear the parent
pane1_parent.empty();
// add the new sub-pane back to the parent div
pane1_parent.append(parent_sub);
// pull the sub-div's from split_panes
delete split_panes[pane1];
delete split_panes[pane2];
// add our parent pane back into the split_panes list for future splitting
split_panes[pane1_parent.attr('id')] = back['undo'];
}
//
// UI elements
//
//
// Draw "Split Controls" Dialog
var onSplitDialog = function () {
var dialog = $("#splitdialogcontent");
dialog.empty();
var selection = '<select name="pane">';
for ( var pane in split_panes ) {
selection = selection + '<option value="' + pane + '">' + pane + '</option>';
}
selection = "Pane to split: " + selection + "</select> ";
dialog.append(selection);
dialog.append('<input type="radio" name="direction" value="vertical" checked>top/bottom </>');
dialog.append('<input type="radio" name="direction" value="horizontal">side-by-side <hr />');
dialog.append('Pane 1: <input type="text" name="new_pane1" value="" />');
dialog.append('<input type="radio" name="flow1" value="linefeed" checked>newlines </>');
dialog.append('<input type="radio" name="flow1" value="replace">replace </>');
dialog.append('<input type="radio" name="flow1" value="append">append <hr />');
dialog.append('Pane 2: <input type="text" name="new_pane2" value="" />');
dialog.append('<input type="radio" name="flow2" value="linefeed" checked>newlines </>');
dialog.append('<input type="radio" name="flow2" value="replace">replace </>');
dialog.append('<input type="radio" name="flow2" value="append">append <hr />');
dialog.append('<div id="splitclose" class="btn btn-large btn-outline-primary float-right">Split</div>');
$("#splitclose").bind("click", onSplitDialogClose);
plugins['popups'].togglePopup("#splitdialog");
}
//
// Close "Split Controls" Dialog
var onSplitDialogClose = function () {
var pane = $("select[name=pane]").val();
var direction = $("input[name=direction]:checked").attr("value");
var new_pane1 = $("input[name=new_pane1]").val();
var new_pane2 = $("input[name=new_pane2]").val();
var flow1 = $("input[name=flow1]:checked").attr("value");
var flow2 = $("input[name=flow2]:checked").attr("value");
if( new_pane1 == "" ) {
new_pane1 = 'pane_'+num_splits;
num_splits++;
}
if( new_pane2 == "" ) {
new_pane2 = 'pane_'+num_splits;
num_splits++;
}
if( document.getElementById(new_pane1) ) {
alert('An element: "' + new_pane1 + '" already exists');
return;
}
if( document.getElementById(new_pane2) ) {
alert('An element: "' + new_pane2 + '" already exists');
return;
}
dynamic_split( pane, direction, new_pane1, new_pane2, flow1, flow2, [50,50] );
plugins['popups'].closePopup("#splitdialog");
}
//
// Draw "Pane Controls" dialog
var onPaneControlDialog = function () {
var dialog = $("#panedialogcontent");
dialog.empty();
var selection = '<select name="assign-pane">';
for ( var pane in split_panes ) {
selection = selection + '<option value="' + pane + '">' + pane + '</option>';
}
selection = "Assign to pane: " + selection + "</select> <hr />";
dialog.append(selection);
var multiple = '<select multiple name="assign-type">';
for ( var type in known_types ) {
multiple = multiple + '<option value="' + known_types[type] + '">' + known_types[type] + '</option>';
}
multiple = "Content types: " + multiple + "</select> <hr />";
dialog.append(multiple);
dialog.append('<div id="paneclose" class="btn btn-large btn-outline-primary float-right">Assign</div>');
$("#paneclose").bind("click", onPaneControlDialogClose);
plugins['popups'].togglePopup("#panedialog");
}
//
// Close "Pane Controls" dialog
var onPaneControlDialogClose = function () {
var pane = $("select[name=assign-pane]").val();
var types = $("select[name=assign-type]").val();
// var types = new Array;
// $('#splitdialogcontent input[type=checkbox]:checked').each(function() {
// types.push( $(this).attr('value') );
// });
set_pane_types( pane, types );
plugins['popups'].closePopup("#panedialog");
}
//
// helper function sending text to a pane
var txtToPane = function (panekey, txt) {
var pane = split_panes[panekey];
var text_div = $('#' + panekey + '-sub');
if ( pane['update_method'] == 'replace' ) {
text_div.html(txt)
} else if ( pane['update_method'] == 'append' ) {
text_div.append(txt);
var scrollHeight = text_div.parent().prop("scrollHeight");
text_div.parent().animate({ scrollTop: scrollHeight }, 0);
} else { // line feed
text_div.append("<div class='out'>" + txt + "</div>");
var scrollHeight = text_div.parent().prop("scrollHeight");
text_div.parent().animate({ scrollTop: scrollHeight }, 0);
}
}
//
// plugin functions
//
//
// Accept plugin onText events
var onText = function (args, kwargs) {
// If the message is not itself tagged, we'll assume it
// should go into any panes with 'all' or 'rest' set
var msgtype = "rest";
if ( kwargs && 'type' in kwargs ) {
msgtype = kwargs['type'];
if ( ! known_types.includes(msgtype) ) {
// this is a new output type that can be mapped to panes
console.log('detected new output type: ' + msgtype)
known_types.push(msgtype);
}
}
var target_panes = [];
var rest_panes = [];
for (var key in split_panes) {
var pane = split_panes[key];
// is this message type mapped to this pane (or does the pane has an 'all' type)?
if (pane['types'].length > 0) {
if (pane['types'].includes(msgtype) || pane['types'].includes('all')) {
target_panes.push(key);
} else if (pane['types'].includes('rest')) {
// store rest-panes in case we have no explicit to send to
rest_panes.push(key);
}
} else {
// unassigned panes are assumed to be rest-panes too
rest_panes.push(key);
}
}
var ntargets = target_panes.length;
var nrests = rest_panes.length;
if (ntargets > 0) {
// we have explicit target panes to send to
for (var i=0; i<ntargets; i++) {
txtToPane(target_panes[i], args[0]);
}
return true;
} else if (nrests > 0) {
// no targets, send remainder to rest-panes/unassigned
for (var i=0; i<nrests; i++) {
txtToPane(rest_panes[i], args[0]);
}
return true;
}
// unhandled message
return false;
}
//
// onKeydown check for 'ESC' key.
var onKeydown = function (event) {
var code = event.which;
if (code === 27) { // Escape key
if ($('#splitdialog').is(':visible')) {
plugins['popups'].closePopup("#splitdialog");
return true;
}
if ($('#panedialog').is(':visible')) {
plugins['popups'].closePopup("#panedialog");
return true;
}
}
// capture all keys while one of our "modal" dialogs is open
if ($('#splitdialogcontent').is(':visible') || $('#panedialogcontent').is(':visible')) {
return true;
}
return false;
}
//
// Required plugin "init" function
var init = function(settings) {
known_types.push('help');
Split(['#main','#input'], {
direction: 'vertical',
sizes: [90,10],
gutterSize: 4,
minSize: [50,50],
});
split_panes['main'] = { 'types': [], 'update_method': 'linefeed' };
// Create our UI
addToolbarButtons();
addSplitDialog();
addPaneDialog();
// Register our utility button events
$("#splitbutton").bind("click", onSplitDialog);
$("#panebutton").bind("click", onPaneControlDialog);
$("#undobutton").bind("click", undo_split);
// Event when client window changes
$(window).bind("resize", doWindowResize);
$("[data-role-input]").bind("resize", doWindowResize)
.bind("paste", resizeInputField)
.bind("cut", resizeInputField);
// Event when any key is pressed
$(document).keyup(resizeInputField);
console.log("Splithandler Plugin Initialized.");
}
return {
init: init,
onText: onText,
dynamic_split: dynamic_split,
undo_split: undo_split,
set_pane_types: set_pane_types,
onKeydown: onKeydown,
}
})()
plugin_handler.add('splithandler', splithandler_plugin);

View file

@ -64,7 +64,6 @@ JQuery available.
<!-- set up splits before loading the GUI -->
<!--
<script src="https://unpkg.com/split.js@1.5.9/dist/split.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script>
-->
<script type="text/javascript" src="https://golden-layout.com/files/latest/js/goldenlayout.min.js"></script>