Resolve merge conflicts
This commit is contained in:
commit
90a1a0cba8
35 changed files with 2766 additions and 973 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Evennia 1.0 (2019-) (develop branch, WIP)
|
## 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
|
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)
|
- 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
|
- `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.
|
- 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
|
- Make `object.search` support 'stacks=0' keyword - if ``>0``, the method will return
|
||||||
N identical matches instead of triggering a multi-match error.
|
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
|
### Evennia 0.95 (master)
|
||||||
"TutorialWeaponRack" to prevent collisions with classes in mygame
|
|
||||||
- `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False
|
- `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False
|
||||||
- `py` command now reroutes stdout to output results in-game client. `py`
|
- `py` command now reroutes stdout to output results in-game client. `py`
|
||||||
without arguments starts a full interactive Python console.
|
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)
|
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
|
- Using `EvMore pagination`, dramatically improves performance of `spawn/list` and `scripts` listings
|
||||||
(100x speed increase for displaying 1000+ prototypes/scripts).
|
(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)
|
## Evennia 0.9 (2018-2019)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
* `notifications.js` Defines onText. Generates browser notification events for each new message
|
||||||
while the tab is hidden.
|
while the tab is hidden.
|
||||||
* `oob.js` Defines onSend. Allows the user to test/send Out Of Band json messages to the server.
|
* `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
|
* `options.js` Defines most callbacks. Provides a popup-based UI to coordinate options settings with the server.
|
||||||
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.
|
||||||
* `options2.js` Defines most callbacks. Provides a goldenlayout-based version of the
|
* `popups.js` Provides default popups/Dialog UI for other plugins to use.
|
||||||
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.
|
|
||||||
|
|
||||||
# Writing your own Plugins
|
# Writing your own Plugins
|
||||||
|
|
||||||
So, you love the functionality of the webclient, but your game has specific types of text that need
|
So, you love the functionality of the webclient, but your game has specific
|
||||||
to be separated out into their own space, visually. There are two plugins to help with this. The
|
types of text that need to be separated out into their own space, visually.
|
||||||
Goldenlayout plugin framework, and the older Splithandler framework.
|
The Goldenlayout plugin framework can help with this.
|
||||||
|
|
||||||
## GoldenLayout
|
## 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.
|
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
|
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.
|
||||||
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.
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
2020-06-12 22:36:53. There are known conversion issues and missing links.
|
2020-06-12 22:36:53. There are known conversion issues and missing links.
|
||||||
This will slowly be ironed out as this is developed.
|
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.
|
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
|
- [Links](./Links) - useful links
|
||||||
- [Table of Contents](./toc) - an alphabetical listing of all regular documentation pages
|
- [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)!
|
||||||
|
|
|
||||||
|
|
@ -1262,7 +1262,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
||||||
]
|
]
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.log_trace()
|
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)
|
now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, now.day, now.hour, now.minute)
|
||||||
if _MUDINFO_CHANNEL:
|
if _MUDINFO_CHANNEL:
|
||||||
_MUDINFO_CHANNEL.tempmsg(f"[{_MUDINFO_CHANNEL.key}, {now}]: {message}")
|
_MUDINFO_CHANNEL.tempmsg(f"[{_MUDINFO_CHANNEL.key}, {now}]: {message}")
|
||||||
|
|
|
||||||
|
|
@ -494,6 +494,11 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
|
||||||
cmdset = None
|
cmdset = None
|
||||||
for cset in (cset for cset in local_obj_cmdsets if cset):
|
for cset in (cset for cset in local_obj_cmdsets if cset):
|
||||||
cset.duplicates = cset.old_duplicates
|
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)
|
returnValue(cmdset)
|
||||||
except ErrorReported:
|
except ErrorReported:
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -106,9 +106,9 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
||||||
commands preference.
|
commands preference.
|
||||||
|
|
||||||
duplicates - determines what happens when two sets of equal
|
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
|
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
|
result will be a merger with more than one of each
|
||||||
name match. This will usually lead to the account
|
name match. This will usually lead to the account
|
||||||
receiving a multiple-match error higher up the road,
|
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
|
select which ball to kick ... Allowing duplicates
|
||||||
only makes sense for Union and Intersect, the setting
|
only makes sense for Union and Intersect, the setting
|
||||||
is ignored for the other mergetypes.
|
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
|
key_mergetype (dict) - allows the cmdset to define a unique
|
||||||
mergetype for particular cmdsets. Format is
|
mergetype for particular cmdsets. Format is
|
||||||
|
|
@ -144,14 +154,27 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
||||||
mergetype = "Union"
|
mergetype = "Union"
|
||||||
priority = 0
|
priority = 0
|
||||||
|
|
||||||
# These flags, if set to None, will allow "pass-through" of lower-prio settings
|
# These flags, if set to None should be interpreted as 'I don't care' and,
|
||||||
# of True/False. If set to True/False, will override lower-prio settings.
|
# 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_exits = None
|
||||||
no_objs = None
|
no_objs = None
|
||||||
no_channels = None
|
no_channels = None
|
||||||
# same as above, but if left at None in the final merged set, the
|
# The .duplicates setting does not propagate and since duplicates can only happen
|
||||||
# cmdhandler will auto-assume True for Objects and stay False for all
|
# on same-prio cmdsets, there is no concept of passthrough on `None`.
|
||||||
# other entities.
|
# 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
|
duplicates = None
|
||||||
|
|
||||||
permanent = False
|
permanent = False
|
||||||
|
|
@ -334,7 +357,15 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
||||||
commands (str): Representation of commands in Cmdset.
|
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):
|
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
|
# 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
|
# 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_c.no_channels = (
|
||||||
self.no_channels if cmdset_a.no_channels is None else cmdset_a.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_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.no_objs = self.no_objs if cmdset_a.no_objs is None else cmdset_a.no_objs
|
||||||
|
cmdset_c.duplicates = None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# B higher priority than A
|
# 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
|
# 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
|
# 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_c.no_channels = (
|
||||||
cmdset_a.no_channels if self.no_channels is None else self.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_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.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
|
# we store actual_mergetype since key_mergetypes
|
||||||
# might be different from the main mergetype.
|
# might be different from the main mergetype.
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,10 @@ class CmdSetHandler(object):
|
||||||
|
|
||||||
# the id of the "merged" current cmdset for easy access.
|
# the id of the "merged" current cmdset for easy access.
|
||||||
self.key = None
|
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
|
self.current = None
|
||||||
# this holds a history of CommandSets
|
# this holds a history of CommandSets
|
||||||
self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)]
|
self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)]
|
||||||
|
|
@ -311,27 +314,13 @@ class CmdSetHandler(object):
|
||||||
Display current commands
|
Display current commands
|
||||||
"""
|
"""
|
||||||
|
|
||||||
string = ""
|
strings = ["<CmdSetHandler> stack:"]
|
||||||
mergelist = []
|
mergelist = []
|
||||||
if len(self.cmdset_stack) > 1:
|
if len(self.cmdset_stack) > 1:
|
||||||
# We have more than one cmdset in stack; list them all
|
# We have more than one cmdset in stack; list them all
|
||||||
for snum, cmdset in enumerate(self.cmdset_stack):
|
for snum, cmdset in enumerate(self.cmdset_stack):
|
||||||
mergetype = self.mergetype_stack[snum]
|
mergelist.append(str(snum + 1))
|
||||||
permstring = "non-perm"
|
strings.append(f" {snum + 1}: {cmdset}")
|
||||||
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"
|
|
||||||
|
|
||||||
# Display the currently active cmdset, limited by self.obj's permissions
|
# Display the currently active cmdset, limited by self.obj's permissions
|
||||||
mergetype = self.mergetype_stack[-1]
|
mergetype = self.mergetype_stack[-1]
|
||||||
|
|
@ -339,27 +328,15 @@ class CmdSetHandler(object):
|
||||||
merged_on = self.cmdset_stack[-2].key
|
merged_on = self.cmdset_stack[-2].key
|
||||||
mergetype = _("custom {mergetype} on cmdset '{cmdset}'")
|
mergetype = _("custom {mergetype} on cmdset '{cmdset}'")
|
||||||
mergetype = mergetype.format(mergetype=mergetype, cmdset=merged_on)
|
mergetype = mergetype.format(mergetype=mergetype, cmdset=merged_on)
|
||||||
|
|
||||||
if mergelist:
|
if mergelist:
|
||||||
tmpstring = _(" <Merged {mergelist} {mergetype}, prio {prio}>: {current}")
|
# current is a result of mergers
|
||||||
string += tmpstring.format(
|
mergelist="+".join(mergelist)
|
||||||
mergelist="+".join(mergelist),
|
strings.append(f" <Merged {mergelist}>: {self.current}")
|
||||||
mergetype=mergetype,
|
|
||||||
prio=self.current.priority,
|
|
||||||
current=self.current,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
permstring = "non-perm"
|
# current is a single cmdset
|
||||||
if self.current.permanent:
|
strings.append(" " + str(self.current))
|
||||||
permstring = "perm"
|
return "\n".join(strings).rstrip()
|
||||||
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()
|
|
||||||
|
|
||||||
def _import_cmdset(self, cmdset_path, emit_to_obj=None):
|
def _import_cmdset(self, cmdset_path, emit_to_obj=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -381,12 +358,22 @@ class CmdSetHandler(object):
|
||||||
def update(self, init_mode=False):
|
def update(self, init_mode=False):
|
||||||
"""
|
"""
|
||||||
Re-adds all sets in the handler to have an updated current
|
Re-adds all sets in the handler to have an updated current
|
||||||
set.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
init_mode (bool, optional): Used automatically right after
|
init_mode (bool, optional): Used automatically right after
|
||||||
this handler was created; it imports all permanent cmdsets
|
this handler was created; it imports all permanent cmdsets
|
||||||
from the database.
|
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:
|
if init_mode:
|
||||||
# reimport all permanent cmdsets
|
# reimport all permanent cmdsets
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,9 @@ class Command(object, metaclass=CommandMeta):
|
||||||
arg_regex - (optional) raw string regex defining how the argument part of
|
arg_regex - (optional) raw string regex defining how the argument part of
|
||||||
the command should look in order to match for this command
|
the command should look in order to match for this command
|
||||||
(e.g. must it be a space between cmdname and arg?)
|
(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
|
(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
|
system to create the help entry for the command, so it's a good idea to
|
||||||
|
|
|
||||||
|
|
@ -2429,13 +2429,13 @@ class CmdExamine(ObjManipCommand):
|
||||||
)
|
)
|
||||||
return output
|
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.
|
Helper function that creates a nice report about an object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj (any): Object to analyze.
|
obj (any): Object to analyze.
|
||||||
avail_cmdset (CmdSet): Current cmdset for object.
|
current_cmdset (CmdSet): Current cmdset for object.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The formatted string.
|
str: The formatted string.
|
||||||
|
|
@ -2513,15 +2513,36 @@ class CmdExamine(ObjManipCommand):
|
||||||
# cmdsets
|
# cmdsets
|
||||||
if not (len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"):
|
if not (len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"):
|
||||||
# all() returns a 'stack', so make a copy to sort.
|
# 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)
|
stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority, reverse=True)
|
||||||
output["Stored Cmdset(s)"] = "\n " + "\n ".join(
|
stored = []
|
||||||
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority})"
|
for cmdset in stored_cmdsets:
|
||||||
for cmdset in stored_cmdsets
|
if cmdset.key == "_EMPTY_CMDSET":
|
||||||
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
|
# 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
|
# we always at least try to add account- and session sets since these are ignored
|
||||||
# if we merge on the object level.
|
# if we merge on the object level.
|
||||||
if hasattr(obj, "account") and obj.account:
|
if hasattr(obj, "account") and obj.account:
|
||||||
|
|
@ -2551,15 +2572,24 @@ class CmdExamine(ObjManipCommand):
|
||||||
pass
|
pass
|
||||||
all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()]
|
all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()]
|
||||||
all_cmdsets.sort(key=lambda x: x.priority, reverse=True)
|
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)
|
output[f"Commands available to {obj.key} (result of Merged CmdSets)"] = str(cmdsetstr)
|
||||||
|
|
||||||
# scripts
|
# scripts
|
||||||
if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all():
|
if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all():
|
||||||
output["Scripts"] = "\n " + f"{obj.scripts}"
|
output["Scripts"] = "\n " + f"{obj.scripts}"
|
||||||
|
|
|
||||||
|
|
@ -808,17 +808,49 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
||||||
lastpages = pages[-number:]
|
lastpages = pages[-number:]
|
||||||
else:
|
else:
|
||||||
lastpages = pages
|
lastpages = pages
|
||||||
template = "|w%s|n |c%s|n to |c%s|n: %s"
|
to_template = "|w{date}{clr} {sender}|nto{clr}{receiver}|n:> {message}"
|
||||||
lastpages = "\n ".join(
|
from_template = "|w{date}{clr} {receiver}|nfrom{clr}{sender}|n:< {message}"
|
||||||
template
|
listing = []
|
||||||
% (
|
prev_selfsend = False
|
||||||
utils.datetime_format(page.date_created),
|
for page in lastpages:
|
||||||
",".join(obj.key for obj in page.senders),
|
multi_send = len(page.senders) > 1
|
||||||
"|n,|c ".join([obj.name for obj in page.receivers]),
|
multi_recv = len(page.receivers) > 1
|
||||||
page.message,
|
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:
|
if lastpages:
|
||||||
string = "Your latest pages:\n %s" % lastpages
|
string = "Your latest pages:\n %s" % lastpages
|
||||||
|
|
|
||||||
|
|
@ -378,10 +378,12 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
|
||||||
if not items:
|
if not items:
|
||||||
string = "You are not carrying anything."
|
string = "You are not carrying anything."
|
||||||
else:
|
else:
|
||||||
|
from evennia.utils.ansi import raw as raw_ansi
|
||||||
table = self.styled_table(border="header")
|
table = self.styled_table(border="header")
|
||||||
for item in items:
|
for item in items:
|
||||||
table.add_row("|C%s|n" % item.name, item.db.desc or "")
|
table.add_row(f"|C{item.name}|n",
|
||||||
string = "|wYou are carrying:\n%s" % table
|
"{}|n".format(utils.crop(raw_ansi(item.db.desc), width=50) or ""))
|
||||||
|
string = f"|wYou are carrying:\n{table}"
|
||||||
self.caller.msg(string)
|
self.caller.msg(string)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -294,10 +294,12 @@ class CmdHelp(Command):
|
||||||
hdict_topic = defaultdict(list)
|
hdict_topic = defaultdict(list)
|
||||||
# create the dictionaries {category:[topic, topic ...]} required by format_help_list
|
# create the dictionaries {category:[topic, topic ...]} required by format_help_list
|
||||||
# Filter commands that should be reached by the help
|
# 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:
|
for cmd in all_cmds:
|
||||||
if self.should_list_cmd(cmd, caller):
|
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]
|
[hdict_topic[topic.help_category].append(topic.key) for topic in all_topics]
|
||||||
# report back
|
# report back
|
||||||
self.msg_help(self.format_help_list(hdict_cmd, hdict_topic))
|
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}*"]:
|
for match_query in [f"{query}~1", f"{query}*"]:
|
||||||
# We first do an exact word-match followed by a start-by query
|
# We first do an exact word-match followed by a start-by query
|
||||||
|
|
||||||
matches, suggestions = help_search_with_index(
|
matches, suggestions = help_search_with_index(
|
||||||
match_query, entries, suggestion_maxnum=self.suggestion_maxnum
|
match_query, entries, suggestion_maxnum=self.suggestion_maxnum
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -984,7 +984,8 @@ class TestBuilding(CommandTest):
|
||||||
self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was set to Room")
|
self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was set to Room")
|
||||||
|
|
||||||
def test_list_cmdsets(self):
|
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'")
|
self.call(building.CmdListCmdSets(), "NotFound", "Could not find 'NotFound'")
|
||||||
|
|
||||||
def test_typeclass(self):
|
def test_typeclass(self):
|
||||||
|
|
|
||||||
|
|
@ -194,27 +194,71 @@ class TestCmdSetMergers(TestCase):
|
||||||
self.assertEqual(len(cmdset_f.commands), 4)
|
self.assertEqual(len(cmdset_f.commands), 4)
|
||||||
self.assertTrue(all(True for cmd in cmdset_f.commands if cmd.from_cmdset == "A"))
|
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
|
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
|
cmdset_f = d + c + b + a # reverse, same-prio
|
||||||
self.assertTrue(cmdset_f.no_exits)
|
self.assertTrue(cmdset_f.no_exits)
|
||||||
self.assertTrue(cmdset_f.no_objs)
|
self.assertTrue(cmdset_f.no_objs)
|
||||||
self.assertTrue(cmdset_f.no_channels)
|
self.assertTrue(cmdset_f.no_channels)
|
||||||
self.assertTrue(cmdset_f.duplicates)
|
self.assertIsNone(cmdset_f.duplicates)
|
||||||
self.assertEqual(len(cmdset_f.commands), 8)
|
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
|
cmdset_f = a + b + c + d # forward, same-prio
|
||||||
self.assertTrue(cmdset_f.no_exits)
|
self.assertTrue(cmdset_f.no_exits)
|
||||||
self.assertTrue(cmdset_f.no_objs)
|
self.assertTrue(cmdset_f.no_objs)
|
||||||
self.assertTrue(cmdset_f.no_channels)
|
self.assertTrue(cmdset_f.no_channels)
|
||||||
self.assertFalse(cmdset_f.duplicates)
|
self.assertIsNone(cmdset_f.duplicates)
|
||||||
self.assertEqual(len(cmdset_f.commands), 4)
|
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
|
a.priority = 2
|
||||||
b.priority = 1
|
b.priority = 1
|
||||||
c.priority = 0
|
c.priority = 0
|
||||||
|
|
@ -223,14 +267,35 @@ class TestCmdSetMergers(TestCase):
|
||||||
self.assertTrue(cmdset_f.no_exits)
|
self.assertTrue(cmdset_f.no_exits)
|
||||||
self.assertTrue(cmdset_f.no_objs)
|
self.assertTrue(cmdset_f.no_objs)
|
||||||
self.assertTrue(cmdset_f.no_channels)
|
self.assertTrue(cmdset_f.no_channels)
|
||||||
self.assertTrue(cmdset_f.duplicates)
|
self.assertIsNone(cmdset_f.duplicates)
|
||||||
self.assertEqual(len(cmdset_f.commands), 4)
|
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.
|
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_exits)
|
||||||
self.assertTrue(cmdset_f.no_objs)
|
self.assertTrue(cmdset_f.no_objs)
|
||||||
self.assertTrue(cmdset_f.no_channels)
|
self.assertTrue(cmdset_f.no_channels)
|
||||||
self.assertTrue(cmdset_f.duplicates)
|
self.assertIsNone(cmdset_f.duplicates)
|
||||||
self.assertEqual(len(cmdset_f.commands), 4)
|
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
|
a.priority = -1
|
||||||
b.priority = 0
|
b.priority = 0
|
||||||
c.priority = 1
|
c.priority = 1
|
||||||
|
|
@ -239,32 +304,678 @@ class TestCmdSetMergers(TestCase):
|
||||||
self.assertTrue(cmdset_f.no_exits)
|
self.assertTrue(cmdset_f.no_exits)
|
||||||
self.assertTrue(cmdset_f.no_objs)
|
self.assertTrue(cmdset_f.no_objs)
|
||||||
self.assertTrue(cmdset_f.no_channels)
|
self.assertTrue(cmdset_f.no_channels)
|
||||||
self.assertFalse(cmdset_f.duplicates)
|
self.assertIsNone(cmdset_f.duplicates)
|
||||||
self.assertEqual(len(cmdset_f.commands), 4)
|
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
|
cmdset_f = a + b + c + d # forward, A low prio
|
||||||
self.assertTrue(cmdset_f.no_exits)
|
self.assertTrue(cmdset_f.no_exits)
|
||||||
self.assertTrue(cmdset_f.no_objs)
|
self.assertTrue(cmdset_f.no_objs)
|
||||||
self.assertTrue(cmdset_f.no_channels)
|
self.assertTrue(cmdset_f.no_channels)
|
||||||
self.assertFalse(cmdset_f.duplicates)
|
self.assertIsNone(cmdset_f.duplicates)
|
||||||
self.assertEqual(len(cmdset_f.commands), 4)
|
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
|
c.no_exits = False
|
||||||
b.no_objs = False
|
b.no_objs = False
|
||||||
d.duplicates = False
|
d.duplicates = False
|
||||||
# higher-prio sets will change the option up the chain
|
# 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
|
cmdset_f = a + b + c + d # forward, A low prio
|
||||||
self.assertFalse(cmdset_f.no_exits)
|
self.assertFalse(cmdset_f.no_exits)
|
||||||
self.assertFalse(cmdset_f.no_objs)
|
self.assertFalse(cmdset_f.no_objs)
|
||||||
self.assertTrue(cmdset_f.no_channels)
|
self.assertTrue(cmdset_f.no_channels)
|
||||||
self.assertFalse(cmdset_f.duplicates)
|
self.assertIsNone(cmdset_f.duplicates)
|
||||||
self.assertEqual(len(cmdset_f.commands), 4)
|
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
|
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
|
c.duplicates = True
|
||||||
cmdset_f = d + b + c + a # two last mergers 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)
|
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
|
# test cmdhandler functions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from evennia.utils.test_resources import EvenniaTest
|
|
||||||
from evennia import DefaultChannel
|
from evennia import DefaultChannel
|
||||||
from evennia.utils.create import create_message
|
from evennia.utils.create import create_message
|
||||||
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
|
|
||||||
|
|
||||||
class ObjectCreationTest(EvenniaTest):
|
class ObjectCreationTest(EvenniaTest):
|
||||||
|
|
@ -16,3 +16,34 @@ class ObjectCreationTest(EvenniaTest):
|
||||||
msg = create_message("peewee herman", "heh-heh!", header="mail time!")
|
msg = create_message("peewee herman", "heh-heh!", header="mail time!")
|
||||||
self.assertTrue(msg)
|
self.assertTrue(msg)
|
||||||
self.assertEqual(str(msg), "peewee herman->: heh-heh!")
|
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)
|
||||||
|
|
|
||||||
|
|
@ -104,36 +104,34 @@ tutorial
|
||||||
# ... and describe it.
|
# ... and describe it.
|
||||||
#
|
#
|
||||||
@desc
|
@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 ...
|
# Show that the tutorial command works ...
|
||||||
#
|
#
|
||||||
@set here/tutorial_info =
|
@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
|
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
|
intro room assigns some properties to your character, like a simple
|
||||||
"health" property used when fighting. Other rooms and puzzles might do
|
"health" property used when fighting. Other rooms and puzzles might do
|
||||||
|
|
@ -294,14 +292,14 @@ start
|
||||||
on the sign.
|
on the sign.
|
||||||
# Set a climbable object for discovering a hidden exit
|
# 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
|
@desc tree = Only the sturdiest of trees survive at the edge of the
|
||||||
moor. A small group of huddling black things has dug in near the
|
moor. A small huddling black thing has dug in near the
|
||||||
cliff edge, eternally pummeled by wind and salt to become an integral
|
cliff edge, eternally pummeled by wind and salt to become an integral
|
||||||
part of the gloomy scenery.
|
part of the gloomy scenery.
|
||||||
#
|
#
|
||||||
@lock trees = get:false()
|
@lock tree = get:false()
|
||||||
#
|
#
|
||||||
@set trees/get_err_msg =
|
@set trees/get_err_msg =
|
||||||
The group of old trees have withstood the eternal wind for hundreds
|
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
|
# regular exits back to the cliff, that is handled by the bridge
|
||||||
# typeclass itself.
|
# typeclass itself.
|
||||||
#
|
#
|
||||||
@dig The old bridge;bridge;tut#05
|
@dig The old bridge;bridge;east;e;tut#05
|
||||||
: tutorial_world.rooms.BridgeRoom
|
: tutorial_world.rooms.BridgeRoom
|
||||||
= old bridge;east;e;bridge;hangbridge
|
= old bridge;east;e;bridge;hangbridge
|
||||||
#
|
#
|
||||||
|
|
|
||||||
782
evennia/contrib/tutorial_world/intro_menu.py
Normal file
782
evennia/contrib/tutorial_world/intro_menu.py
Normal 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)
|
||||||
|
|
@ -22,7 +22,7 @@ TutorialWeaponRack
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from evennia import DefaultObject, DefaultExit, Command, CmdSet
|
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
|
from evennia.prototypes.spawner import spawn
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
@ -647,7 +647,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
||||||
"""called when the object is first created."""
|
"""called when the object is first created."""
|
||||||
super().at_object_creation()
|
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,
|
# starting root positions. H1/H2 are the horizontally hanging roots,
|
||||||
# V1/V2 the vertically hanging ones. Each can have three positions:
|
# 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
|
# start a 45 second timer before closing again. We store the deferred so it can be
|
||||||
# killed in unittesting.
|
# killed in unittesting.
|
||||||
self.deferred = delay(45, self.reset)
|
self.deferred = delay(45, self.reset)
|
||||||
|
return True
|
||||||
|
|
||||||
def _translate_position(self, root, ipos):
|
def _translate_position(self, root, ipos):
|
||||||
"""Translates the position into words"""
|
"""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 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) "
|
"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 "
|
"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
|
# display the root positions to help with the puzzle
|
||||||
for key, pos in self.db.root_pos.items():
|
for key, pos in self.db.root_pos.items():
|
||||||
|
|
@ -833,6 +834,7 @@ class CmdAttack(Command):
|
||||||
"stab",
|
"stab",
|
||||||
"slash",
|
"slash",
|
||||||
"chop",
|
"chop",
|
||||||
|
"bash",
|
||||||
"parry",
|
"parry",
|
||||||
"defend",
|
"defend",
|
||||||
]
|
]
|
||||||
|
|
@ -875,7 +877,7 @@ class CmdAttack(Command):
|
||||||
tstring = "%s stabs at you with %s. " % (self.caller.key, self.obj.key)
|
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)
|
ostring = "%s stabs at %s with %s. " % (self.caller.key, target.key, self.obj.key)
|
||||||
self.caller.db.combat_parry_mode = False
|
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
|
hit = float(self.obj.db.hit) # un modified due to slash
|
||||||
damage = self.obj.db.damage # un modified due to slash
|
damage = self.obj.db.damage # un modified due to slash
|
||||||
string = "You slash with %s. " % self.obj.key
|
string = "You slash with %s. " % self.obj.key
|
||||||
|
|
@ -1150,7 +1152,14 @@ class TutorialWeaponRack(TutorialObject):
|
||||||
self.db.rack_id = "weaponrack_1"
|
self.db.rack_id = "weaponrack_1"
|
||||||
# these are prototype names from the prototype
|
# these are prototype names from the prototype
|
||||||
# dictionary above.
|
# 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.no_more_weapons_msg = "you find nothing else of use."
|
||||||
self.db.available_weapons = ["knife", "dagger", "sword", "club"]
|
self.db.available_weapons = ["knife", "dagger", "sword", "club"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,12 +69,14 @@ class CmdTutorial(Command):
|
||||||
target = caller.search(self.args.strip())
|
target = caller.search(self.args.strip())
|
||||||
if not target:
|
if not target:
|
||||||
return
|
return
|
||||||
helptext = target.db.tutorial_info
|
helptext = target.db.tutorial_info or ""
|
||||||
if helptext:
|
|
||||||
caller.msg("|G%s|n" % helptext)
|
|
||||||
else:
|
|
||||||
caller.msg("|RSorry, there is no tutorial help available here.|n")
|
|
||||||
|
|
||||||
|
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
|
# for the @detail command we inherit from MuxCommand, since
|
||||||
# we want to make use of MuxCommand's pre-parsing of '=' in the
|
# 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)
|
looking_at_obj.at_desc(looker=caller)
|
||||||
return
|
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):
|
class TutorialRoomCmdSet(CmdSet):
|
||||||
"""
|
"""
|
||||||
|
|
@ -216,6 +238,7 @@ class TutorialRoomCmdSet(CmdSet):
|
||||||
self.add(CmdTutorial())
|
self.add(CmdTutorial())
|
||||||
self.add(CmdTutorialSetDetail())
|
self.add(CmdTutorialSetDetail())
|
||||||
self.add(CmdTutorialLook())
|
self.add(CmdTutorialLook())
|
||||||
|
self.add(CmdTutorialGiveUp())
|
||||||
|
|
||||||
|
|
||||||
class TutorialRoom(DefaultRoom):
|
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):
|
class IntroRoom(TutorialRoom):
|
||||||
"""
|
"""
|
||||||
|
|
@ -381,6 +429,7 @@ class IntroRoom(TutorialRoom):
|
||||||
"This assigns the health Attribute to "
|
"This assigns the health Attribute to "
|
||||||
"the account."
|
"the account."
|
||||||
)
|
)
|
||||||
|
self.cmdset.add(CmdSetEvenniaIntro, permanent=True)
|
||||||
|
|
||||||
def at_object_receive(self, character, source_location):
|
def at_object_receive(self, character, source_location):
|
||||||
"""
|
"""
|
||||||
|
|
@ -396,8 +445,12 @@ class IntroRoom(TutorialRoom):
|
||||||
|
|
||||||
if character.is_superuser:
|
if character.is_superuser:
|
||||||
string = "-" * 78 + SUPERUSER_WARNING + "-" * 78
|
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."""
|
"""This groups the bridge commands. We will store it on the room."""
|
||||||
|
|
||||||
key = "Bridge commands"
|
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):
|
def at_cmdset_creation(self):
|
||||||
"""Called at first cmdset creation"""
|
"""Called at first cmdset creation"""
|
||||||
|
|
@ -679,7 +732,7 @@ class BridgeRoom(WeatherRoom):
|
||||||
self.db.east_exit = "gate"
|
self.db.east_exit = "gate"
|
||||||
self.db.fall_exit = "cliffledge"
|
self.db.fall_exit = "cliffledge"
|
||||||
# add the cmdset on the room.
|
# 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
|
# since the default Character's at_look() will access the room's
|
||||||
# return_description (this skips the cmdset) when
|
# return_description (this skips the cmdset) when
|
||||||
# first entering it, we need to explicitly turn off the room
|
# 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"):
|
if obj.typeclass_path.startswith("evennia.contrib.tutorial_world"):
|
||||||
obj.delete()
|
obj.delete()
|
||||||
character.tags.clear(category="tutorial_world")
|
character.tags.clear(category="tutorial_world")
|
||||||
|
|
||||||
|
def at_object_leave(self, character, destination):
|
||||||
|
if character.account:
|
||||||
|
character.account.execute_cmd("unquell")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1981,12 +1981,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
||||||
# whisper mode
|
# whisper mode
|
||||||
msg_type = "whisper"
|
msg_type = "whisper"
|
||||||
msg_self = (
|
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
|
msg_location = None
|
||||||
else:
|
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_location = msg_location or '{object} says, "{speech}"'
|
||||||
msg_receivers = msg_receivers or message
|
msg_receivers = msg_receivers or message
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ There main function is `spawn(*prototype)`, where the `prototype`
|
||||||
is a dictionary like this:
|
is a dictionary like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from evennia.prototypes import prototypes
|
from evennia.prototypes import prototypes, spawner
|
||||||
|
|
||||||
prot = {
|
prot = {
|
||||||
"prototype_key": "goblin",
|
"prototype_key": "goblin",
|
||||||
|
|
@ -22,7 +22,10 @@ prot = {
|
||||||
"tags": ["mob", "evil", ('greenskin','mob')]
|
"tags": ["mob", "evil", ('greenskin','mob')]
|
||||||
"attrs": [("weapon", "sword")]
|
"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)
|
prot = prototypes.create_prototype(prot)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -82,13 +85,13 @@ import random
|
||||||
|
|
||||||
{
|
{
|
||||||
"prototype_key": "goblin_wizard",
|
"prototype_key": "goblin_wizard",
|
||||||
"prototype_parent": GOBLIN,
|
"prototype_parent": "GOBLIN",
|
||||||
"key": "goblin wizard",
|
"key": "goblin wizard",
|
||||||
"spells": ["fire ball", "lighting bolt"]
|
"spells": ["fire ball", "lighting bolt"]
|
||||||
}
|
}
|
||||||
|
|
||||||
GOBLIN_ARCHER = {
|
GOBLIN_ARCHER = {
|
||||||
"prototype_parent": GOBLIN,
|
"prototype_parent": "GOBLIN",
|
||||||
"key": "goblin archer",
|
"key": "goblin archer",
|
||||||
"attack_skill": (random, (5, 10))"
|
"attack_skill": (random, (5, 10))"
|
||||||
"attacks": ["short bow"]
|
"attacks": ["short bow"]
|
||||||
|
|
@ -104,7 +107,7 @@ ARCHWIZARD = {
|
||||||
|
|
||||||
GOBLIN_ARCHWIZARD = {
|
GOBLIN_ARCHWIZARD = {
|
||||||
"key" : "goblin archwizard"
|
"key" : "goblin archwizard"
|
||||||
"prototype_parent": (GOBLIN_WIZARD, ARCHWIZARD),
|
"prototype_parent": ("GOBLIN_WIZARD", "ARCHWIZARD"),
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ LIMBO_DESC = _(
|
||||||
"""
|
"""
|
||||||
Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if you need
|
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.
|
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'.
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -397,7 +397,7 @@ if WEBSERVER_ENABLED:
|
||||||
w_interface = WEBSOCKET_CLIENT_INTERFACE
|
w_interface = WEBSOCKET_CLIENT_INTERFACE
|
||||||
w_ifacestr = ""
|
w_ifacestr = ""
|
||||||
if w_interface not in ("0.0.0.0", "::") or len(WEBSERVER_INTERFACES) > 1:
|
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
|
port = WEBSOCKET_CLIENT_PORT
|
||||||
|
|
||||||
class Websocket(WebSocketServerFactory):
|
class Websocket(WebSocketServerFactory):
|
||||||
|
|
|
||||||
|
|
@ -239,16 +239,16 @@ class PortalSessionHandler(SessionHandler):
|
||||||
def server_connect(self, protocol_path="", config=dict()):
|
def server_connect(self, protocol_path="", config=dict()):
|
||||||
"""
|
"""
|
||||||
Called by server to force the initialization of a new protocol
|
Called by server to force the initialization of a new protocol
|
||||||
instance. Server wants this instance to get a unique sessid
|
instance. Server wants this instance to get a unique sessid and to be
|
||||||
and to be connected back as normal. This is used to initiate
|
connected back as normal. This is used to initiate irc/rss etc
|
||||||
irc/rss etc connections.
|
connections.
|
||||||
|
|
||||||
Args:
|
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
|
for the protocol used, eg
|
||||||
'evennia.server.portal.irc.IRCClientFactory'
|
'evennia.server.portal.irc.IRCClientFactory'
|
||||||
config (dict): Dictionary of configuration options, fed as
|
config (dict): Dictionary of configuration options, fed as
|
||||||
`**kwargs` to protocol class' __init__ method.
|
`**kwarg` to protocol class `__init__` method.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
RuntimeError: If The correct factory class is not found.
|
RuntimeError: If The correct factory class is not found.
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,7 @@ This implements the following telnet OOB communication protocols:
|
||||||
- GMCP (Generic Mud Communication Protocol) as per
|
- GMCP (Generic Mud Communication Protocol) as per
|
||||||
http://www.ironrealms.com/rapture/manual/files/FeatGMCP-txt.html#Generic_MUD_Communication_Protocol%28GMCP%29
|
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
|
import re
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,16 @@ class AjaxWebClient(resource.Resource):
|
||||||
csessid = self.get_client_sessid(request)
|
csessid = self.get_client_sessid(request)
|
||||||
|
|
||||||
remote_addr = request.getClientIP()
|
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)" % (
|
host_string = "%s (%s:%s)" % (
|
||||||
_SERVERNAME,
|
_SERVERNAME,
|
||||||
request.getRequestHostname(),
|
request.getRequestHostname(),
|
||||||
|
|
|
||||||
|
|
@ -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
|
(no randomness) and allows for setting up a more complex chain of
|
||||||
commands (such as creating an account and logging in).
|
commands (such as creating an account and logging in).
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Dummy runner settings
|
# Dummy runner settings
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ both one or two arguments interchangeably. It also accepts nodes
|
||||||
that takes `**kwargs`.
|
that takes `**kwargs`.
|
||||||
|
|
||||||
The menu tree itself is available on the caller as
|
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
|
temporary state variables between nodes, since this NAttribute is
|
||||||
deleted when the menu is exited.
|
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
|
import inspect
|
||||||
|
|
||||||
|
from ast import literal_eval
|
||||||
|
from fnmatch import fnmatch
|
||||||
|
|
||||||
from inspect import isfunction, getargspec
|
from inspect import isfunction, getargspec
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia import Command, CmdSet
|
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.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
|
||||||
from evennia.commands import cmdhandler
|
from evennia.commands import cmdhandler
|
||||||
|
|
||||||
|
# i18n
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
# read from protocol NAWS later?
|
# read from protocol NAWS later?
|
||||||
_MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
_MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||||
|
|
||||||
|
|
@ -189,11 +295,10 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
|
||||||
|
|
||||||
# Return messages
|
# Return messages
|
||||||
|
|
||||||
# i18n
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
|
|
||||||
_ERR_NOT_IMPLEMENTED = _(
|
_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_GENERAL = _("Error in menu node '{nodename}'.")
|
||||||
_ERR_NO_OPTION_DESC = _("No description.")
|
_ERR_NO_OPTION_DESC = _("No description.")
|
||||||
|
|
@ -227,6 +332,19 @@ class EvMenuError(RuntimeError):
|
||||||
pass
|
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
|
# Menu command and command set
|
||||||
|
|
@ -243,6 +361,10 @@ class CmdEvMenuNode(Command):
|
||||||
aliases = [_CMD_NOMATCH]
|
aliases = [_CMD_NOMATCH]
|
||||||
locks = "cmd:all()"
|
locks = "cmd:all()"
|
||||||
help_category = "Menu"
|
help_category = "Menu"
|
||||||
|
auto_help_display_key = "<menu commands>"
|
||||||
|
|
||||||
|
def get_help(self):
|
||||||
|
return "Menu commands are explained within the menu."
|
||||||
|
|
||||||
def func(self):
|
def func(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -271,28 +393,28 @@ class CmdEvMenuNode(Command):
|
||||||
caller = self.caller
|
caller = self.caller
|
||||||
# we store Session on the menu since this can be hard to
|
# we store Session on the menu since this can be hard to
|
||||||
# get in multisession environemtns if caller is an Account.
|
# get in multisession environemtns if caller is an Account.
|
||||||
menu = caller.ndb._menutree
|
menu = caller.ndb._evmenu
|
||||||
if not menu:
|
if not menu:
|
||||||
if _restore(caller):
|
if _restore(caller):
|
||||||
return
|
return
|
||||||
orig_caller = caller
|
orig_caller = caller
|
||||||
caller = caller.account if hasattr(caller, "account") else None
|
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 not menu:
|
||||||
if caller and _restore(caller):
|
if caller and _restore(caller):
|
||||||
return
|
return
|
||||||
caller = self.session
|
caller = self.session
|
||||||
menu = caller.ndb._menutree
|
menu = caller.ndb._evmenu
|
||||||
if not menu:
|
if not menu:
|
||||||
# can't restore from a session
|
# 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(
|
orig_caller.msg(
|
||||||
err
|
err
|
||||||
) # don't give the session as a kwarg here, direct to original
|
) # don't give the session as a kwarg here, direct to original
|
||||||
raise EvMenuError(err)
|
raise EvMenuError(err)
|
||||||
# we must do this after the caller with the menu has been correctly identified since it
|
# 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).
|
# 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.
|
# we have a menu, use it.
|
||||||
menu.parse_input(self.raw_string)
|
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
|
This object represents an operational menu. It is initialized from
|
||||||
a menufile.py instruction.
|
a menufile.py instruction.
|
||||||
|
|
@ -425,9 +547,9 @@ class EvMenu(object):
|
||||||
EvMenuError: If the start/end node is not found in menu tree.
|
EvMenuError: If the start/end node is not found in menu tree.
|
||||||
|
|
||||||
Notes:
|
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
|
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
|
property is a good one for storing intermediary data on between nodes since it
|
||||||
will be automatically deleted when the menu closes.
|
will be automatically deleted when the menu closes.
|
||||||
|
|
||||||
|
|
@ -478,7 +600,7 @@ class EvMenu(object):
|
||||||
self.test_nodetext = ""
|
self.test_nodetext = ""
|
||||||
|
|
||||||
# assign kwargs as initialization vars on ourselves.
|
# assign kwargs as initialization vars on ourselves.
|
||||||
if set(
|
reserved_clash = set(
|
||||||
(
|
(
|
||||||
"_startnode",
|
"_startnode",
|
||||||
"_menutree",
|
"_menutree",
|
||||||
|
|
@ -492,22 +614,26 @@ class EvMenu(object):
|
||||||
"cmdset_mergetype",
|
"cmdset_mergetype",
|
||||||
"auto_quit",
|
"auto_quit",
|
||||||
)
|
)
|
||||||
).intersection(set(kwargs.keys())):
|
).intersection(set(kwargs.keys()))
|
||||||
|
if reserved_clash:
|
||||||
raise RuntimeError(
|
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():
|
for key, val in kwargs.items():
|
||||||
setattr(self, key, val)
|
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
|
# an evmenu already exists - we try to close it cleanly. Note that this will
|
||||||
# not fire the previous menu's end node.
|
# not fire the previous menu's end node.
|
||||||
try:
|
try:
|
||||||
self.caller.ndb._menutree.close_menu()
|
self.caller.ndb._evmenu.close_menu()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# store ourself on the object
|
# store ourself on the object
|
||||||
|
self.caller.ndb._evmenu = self
|
||||||
|
|
||||||
|
# DEPRECATED - for backwards-compatibility
|
||||||
self.caller.ndb._menutree = self
|
self.caller.ndb._menutree = self
|
||||||
|
|
||||||
if persistent:
|
if persistent:
|
||||||
|
|
@ -527,7 +653,7 @@ class EvMenu(object):
|
||||||
caller.attributes.add("_menutree_saved", (self.__class__, (menudata,), calldict))
|
caller.attributes.add("_menutree_saved", (self.__class__, (menudata,), calldict))
|
||||||
caller.attributes.add("_menutree_saved_startnode", (startnode, startnode_input))
|
caller.attributes.add("_menutree_saved_startnode", (startnode, startnode_input))
|
||||||
except Exception as err:
|
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)
|
logger.log_trace(_TRACE_PERSISTENT_SAVING)
|
||||||
persistent = False
|
persistent = False
|
||||||
|
|
||||||
|
|
@ -537,11 +663,19 @@ class EvMenu(object):
|
||||||
menu_cmdset.priority = int(cmdset_priority)
|
menu_cmdset.priority = int(cmdset_priority)
|
||||||
self.caller.cmdset.add(menu_cmdset, permanent=persistent)
|
self.caller.cmdset.add(menu_cmdset, permanent=persistent)
|
||||||
|
|
||||||
|
reserved_startnode_kwargs = set(("nodename", "raw_string"))
|
||||||
startnode_kwargs = {}
|
startnode_kwargs = {}
|
||||||
if isinstance(startnode_input, (tuple, list)) and len(startnode_input) > 1:
|
if isinstance(startnode_input, (tuple, list)) and len(startnode_input) > 1:
|
||||||
startnode_input, startnode_kwargs = startnode_input[:2]
|
startnode_input, startnode_kwargs = startnode_input[:2]
|
||||||
if not isinstance(startnode_kwargs, dict):
|
if not isinstance(startnode_kwargs, dict):
|
||||||
raise EvMenuError("startnode_input must be either a str or a tuple (str, 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
|
# start the menu
|
||||||
self.goto(self._startnode, startnode_input, **startnode_kwargs)
|
self.goto(self._startnode, startnode_input, **startnode_kwargs)
|
||||||
|
|
||||||
|
|
@ -631,7 +765,7 @@ class EvMenu(object):
|
||||||
ret = callback(self.caller)
|
ret = callback(self.caller)
|
||||||
except EvMenuError:
|
except EvMenuError:
|
||||||
errmsg = _ERR_GENERAL.format(nodename=callback)
|
errmsg = _ERR_GENERAL.format(nodename=callback)
|
||||||
self.caller.msg(errmsg, self._session)
|
self.msg(errmsg)
|
||||||
logger.log_trace()
|
logger.log_trace()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
@ -656,20 +790,21 @@ class EvMenu(object):
|
||||||
try:
|
try:
|
||||||
node = self._menutree[nodename]
|
node = self._menutree[nodename]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
self.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename))
|
||||||
raise EvMenuError
|
raise EvMenuError
|
||||||
try:
|
try:
|
||||||
|
kwargs["_current_nodename"] = nodename
|
||||||
ret = self._safe_call(node, raw_string, **kwargs)
|
ret = self._safe_call(node, raw_string, **kwargs)
|
||||||
if isinstance(ret, (tuple, list)) and len(ret) > 1:
|
if isinstance(ret, (tuple, list)) and len(ret) > 1:
|
||||||
nodetext, options = ret[:2]
|
nodetext, options = ret[:2]
|
||||||
else:
|
else:
|
||||||
nodetext, options = ret, None
|
nodetext, options = ret, None
|
||||||
except KeyError:
|
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()
|
logger.log_trace()
|
||||||
raise EvMenuError
|
raise EvMenuError
|
||||||
except Exception:
|
except Exception:
|
||||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
|
self.msg(_ERR_GENERAL.format(nodename=nodename))
|
||||||
logger.log_trace()
|
logger.log_trace()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
@ -679,8 +814,27 @@ class EvMenu(object):
|
||||||
|
|
||||||
return nodetext, options
|
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):
|
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).
|
Run a function or node as a callback (with the 'exec' option key).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -723,7 +877,7 @@ class EvMenu(object):
|
||||||
ret, kwargs = ret[:2]
|
ret, kwargs = ret[:2]
|
||||||
except EvMenuError as err:
|
except EvMenuError as err:
|
||||||
errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string.rstrip(), 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)
|
logger.log_trace(errmsg)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -904,12 +1058,14 @@ class EvMenu(object):
|
||||||
# avoid multiple calls from different sources
|
# avoid multiple calls from different sources
|
||||||
self._quitting = True
|
self._quitting = True
|
||||||
self.caller.cmdset.remove(EvMenuCmdSet)
|
self.caller.cmdset.remove(EvMenuCmdSet)
|
||||||
del self.caller.ndb._menutree
|
del self.caller.ndb._evmenu
|
||||||
if self._persistent:
|
if self._persistent:
|
||||||
self.caller.attributes.remove("_menutree_saved")
|
self.caller.attributes.remove("_menutree_saved")
|
||||||
self.caller.attributes.remove("_menutree_saved_startnode")
|
self.caller.attributes.remove("_menutree_saved_startnode")
|
||||||
if self.cmd_on_exit is not None:
|
if self.cmd_on_exit is not None:
|
||||||
self.cmd_on_exit(self.caller, self)
|
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):
|
def print_debug_info(self, arg):
|
||||||
"""
|
"""
|
||||||
|
|
@ -968,7 +1124,7 @@ class EvMenu(object):
|
||||||
)
|
)
|
||||||
+ "\n |y... END MENU DEBUG|n"
|
+ "\n |y... END MENU DEBUG|n"
|
||||||
)
|
)
|
||||||
self.caller.msg(debugtxt)
|
self.msg(debugtxt)
|
||||||
|
|
||||||
def parse_input(self, raw_string):
|
def parse_input(self, raw_string):
|
||||||
"""
|
"""
|
||||||
|
|
@ -985,30 +1141,35 @@ class EvMenu(object):
|
||||||
"""
|
"""
|
||||||
cmd = strip_ansi(raw_string.strip().lower())
|
cmd = strip_ansi(raw_string.strip().lower())
|
||||||
|
|
||||||
if cmd in self.options:
|
try:
|
||||||
# this will take precedence over the default commands
|
if self.options and cmd in self.options:
|
||||||
# below
|
# this will take precedence over the default commands
|
||||||
goto, goto_kwargs, execfunc, exec_kwargs = self.options[cmd]
|
# below
|
||||||
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
goto, goto_kwargs, execfunc, exec_kwargs = self.options[cmd]
|
||||||
elif self.auto_look and cmd in ("look", "l"):
|
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||||
self.display_nodetext()
|
elif self.auto_look and cmd in ("look", "l"):
|
||||||
elif self.auto_help and cmd in ("help", "h"):
|
self.display_nodetext()
|
||||||
self.display_helptext()
|
elif self.auto_help and cmd in ("help", "h"):
|
||||||
elif self.auto_quit and cmd in ("quit", "q", "exit"):
|
self.display_helptext()
|
||||||
self.close_menu()
|
elif self.auto_quit and cmd in ("quit", "q", "exit"):
|
||||||
elif self.debug_mode and cmd.startswith("menudebug"):
|
self.close_menu()
|
||||||
self.print_debug_info(cmd[9:].strip())
|
elif self.debug_mode and cmd.startswith("menudebug"):
|
||||||
elif self.default:
|
self.print_debug_info(cmd[9:].strip())
|
||||||
goto, goto_kwargs, execfunc, exec_kwargs = self.default
|
elif self.default:
|
||||||
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
goto, goto_kwargs, execfunc, exec_kwargs = self.default
|
||||||
else:
|
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||||
self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session)
|
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):
|
def display_nodetext(self):
|
||||||
self.caller.msg(self.nodetext, session=self._session)
|
self.msg(self.nodetext)
|
||||||
|
|
||||||
def display_helptext(self):
|
def display_helptext(self):
|
||||||
self.caller.msg(self.helptext, session=self._session)
|
self.msg(self.helptext)
|
||||||
|
|
||||||
# formatters - override in a child class
|
# 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):
|
_HELP_NO_OPTION_MATCH = _("Choose an option or try 'help'.")
|
||||||
return kwargs.get("name", "test_dynamic_node"), {"name": "replaced!"}
|
|
||||||
|
_OPTION_INPUT_MARKER = ">"
|
||||||
|
_OPTION_ALIAS_MARKER = ";"
|
||||||
|
_OPTION_SEP_MARKER = ":"
|
||||||
|
_OPTION_CALL_MARKER = "->"
|
||||||
|
_OPTION_COMMENT_START = "#"
|
||||||
|
|
||||||
|
|
||||||
def test_start_node(caller):
|
# Input/option/goto handler functions that allows for dynamically generated
|
||||||
menu = caller.ndb._menutree
|
# nodes read from the menu template.
|
||||||
text = """
|
|
||||||
This is an example menu.
|
|
||||||
|
|
||||||
If you enter anything except the valid options, your input will be
|
def _process_callable(caller, goto, goto_callables, raw_string,
|
||||||
recorded and you will be brought to a menu entry showing your
|
current_nodename, kwargs):
|
||||||
input.
|
"""
|
||||||
|
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.
|
goto = goto_callables[gotofunc](caller, raw_string, **kwargs)
|
||||||
""" % (
|
if goto is None:
|
||||||
menu.testval,
|
return goto, {"generated_nodename": current_nodename}
|
||||||
menu.testval2,
|
return goto, {"generated_nodename": goto}
|
||||||
)
|
|
||||||
|
|
||||||
options = (
|
|
||||||
{
|
def _generated_goto_func(caller, raw_string, **kwargs):
|
||||||
"key": ("|yS|net", "s"),
|
"""
|
||||||
"desc": "Set an attribute on yourself.",
|
This rerouter handles normal direct goto func call matches.
|
||||||
"exec": lambda caller: caller.attributes.add("menuattrtest", "Test value"),
|
|
||||||
"goto": "test_set_node",
|
key : ... -> goto_callable(**kwargs)
|
||||||
},
|
|
||||||
{
|
"""
|
||||||
"key": ("|yL|nook", "l"),
|
goto = kwargs["evmenu_goto"]
|
||||||
"desc": "Look and see a custom message.",
|
goto_callables = kwargs["evmenu_goto_callables"]
|
||||||
"goto": "test_look_node",
|
current_nodename = kwargs["evmenu_current_nodename"]
|
||||||
},
|
return _process_callable(caller, goto, goto_callables, raw_string,
|
||||||
{"key": ("|yV|niew", "v"), "desc": "View your own name", "goto": "test_view_node"},
|
current_nodename, kwargs)
|
||||||
{
|
|
||||||
"key": ("|yD|nynamic", "d"),
|
|
||||||
"desc": "Dynamic node",
|
def _generated_input_goto_func(caller, raw_string, **kwargs):
|
||||||
"goto": (_generate_goto, {"name": "test_dynamic_node"}),
|
"""
|
||||||
},
|
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
|
||||||
"key": ("|yQ|nuit", "quit", "q", "Q"),
|
*actual* goto-funcs. We map to those here.
|
||||||
"desc": "Quit this menu example.",
|
|
||||||
"goto": "test_end_node",
|
>pattern: ... -> goto_callable
|
||||||
},
|
|
||||||
{"key": "_default", "goto": "test_displayinput_node"},
|
"""
|
||||||
)
|
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
|
return text, options
|
||||||
|
|
||||||
|
|
||||||
def test_look_node(caller):
|
def parse_menu_template(caller, menu_template, goto_callables=None):
|
||||||
text = "This is a custom look location!"
|
"""
|
||||||
options = {
|
Parse menu-template string. The main function of the EvMenu templating system.
|
||||||
"key": ("|yL|nook", "l"),
|
|
||||||
"desc": "Go back to the previous menu.",
|
|
||||||
"goto": "test_start_node",
|
|
||||||
}
|
|
||||||
return text, options
|
|
||||||
|
|
||||||
|
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):
|
Returns:
|
||||||
text = (
|
dict: A `{"node": nodefunc}` menutree suitable to pass into EvMenu.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def _validate_kwarg(goto, kwarg):
|
||||||
"""
|
"""
|
||||||
The attribute 'menuattrtest' was set to
|
Validate goto-callable kwarg is on correct form.
|
||||||
|
|
||||||
|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
|
if not "=" in kwarg:
|
||||||
the node text as a tuple - the second string in that tuple will be
|
raise RuntimeError(
|
||||||
used as the help text.
|
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"}
|
def _parse_options(nodename, optiontxt, goto_callables):
|
||||||
return text, options
|
|
||||||
|
|
||||||
|
|
||||||
def test_view_node(caller, **kwargs):
|
|
||||||
text = (
|
|
||||||
"""
|
"""
|
||||||
Your name is |g%s|n!
|
Parse option section into option dict.
|
||||||
|
|
||||||
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:
|
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
|
key, goto = [part.strip() for part in optline.split(_OPTION_SEP_MARKER, 1)]
|
||||||
was not just an example.
|
|
||||||
|
|
||||||
This node has an option with a single alias "_default", which
|
# desc -> goto
|
||||||
makes it hidden from view. It catches all input (except the
|
if _OPTION_CALL_MARKER in goto:
|
||||||
in-menu help/quit commands) and will, in this case, bring you back
|
desc, goto = [part.strip() for part in goto.split(_OPTION_CALL_MARKER, 1)]
|
||||||
to the start node.
|
|
||||||
|
# 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()
|
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
|
||||||
options = {"key": "_default", "goto": "test_start_node"}
|
EvMenu with its results.
|
||||||
return text, options
|
|
||||||
|
|
||||||
|
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):
|
Returns:
|
||||||
mode = kwargs.get("mode", "exec")
|
EvMenu: The generated EvMenu.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
goto_callables = goto_callables or {}
|
||||||
key = "testmenu"
|
menu_tree = parse_menu_template(caller, menu_template, goto_callables)
|
||||||
|
return EvMenu(
|
||||||
def func(self):
|
caller,
|
||||||
|
menu_tree,
|
||||||
if not self.args:
|
persistent=persistent,
|
||||||
self.caller.msg("Usage: testmenu menumodule")
|
**kwargs,
|
||||||
return
|
)
|
||||||
# start menu
|
|
||||||
EvMenu(
|
|
||||||
self.caller,
|
|
||||||
self.args.strip(),
|
|
||||||
startnode="test_start_node",
|
|
||||||
persistent=True,
|
|
||||||
cmdset_mergetype="Replace",
|
|
||||||
testval="val",
|
|
||||||
testval2="val2",
|
|
||||||
)
|
|
||||||
|
|
|
||||||
221
evennia/utils/tests/data/evmenu_example.py
Normal file
221
evennia/utils/tests/data/evmenu_example.py
Normal 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",
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
|
@ -18,7 +18,9 @@ To help debug the menu, turn on `debug_output`, which will print the traversal p
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
from anything import Anything
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
from evennia.utils import evmenu
|
from evennia.utils import evmenu
|
||||||
from evennia.utils import ansi
|
from evennia.utils import ansi
|
||||||
from mock import MagicMock
|
from mock import MagicMock
|
||||||
|
|
@ -229,7 +231,7 @@ class TestEvMenu(TestCase):
|
||||||
|
|
||||||
class TestEvMenuExample(TestEvMenu):
|
class TestEvMenuExample(TestEvMenu):
|
||||||
|
|
||||||
menutree = "evennia.utils.evmenu"
|
menutree = "evennia.utils.tests.data.evmenu_example"
|
||||||
startnode = "test_start_node"
|
startnode = "test_start_node"
|
||||||
kwargs = {"testval": "val", "testval2": "val2"}
|
kwargs = {"testval": "val", "testval2": "val2"}
|
||||||
debug_output = False
|
debug_output = False
|
||||||
|
|
@ -262,3 +264,79 @@ class TestEvMenuExample(TestEvMenu):
|
||||||
def test_kwargsave(self):
|
def test_kwargsave(self):
|
||||||
self.assertTrue(hasattr(self.menu, "testval"))
|
self.assertTrue(hasattr(self.menu, "testval"))
|
||||||
self.assertTrue(hasattr(self.menu, "testval2"))
|
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)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
let goldenlayout = (function () {
|
let goldenlayout = (function () {
|
||||||
|
|
||||||
var myLayout;
|
var myLayout;
|
||||||
var knownTypes = ["all", "untagged"];
|
var knownTypes = ["all", "untagged", "testing"];
|
||||||
var untagged = [];
|
var untagged = [];
|
||||||
|
|
||||||
var newTabConfig = {
|
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
|
// If the message is not itself tagged, we"ll assume it
|
||||||
// should go into any panes with "all" and "untagged" set
|
// should go into any panes with "all" and "untagged" set
|
||||||
|
var divArray = [];
|
||||||
var msgtype = "untagged";
|
var msgtype = "untagged";
|
||||||
|
|
||||||
if ( kwargs && "type" in kwargs ) {
|
if ( kwargs && "type" in kwargs ) {
|
||||||
|
|
@ -419,37 +421,47 @@ let goldenlayout = (function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let messageDelivered = false;
|
|
||||||
let components = myLayout.root.getItemsByType("component");
|
let components = myLayout.root.getItemsByType("component");
|
||||||
|
|
||||||
components.forEach( function (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 destDiv = component.container.getElement().children(".content");
|
||||||
let attrTypes = textDiv.attr("types");
|
let attrTypes = destDiv.attr("types");
|
||||||
let paneTypes = attrTypes ? attrTypes.split(" ") : [];
|
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")
|
// is this message type listed in this pane"s types (or is this pane catching "all")
|
||||||
if( paneTypes.includes(msgtype) || paneTypes.includes("all") ) {
|
if( paneTypes.includes(msgtype) || paneTypes.includes("all") ) {
|
||||||
routeMsg( textDiv, txt, updateMethod );
|
divArray.push(destDiv);
|
||||||
messageDelivered = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// is this pane catching "upmapped" messages?
|
// is this pane catching "upmapped" messages?
|
||||||
// And is this message type listed in the untagged types array?
|
// And is this message type listed in the untagged types array?
|
||||||
if( paneTypes.includes("untagged") && untagged.includes(msgtype) ) {
|
if( paneTypes.includes("untagged") && untagged.includes(msgtype) ) {
|
||||||
routeMsg( textDiv, txt, updateMethod );
|
divArray.push(destDiv);
|
||||||
messageDelivered = true;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if ( messageDelivered ) {
|
return divArray;
|
||||||
return true;
|
}
|
||||||
}
|
|
||||||
// unhandled message
|
|
||||||
return false;
|
//
|
||||||
|
//
|
||||||
|
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; },
|
getGL: function () { return myLayout; },
|
||||||
addKnownType: addKnownType,
|
addKnownType: addKnownType,
|
||||||
onTabCreate: onTabCreate,
|
onTabCreate: onTabCreate,
|
||||||
|
routeMessage: routeMessage,
|
||||||
}
|
}
|
||||||
}());
|
}());
|
||||||
window.plugin_handler.add("goldenlayout", goldenlayout);
|
window.plugin_handler.add("goldenlayout", goldenlayout);
|
||||||
|
|
|
||||||
|
|
@ -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( image="URL" )
|
||||||
* target.msg( audio="URL" )
|
* target.msg( audio="URL" )
|
||||||
* target.msg( video="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 () {
|
let multimedia_plugin = (function () {
|
||||||
//
|
//
|
||||||
var image = function (args, kwargs) {
|
var image = function (args, kwargs) {
|
||||||
var mwin = $("#messagewindow");
|
let options = window.options;
|
||||||
mwin.append("<img src='"+ args[0] +"'/>");
|
if( !("mm_image" in options) || options["mm_image"] === false ) { return; }
|
||||||
mwin.scrollTop(mwin[0].scrollHeight);
|
|
||||||
|
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) {
|
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)
|
// create an HTML5 audio control (only .mp3 is fully compatible with all major browsers)
|
||||||
var mwin = $("#messagewindow");
|
var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
|
||||||
mwin.append("<audio controls='' autoplay='' style='height:17px;width:175px'>" +
|
mwins.forEach( function (mwin) {
|
||||||
"<source src='"+ args[0] +"'/>" +
|
mwin.append("<audio controls='' autoplay='' style='height:17px;width:175px'>" +
|
||||||
"</audio>");
|
"<source src='"+ args[0] +"'/>" +
|
||||||
mwin.scrollTop(mwin[0].scrollHeight);
|
"</audio>");
|
||||||
|
mwin.scrollTop(mwin[0].scrollHeight);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
var video = function (args, kwargs) {
|
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)
|
// create an HTML5 video element (only h264 .mp4 is compatible with all major browsers)
|
||||||
var mwin = $("#messagewindow");
|
var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
|
||||||
mwin.append("<video controls='' autoplay=''>" +
|
mwins.forEach( function (mwin) {
|
||||||
"<source src='"+ args[0] +"'/>" +
|
mwin.append("<video controls='' autoplay=''>" +
|
||||||
"</video>");
|
"<source src='"+ args[0] +"'/>" +
|
||||||
mwin.scrollTop(mwin[0].scrollHeight);
|
"</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
|
// Mandatory plugin init function
|
||||||
var init = function () {
|
var init = function () {
|
||||||
Evennia = window.Evennia;
|
let options = window.options;
|
||||||
Evennia.emitter.on('image', image); // capture "image" commands
|
options["mm_image"] = true;
|
||||||
Evennia.emitter.on('audio', audio); // capture "audio" commands
|
options["mm_audio"] = true;
|
||||||
Evennia.emitter.on('video', video); // capture "video" commands
|
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');
|
console.log('Multimedia plugin initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init: init,
|
init: init,
|
||||||
|
onOptionsUI: onOptionsUI,
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
plugin_handler.add('multimedia_plugin', multimedia_plugin);
|
plugin_handler.add("multimedia_plugin", multimedia_plugin);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ let options2 = (function () {
|
||||||
onLoggedIn: onLoggedIn,
|
onLoggedIn: onLoggedIn,
|
||||||
onOptionsUI: onOptionsUI,
|
onOptionsUI: onOptionsUI,
|
||||||
onPrompt: onPrompt,
|
onPrompt: onPrompt,
|
||||||
|
onOptionCheckboxChanged: onOptionCheckboxChanged,
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
window.plugin_handler.add("options2", options2);
|
window.plugin_handler.add("options2", options2);
|
||||||
|
|
|
||||||
|
|
@ -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">⇹</button>') );
|
|
||||||
toolbar.append( $('<button id="panebutton" type="button">⚙</button>') );
|
|
||||||
toolbar.append( $('<button id="undobutton" type="button">↶</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);
|
|
||||||
|
|
@ -64,7 +64,6 @@ JQuery available.
|
||||||
|
|
||||||
<!-- set up splits before loading the GUI -->
|
<!-- 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 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>
|
<script type="text/javascript" src="https://golden-layout.com/files/latest/js/goldenlayout.min.js"></script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue