mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
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)
|
||||
|
||||
- new `drop:holds()` lock default to limit dropping nonsensical things. Access check
|
||||
- New `drop:holds()` lock default to limit dropping nonsensical things. Access check
|
||||
defaults to True for backwards-compatibility in 0.9, will be False in 1.0
|
||||
- REST API allows you external access to db objects through HTTP requests (Tehom)
|
||||
- `Object.normalize_name` and `.validate_name` added to (by default) enforce latinify
|
||||
|
|
@ -20,10 +20,10 @@
|
|||
- Change default multimatch syntax from 1-obj, 2-obj to obj-1, obj-2.
|
||||
- Make `object.search` support 'stacks=0' keyword - if ``>0``, the method will return
|
||||
N identical matches instead of triggering a multi-match error.
|
||||
- Add `tags.has()` method for checking if an object has a tag or tags (PR by ChrisLR)
|
||||
|
||||
### Already in master
|
||||
- Renamed Tutorial classes "Weapon" and "WeaponRack" to "TutorialWeapon" and
|
||||
"TutorialWeaponRack" to prevent collisions with classes in mygame
|
||||
|
||||
### Evennia 0.95 (master)
|
||||
- `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False
|
||||
- `py` command now reroutes stdout to output results in-game client. `py`
|
||||
without arguments starts a full interactive Python console.
|
||||
|
|
@ -92,7 +92,14 @@ without arguments starts a full interactive Python console.
|
|||
pagination (e.g. to create EvTables for every page instead of splittine one table)
|
||||
- Using `EvMore pagination`, dramatically improves performance of `spawn/list` and `scripts` listings
|
||||
(100x speed increase for displaying 1000+ prototypes/scripts).
|
||||
|
||||
- `EvMenu` now uses the more logically named `.ndb._evmenu` instead of `.ndb._menutree` to store itself.
|
||||
Both still work for backward compatibility, but `_menutree` is deprecated.
|
||||
- `EvMenu.msg(txt)` added as a central place to send text to the user, makes it easier to override.
|
||||
Default `EvMenu.msg` sends with OOB type="menu" for use with OOB and webclient pane-redirects.
|
||||
- New EvMenu templating system for quickly building simpler EvMenus without as much code.
|
||||
- Renamed Tutorial classes "Weapon" and "WeaponRack" to "TutorialWeapon" and
|
||||
"TutorialWeaponRack" to prevent collisions with classes in mygame
|
||||
|
||||
|
||||
## Evennia 0.9 (2018-2019)
|
||||
|
||||
|
|
|
|||
|
|
@ -109,19 +109,15 @@ from the server and display them as inline HTML.
|
|||
* `notifications.js` Defines onText. Generates browser notification events for each new message
|
||||
while the tab is hidden.
|
||||
* `oob.js` Defines onSend. Allows the user to test/send Out Of Band json messages to the server.
|
||||
* `options.js` Defines most callbacks. Provides a popup-based UI to coordinate options settings with
|
||||
the server.
|
||||
* `options2.js` Defines most callbacks. Provides a goldenlayout-based version of the
|
||||
options/settings tab. Integrates with other plugins via the custom onOptionsUI callback.
|
||||
* `popups.js` Provides default popups/Dialog UI for other plugins to use.
|
||||
* `splithandler.js` Defines onText. Provides an older, less-flexible alternative to goldenlayout for
|
||||
multi-window UI to automatically separate out screen real-estate by type of message.
|
||||
* `options.js` Defines most callbacks. Provides a popup-based UI to coordinate options settings with the server.
|
||||
* `options2.js` Defines most callbacks. Provides a goldenlayout-based version of the options/settings tab. Integrates with other plugins via the custom onOptionsUI callback.
|
||||
* `popups.js` Provides default popups/Dialog UI for other plugins to use.
|
||||
|
||||
# Writing your own Plugins
|
||||
|
||||
So, you love the functionality of the webclient, but your game has specific types of text that need
|
||||
to be separated out into their own space, visually. There are two plugins to help with this. The
|
||||
Goldenlayout plugin framework, and the older Splithandler framework.
|
||||
So, you love the functionality of the webclient, but your game has specific
|
||||
types of text that need to be separated out into their own space, visually.
|
||||
The Goldenlayout plugin framework can help with this.
|
||||
|
||||
## GoldenLayout
|
||||
|
||||
|
|
@ -258,87 +254,4 @@ window.plugin_handler.add("myplugin", myplugin);
|
|||
```
|
||||
You can then add "mycomponent" to an item's componentName in your goldenlayout_default_config.js.
|
||||
|
||||
Make sure to stop your server, evennia collectstatic, and restart your server. Then make sure to
|
||||
clear your browser cache before loading the webclient page.
|
||||
|
||||
|
||||
## Older Splithandler
|
||||
The splithandler.js plugin provides a means to do this, but you don't want to have to force every
|
||||
player to set up their own layout every time they use the client.
|
||||
|
||||
Let's create a `mygame/web/static_overrides/webclient/js/plugins/layout.js` plugin!
|
||||
|
||||
First up, follow the directions in Customizing the Web Client section above to override the
|
||||
base.html.
|
||||
|
||||
Next, add the new plugin to your copy of base.html:
|
||||
```
|
||||
<script src={% static "webclient/js/plugins/layout.js" %} language="javascript"
|
||||
type="text/javascript"></script>
|
||||
```
|
||||
Remember, plugins are load-order dependent, so make sure the new `<script>` tag comes after the
|
||||
splithandler.js
|
||||
|
||||
And finally create the layout.js file and add the minimum skeleton of a plugin to it:
|
||||
|
||||
```
|
||||
// my new plugin
|
||||
var my_plugin = (function () {
|
||||
let init = function () {
|
||||
console.log("myplugin! Hello World!");
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
}
|
||||
})();
|
||||
plugin_handler.add("myplugin", my_plugin);
|
||||
```
|
||||
|
||||
Now, `evennia stop`, `evennia collectstatic`, and `evennia start` and then load the webclient up in
|
||||
your browser.
|
||||
Enable developer options and look in the console, and you should see the message 'myplugin! Hello
|
||||
World!'
|
||||
|
||||
Since our layout.js plugin is going to use the splithandler, let's enhance this by adding a check to
|
||||
make sure the splithandler.js plugin has been loaded:
|
||||
|
||||
change the above init function to:
|
||||
```
|
||||
let init = function () {
|
||||
let splithandler = plugins['splithandler'];
|
||||
if( splithandler ) {
|
||||
console.log("MyPlugin initialized");
|
||||
} else {
|
||||
alert('MyPlugin requires the splithandler.js plugin. Please contact the game maintainer to
|
||||
correct this');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And finally, the splithandler.js provides provides two functions to cut up the screen real-estate:
|
||||
`dynamic_split( pane_name_to_cut_apart, direction_of_split, new_pane_name1, new_pane_name2,
|
||||
text_flow_pane1, text_flow_pane2, array_of_split_percentages )`
|
||||
and
|
||||
`set_pane_types( pane_to_set, array_of_known_message_types_to_assign)`
|
||||
|
||||
In this case, we'll cut it into 3 panes, 1 bigger, two smaller, and assign 'help' messages to the
|
||||
top-right pane:
|
||||
```
|
||||
let init = function () {
|
||||
let splithandler = plugins['splithandler'];
|
||||
if( splithandler ) {
|
||||
splithandler.dynamic_split("main","horizontal","left","right","linefeed","linefeed",[50,50]);
|
||||
splithandler.dynamic_split("right","vertical","help","misc","replace","replace",[50,50]);
|
||||
splithandler.set_pane_types('help', ['help']);
|
||||
|
||||
console.log("MyPlugin initialized");
|
||||
} else {
|
||||
alert('MyPlugin requires the splithandler.js plugin. Please contact the game maintainer to
|
||||
correct this');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`evennia stop`, `evennia collectstatic`, and `evennia start` once more, and force-reload your
|
||||
browser page to clear any cached version. You should now have a nicely split layout.
|
||||
Make sure to stop your server, evennia collectstatic, and restart your server. Then make sure to clear your browser cache before loading the webclient page.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
2020-06-12 22:36:53. There are known conversion issues and missing links.
|
||||
This will slowly be ironed out as this is developed.
|
||||
|
||||
For now you are best off using the original wiki, or the less changing v0.9.1
|
||||
For now you are best off using the original wiki, or the less changing v0.9.5
|
||||
of these docs. You have been warned.
|
||||
```
|
||||
|
||||
|
|
@ -36,4 +36,4 @@ This is the manual of [Evennia](http://www.evennia.com), the open source Python
|
|||
- [Links](./Links) - useful links
|
||||
- [Table of Contents](./toc) - an alphabetical listing of all regular documentation pages
|
||||
|
||||
Want to help improve the docs? See the page on [Contributing to the docs](./Contributing-Docs)!
|
||||
Want to help improve the docs? See the page on [Contributing to the docs](./Contributing-Docs)!
|
||||
|
|
|
|||
|
|
@ -1262,7 +1262,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
]
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
now = timezone.localtime()
|
||||
if settings.USE_TZ:
|
||||
now = timezone.localtime()
|
||||
else:
|
||||
now = timezone.now()
|
||||
now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, now.day, now.hour, now.minute)
|
||||
if _MUDINFO_CHANNEL:
|
||||
_MUDINFO_CHANNEL.tempmsg(f"[{_MUDINFO_CHANNEL.key}, {now}]: {message}")
|
||||
|
|
|
|||
|
|
@ -494,6 +494,11 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
|
|||
cmdset = None
|
||||
for cset in (cset for cset in local_obj_cmdsets if cset):
|
||||
cset.duplicates = cset.old_duplicates
|
||||
# important - this syncs the CmdSetHandler's .current field with the
|
||||
# true current cmdset!
|
||||
if cmdset:
|
||||
caller.cmdset.current = cmdset
|
||||
|
||||
returnValue(cmdset)
|
||||
except ErrorReported:
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -106,9 +106,9 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
commands preference.
|
||||
|
||||
duplicates - determines what happens when two sets of equal
|
||||
priority merge. Default has the first of them in the
|
||||
priority merge (only). Defaults to None and has the first of them in the
|
||||
merger (i.e. A above) automatically taking
|
||||
precedence. But if allow_duplicates is true, the
|
||||
precedence. But if `duplicates` is true, the
|
||||
result will be a merger with more than one of each
|
||||
name match. This will usually lead to the account
|
||||
receiving a multiple-match error higher up the road,
|
||||
|
|
@ -119,6 +119,16 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
select which ball to kick ... Allowing duplicates
|
||||
only makes sense for Union and Intersect, the setting
|
||||
is ignored for the other mergetypes.
|
||||
Note that the `duplicates` flag is *not* propagated in
|
||||
a cmdset merger. So `A + B = C` will result in
|
||||
a cmdset with duplicate commands, but C.duplicates will
|
||||
be `None`. For duplication to apply to a whole cmdset
|
||||
stack merge, _all_ cmdsets in the stack must have
|
||||
`.duplicates=True` set.
|
||||
Finally, if a final cmdset has `.duplicates=None` (the normal
|
||||
unless created alone with another value), the cmdhandler
|
||||
will assume True for object-based cmdsets and False for
|
||||
all other. This is usually the most intuitive outcome.
|
||||
|
||||
key_mergetype (dict) - allows the cmdset to define a unique
|
||||
mergetype for particular cmdsets. Format is
|
||||
|
|
@ -144,14 +154,27 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
mergetype = "Union"
|
||||
priority = 0
|
||||
|
||||
# These flags, if set to None, will allow "pass-through" of lower-prio settings
|
||||
# of True/False. If set to True/False, will override lower-prio settings.
|
||||
# These flags, if set to None should be interpreted as 'I don't care' and,
|
||||
# will allow "pass-through" even of lower-prio cmdsets' explicitly True/False
|
||||
# options. If this is set to True/False however, priority matters.
|
||||
no_exits = None
|
||||
no_objs = None
|
||||
no_channels = None
|
||||
# same as above, but if left at None in the final merged set, the
|
||||
# cmdhandler will auto-assume True for Objects and stay False for all
|
||||
# other entities.
|
||||
# The .duplicates setting does not propagate and since duplicates can only happen
|
||||
# on same-prio cmdsets, there is no concept of passthrough on `None`.
|
||||
# The merger of two cmdsets always return in a cmdset with `duplicates=None`
|
||||
# (even if the result may have duplicated commands).
|
||||
# If a final cmdset has `duplicates=None` (normal, unless the cmdset is
|
||||
# created on its own with the flag set), the cmdhandler will auto-assume it to be
|
||||
# True for Object-based cmdsets and stay None/False for all other entities.
|
||||
#
|
||||
# Example:
|
||||
# A and C has .duplicates=True, B has .duplicates=None (or False)
|
||||
# B + A = BA, where BA will have duplicate cmds, but BA.duplicates = None
|
||||
# BA + C = BAC, where BAC will have more duplication, but BAC.duplicates = None
|
||||
#
|
||||
# Basically, for the `.duplicate` setting to survive throughout a
|
||||
# merge-stack, every cmdset in the stack must have `duplicates` set explicitly.
|
||||
duplicates = None
|
||||
|
||||
permanent = False
|
||||
|
|
@ -334,7 +357,15 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
commands (str): Representation of commands in Cmdset.
|
||||
|
||||
"""
|
||||
return ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)])
|
||||
perm = "perm" if self.permanent else "non-perm"
|
||||
options = ", ".join([
|
||||
"{}:{}".format(opt, "T" if getattr(self, opt) else "F")
|
||||
for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
|
||||
if getattr(self, opt) is not None
|
||||
])
|
||||
options = (", " + options) if options else ""
|
||||
return f"<CmdSet {self.key}, {self.mergetype}, {perm}, prio {self.priority}{options}>: " + ", ".join(
|
||||
[str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)])
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
|
|
@ -401,12 +432,15 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
|
||||
# pass through options whenever they are set, unless the merging or higher-prio
|
||||
# set changes the setting (i.e. has a non-None value). We don't pass through
|
||||
# the duplicates setting; that is per-merge
|
||||
# the duplicates setting; that is per-merge; the resulting .duplicates value
|
||||
# is always None (so merging cmdsets must all have explicit values if wanting
|
||||
# to cause duplicates).
|
||||
cmdset_c.no_channels = (
|
||||
self.no_channels if cmdset_a.no_channels is None else cmdset_a.no_channels
|
||||
)
|
||||
cmdset_c.no_exits = self.no_exits if cmdset_a.no_exits is None else cmdset_a.no_exits
|
||||
cmdset_c.no_objs = self.no_objs if cmdset_a.no_objs is None else cmdset_a.no_objs
|
||||
cmdset_c.duplicates = None
|
||||
|
||||
else:
|
||||
# B higher priority than A
|
||||
|
|
@ -428,12 +462,15 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
|
||||
# pass through options whenever they are set, unless the higher-prio
|
||||
# set changes the setting (i.e. has a non-None value). We don't pass through
|
||||
# the duplicates setting; that is per-merge
|
||||
# the duplicates setting; that is per-merge; the resulting .duplicates value#
|
||||
# is always None (so merging cmdsets must all have explicit values if wanting
|
||||
# to cause duplicates).
|
||||
cmdset_c.no_channels = (
|
||||
cmdset_a.no_channels if self.no_channels is None else self.no_channels
|
||||
)
|
||||
cmdset_c.no_exits = cmdset_a.no_exits if self.no_exits is None else self.no_exits
|
||||
cmdset_c.no_objs = cmdset_a.no_objs if self.no_objs is None else self.no_objs
|
||||
cmdset_c.duplicates = None
|
||||
|
||||
# we store actual_mergetype since key_mergetypes
|
||||
# might be different from the main mergetype.
|
||||
|
|
|
|||
|
|
@ -293,7 +293,10 @@ class CmdSetHandler(object):
|
|||
|
||||
# the id of the "merged" current cmdset for easy access.
|
||||
self.key = None
|
||||
# this holds the "merged" current command set
|
||||
# this holds the "merged" current command set. Note that while the .update
|
||||
# method updates this field in order to have it synced when operating on
|
||||
# cmdsets in-code, when the game runs, this field is kept up-to-date by
|
||||
# the cmdsethandler's get_and_merge_cmdsets!
|
||||
self.current = None
|
||||
# this holds a history of CommandSets
|
||||
self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)]
|
||||
|
|
@ -311,27 +314,13 @@ class CmdSetHandler(object):
|
|||
Display current commands
|
||||
"""
|
||||
|
||||
string = ""
|
||||
strings = ["<CmdSetHandler> stack:"]
|
||||
mergelist = []
|
||||
if len(self.cmdset_stack) > 1:
|
||||
# We have more than one cmdset in stack; list them all
|
||||
for snum, cmdset in enumerate(self.cmdset_stack):
|
||||
mergetype = self.mergetype_stack[snum]
|
||||
permstring = "non-perm"
|
||||
if cmdset.permanent:
|
||||
permstring = "perm"
|
||||
if mergetype != cmdset.mergetype:
|
||||
mergetype = "%s^" % (mergetype)
|
||||
string += "\n %i: <%s (%s, prio %i, %s)>: %s" % (
|
||||
snum,
|
||||
cmdset.key,
|
||||
mergetype,
|
||||
cmdset.priority,
|
||||
permstring,
|
||||
cmdset,
|
||||
)
|
||||
mergelist.append(str(snum))
|
||||
string += "\n"
|
||||
mergelist.append(str(snum + 1))
|
||||
strings.append(f" {snum + 1}: {cmdset}")
|
||||
|
||||
# Display the currently active cmdset, limited by self.obj's permissions
|
||||
mergetype = self.mergetype_stack[-1]
|
||||
|
|
@ -339,27 +328,15 @@ class CmdSetHandler(object):
|
|||
merged_on = self.cmdset_stack[-2].key
|
||||
mergetype = _("custom {mergetype} on cmdset '{cmdset}'")
|
||||
mergetype = mergetype.format(mergetype=mergetype, cmdset=merged_on)
|
||||
|
||||
if mergelist:
|
||||
tmpstring = _(" <Merged {mergelist} {mergetype}, prio {prio}>: {current}")
|
||||
string += tmpstring.format(
|
||||
mergelist="+".join(mergelist),
|
||||
mergetype=mergetype,
|
||||
prio=self.current.priority,
|
||||
current=self.current,
|
||||
)
|
||||
# current is a result of mergers
|
||||
mergelist="+".join(mergelist)
|
||||
strings.append(f" <Merged {mergelist}>: {self.current}")
|
||||
else:
|
||||
permstring = "non-perm"
|
||||
if self.current.permanent:
|
||||
permstring = "perm"
|
||||
tmpstring = _(" <{key} ({mergetype}, prio {prio}, {permstring})>:\n {keylist}")
|
||||
string += tmpstring.format(
|
||||
key=self.current.key,
|
||||
mergetype=mergetype,
|
||||
prio=self.current.priority,
|
||||
permstring=permstring,
|
||||
keylist=", ".join(cmd.key for cmd in sorted(self.current, key=lambda o: o.key)),
|
||||
)
|
||||
return string.strip()
|
||||
# current is a single cmdset
|
||||
strings.append(" " + str(self.current))
|
||||
return "\n".join(strings).rstrip()
|
||||
|
||||
def _import_cmdset(self, cmdset_path, emit_to_obj=None):
|
||||
"""
|
||||
|
|
@ -381,12 +358,22 @@ class CmdSetHandler(object):
|
|||
def update(self, init_mode=False):
|
||||
"""
|
||||
Re-adds all sets in the handler to have an updated current
|
||||
set.
|
||||
|
||||
Args:
|
||||
init_mode (bool, optional): Used automatically right after
|
||||
this handler was created; it imports all permanent cmdsets
|
||||
from the database.
|
||||
|
||||
Notes:
|
||||
This method is necessary in order to always have a `.current`
|
||||
cmdset when working with the cmdsethandler in code. But the
|
||||
CmdSetHandler doesn't (cannot) consider external cmdsets and game
|
||||
state. This means that the .current calculated from this method
|
||||
will likely not match the true current cmdset as determined at
|
||||
run-time by `cmdhandler.get_and_merge_cmdsets()`. So in a running
|
||||
game the responsibility of keeping `.current` upt-to-date belongs
|
||||
to the central `cmdhandler.get_and_merge_cmdsets()`!
|
||||
|
||||
"""
|
||||
if init_mode:
|
||||
# reimport all permanent cmdsets
|
||||
|
|
|
|||
|
|
@ -147,6 +147,9 @@ class Command(object, metaclass=CommandMeta):
|
|||
arg_regex - (optional) raw string regex defining how the argument part of
|
||||
the command should look in order to match for this command
|
||||
(e.g. must it be a space between cmdname and arg?)
|
||||
auto_help_display_key - (optional) if given, this replaces the string shown
|
||||
in the auto-help listing. This is particularly useful for system-commands
|
||||
whose actual key is not really meaningful.
|
||||
|
||||
(Note that if auto_help is on, this initial string is also used by the
|
||||
system to create the help entry for the command, so it's a good idea to
|
||||
|
|
|
|||
|
|
@ -2429,13 +2429,13 @@ class CmdExamine(ObjManipCommand):
|
|||
)
|
||||
return output
|
||||
|
||||
def format_output(self, obj, avail_cmdset):
|
||||
def format_output(self, obj, current_cmdset):
|
||||
"""
|
||||
Helper function that creates a nice report about an object.
|
||||
|
||||
Args:
|
||||
obj (any): Object to analyze.
|
||||
avail_cmdset (CmdSet): Current cmdset for object.
|
||||
current_cmdset (CmdSet): Current cmdset for object.
|
||||
|
||||
Returns:
|
||||
str: The formatted string.
|
||||
|
|
@ -2513,15 +2513,36 @@ class CmdExamine(ObjManipCommand):
|
|||
# cmdsets
|
||||
if not (len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"):
|
||||
# all() returns a 'stack', so make a copy to sort.
|
||||
|
||||
def _format_options(cmdset):
|
||||
"""helper for cmdset-option display"""
|
||||
def _truefalse(string, value):
|
||||
if value is None:
|
||||
return ""
|
||||
if value:
|
||||
return f"{string}: T"
|
||||
return f"{string}: F"
|
||||
options = ", ".join(
|
||||
_truefalse(opt, getattr(cmdset, opt))
|
||||
for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
|
||||
if getattr(cmdset, opt) is not None
|
||||
)
|
||||
options = ", " + options if options else ""
|
||||
return options
|
||||
|
||||
# cmdset stored on us
|
||||
stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority, reverse=True)
|
||||
output["Stored Cmdset(s)"] = "\n " + "\n ".join(
|
||||
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority})"
|
||||
for cmdset in stored_cmdsets
|
||||
if cmdset.key != "_EMPTY_CMDSET"
|
||||
)
|
||||
stored = []
|
||||
for cmdset in stored_cmdsets:
|
||||
if cmdset.key == "_EMPTY_CMDSET":
|
||||
continue
|
||||
options = _format_options(cmdset)
|
||||
stored.append(
|
||||
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority}{options})")
|
||||
output["Stored Cmdset(s)"] = "\n " + "\n ".join(stored)
|
||||
|
||||
# this gets all components of the currently merged set
|
||||
all_cmdsets = [(cmdset.key, cmdset) for cmdset in avail_cmdset.merged_from]
|
||||
all_cmdsets = [(cmdset.key, cmdset) for cmdset in current_cmdset.merged_from]
|
||||
# we always at least try to add account- and session sets since these are ignored
|
||||
# if we merge on the object level.
|
||||
if hasattr(obj, "account") and obj.account:
|
||||
|
|
@ -2551,15 +2572,24 @@ class CmdExamine(ObjManipCommand):
|
|||
pass
|
||||
all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()]
|
||||
all_cmdsets.sort(key=lambda x: x.priority, reverse=True)
|
||||
output["Merged Cmdset(s)"] = "\n " + "\n ".join(
|
||||
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype} prio {cmdset.priority})"
|
||||
for cmdset in all_cmdsets
|
||||
)
|
||||
# list the commands available to this object
|
||||
avail_cmdset = sorted([cmd.key for cmd in avail_cmdset if cmd.access(obj, "cmd")])
|
||||
|
||||
cmdsetstr = "\n" + utils.fill(", ".join(avail_cmdset), indent=2)
|
||||
# the resulting merged cmdset
|
||||
options = _format_options(current_cmdset)
|
||||
merged = [
|
||||
f"<Current merged cmdset> ({current_cmdset.mergetype} prio {current_cmdset.priority}{options})"]
|
||||
|
||||
# the merge stack
|
||||
for cmdset in all_cmdsets:
|
||||
options = _format_options(cmdset)
|
||||
merged.append(
|
||||
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype} prio {cmdset.priority}{options})")
|
||||
output["Merged Cmdset(s)"] = "\n " + "\n ".join(merged)
|
||||
|
||||
# list the commands available to this object
|
||||
current_commands = sorted([cmd.key for cmd in current_cmdset if cmd.access(obj, "cmd")])
|
||||
cmdsetstr = "\n" + utils.fill(", ".join(current_commands), indent=2)
|
||||
output[f"Commands available to {obj.key} (result of Merged CmdSets)"] = str(cmdsetstr)
|
||||
|
||||
# scripts
|
||||
if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all():
|
||||
output["Scripts"] = "\n " + f"{obj.scripts}"
|
||||
|
|
|
|||
|
|
@ -808,17 +808,49 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
|||
lastpages = pages[-number:]
|
||||
else:
|
||||
lastpages = pages
|
||||
template = "|w%s|n |c%s|n to |c%s|n: %s"
|
||||
lastpages = "\n ".join(
|
||||
template
|
||||
% (
|
||||
utils.datetime_format(page.date_created),
|
||||
",".join(obj.key for obj in page.senders),
|
||||
"|n,|c ".join([obj.name for obj in page.receivers]),
|
||||
page.message,
|
||||
to_template = "|w{date}{clr} {sender}|nto{clr}{receiver}|n:> {message}"
|
||||
from_template = "|w{date}{clr} {receiver}|nfrom{clr}{sender}|n:< {message}"
|
||||
listing = []
|
||||
prev_selfsend = False
|
||||
for page in lastpages:
|
||||
multi_send = len(page.senders) > 1
|
||||
multi_recv = len(page.receivers) > 1
|
||||
sending = self.caller in page.senders
|
||||
# self-messages all look like sends, so we assume they always
|
||||
# come in close pairs and treat the second of the pair as the recv.
|
||||
selfsend = sending and self.caller in page.receivers
|
||||
if selfsend:
|
||||
if prev_selfsend:
|
||||
# this is actually a receive of a self-message
|
||||
sending = False
|
||||
prev_selfsend = False
|
||||
else:
|
||||
prev_selfsend = True
|
||||
|
||||
clr = "|c" if sending else "|g"
|
||||
|
||||
sender = f"|n,{clr}".join(obj.key for obj in page.senders)
|
||||
receiver = f"|n,{clr}".join([obj.name for obj in page.receivers])
|
||||
if sending:
|
||||
template = to_template
|
||||
sender = f"{sender} " if multi_send else ""
|
||||
receiver = f" {receiver}" if multi_recv else f" {receiver}"
|
||||
else:
|
||||
template = from_template
|
||||
receiver = f"{receiver} " if multi_recv else ""
|
||||
sender = f" {sender} " if multi_send else f" {sender}"
|
||||
|
||||
listing.append(
|
||||
template.format(
|
||||
date=utils.datetime_format(page.date_created),
|
||||
clr=clr,
|
||||
sender=sender,
|
||||
receiver=receiver,
|
||||
message=page.message,
|
||||
)
|
||||
|
||||
)
|
||||
for page in lastpages
|
||||
)
|
||||
lastpages = "\n ".join(listing)
|
||||
|
||||
if lastpages:
|
||||
string = "Your latest pages:\n %s" % lastpages
|
||||
|
|
|
|||
|
|
@ -378,10 +378,12 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
|
|||
if not items:
|
||||
string = "You are not carrying anything."
|
||||
else:
|
||||
from evennia.utils.ansi import raw as raw_ansi
|
||||
table = self.styled_table(border="header")
|
||||
for item in items:
|
||||
table.add_row("|C%s|n" % item.name, item.db.desc or "")
|
||||
string = "|wYou are carrying:\n%s" % table
|
||||
table.add_row(f"|C{item.name}|n",
|
||||
"{}|n".format(utils.crop(raw_ansi(item.db.desc), width=50) or ""))
|
||||
string = f"|wYou are carrying:\n{table}"
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -294,10 +294,12 @@ class CmdHelp(Command):
|
|||
hdict_topic = defaultdict(list)
|
||||
# create the dictionaries {category:[topic, topic ...]} required by format_help_list
|
||||
# Filter commands that should be reached by the help
|
||||
# system, but not be displayed in the table.
|
||||
# system, but not be displayed in the table, or be displayed differently.
|
||||
for cmd in all_cmds:
|
||||
if self.should_list_cmd(cmd, caller):
|
||||
hdict_cmd[cmd.help_category].append(cmd.key)
|
||||
key = (cmd.auto_help_display_key
|
||||
if hasattr(cmd, "auto_help_display_key") else cmd.key)
|
||||
hdict_cmd[cmd.help_category].append(key)
|
||||
[hdict_topic[topic.help_category].append(topic.key) for topic in all_topics]
|
||||
# report back
|
||||
self.msg_help(self.format_help_list(hdict_cmd, hdict_topic))
|
||||
|
|
@ -308,7 +310,6 @@ class CmdHelp(Command):
|
|||
|
||||
for match_query in [f"{query}~1", f"{query}*"]:
|
||||
# We first do an exact word-match followed by a start-by query
|
||||
|
||||
matches, suggestions = help_search_with_index(
|
||||
match_query, entries, suggestion_maxnum=self.suggestion_maxnum
|
||||
)
|
||||
|
|
|
|||
|
|
@ -984,7 +984,8 @@ class TestBuilding(CommandTest):
|
|||
self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was set to Room")
|
||||
|
||||
def test_list_cmdsets(self):
|
||||
self.call(building.CmdListCmdSets(), "", "<DefaultCharacter (Union, prio 0, perm)>:")
|
||||
self.call(building.CmdListCmdSets(), "",
|
||||
"<CmdSetHandler> stack:\n <CmdSet DefaultCharacter, Union, perm, prio 0>:")
|
||||
self.call(building.CmdListCmdSets(), "NotFound", "Could not find 'NotFound'")
|
||||
|
||||
def test_typeclass(self):
|
||||
|
|
|
|||
|
|
@ -194,27 +194,71 @@ class TestCmdSetMergers(TestCase):
|
|||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
self.assertTrue(all(True for cmd in cmdset_f.commands if cmd.from_cmdset == "A"))
|
||||
|
||||
def test_option_transfer(self):
|
||||
"Test transfer of cmdset options"
|
||||
|
||||
class TestOptionTransferTrue(TestCase):
|
||||
"""
|
||||
Test cmdset-merge transfer of the cmdset-special options
|
||||
(no_exits/channels/objs/duplicates etc)
|
||||
|
||||
cmdset A has all True options
|
||||
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.cmdset_a = _CmdSetA()
|
||||
self.cmdset_b = _CmdSetB()
|
||||
self.cmdset_c = _CmdSetC()
|
||||
self.cmdset_d = _CmdSetD()
|
||||
self.cmdset_a.priority = 0
|
||||
self.cmdset_b.priority = 0
|
||||
self.cmdset_c.priority = 0
|
||||
self.cmdset_d.priority = 0
|
||||
self.cmdset_a.no_exits = True
|
||||
self.cmdset_a.no_objs = True
|
||||
self.cmdset_a.no_channels = True
|
||||
self.cmdset_a.duplicates = True
|
||||
|
||||
def test_option_transfer__reverse_sameprio_passthrough(self):
|
||||
"""
|
||||
A has all True options, merges last (normal reverse merge), same prio.
|
||||
The options should pass through to F since none of the other cmdsets
|
||||
care to change the setting from their default None.
|
||||
|
||||
Since A.duplicates = True, the final result is an union of duplicate
|
||||
pairs (8 commands total).
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
# the options should pass through since none of the other cmdsets care
|
||||
# to change the setting from None.
|
||||
a.no_exits = True
|
||||
a.no_objs = True
|
||||
a.no_channels = True
|
||||
a.duplicates = True
|
||||
cmdset_f = d + c + b + a # reverse, same-prio
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertTrue(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 8)
|
||||
|
||||
def test_option_transfer__forward_sameprio_passthrough(self):
|
||||
"""
|
||||
A has all True options, merges first (forward merge), same prio. This
|
||||
should pass those options through since the other all have options set
|
||||
to None. The exception is `duplicates` since that is determined by
|
||||
the two last mergers in the chain both being True.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
cmdset_f = a + b + c + d # forward, same-prio
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertFalse(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_highprio_passthrough(self):
|
||||
"""
|
||||
A has all True options, merges last (normal reverse merge) with the
|
||||
highest prio. This should also pass through.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
|
|
@ -223,14 +267,35 @@ class TestCmdSetMergers(TestCase):
|
|||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertTrue(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_highprio_passthrough(self):
|
||||
"""
|
||||
A has all True options, merges first (forward merge). This is a bit
|
||||
synthetic since it will never happen in practice, but logic should
|
||||
still make it pass through.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
cmdset_f = a + b + c + d # forward, A top priority. This never happens in practice.
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertTrue(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_lowprio_passthrough(self):
|
||||
"""
|
||||
A has all True options, merges last (normal reverse merge) with the lowest
|
||||
prio. This never happens (it would always merge first) but logic should hold
|
||||
and pass through since the other cmdsets have None.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
|
|
@ -239,32 +304,678 @@ class TestCmdSetMergers(TestCase):
|
|||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertFalse(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_lowprio_passthrough(self):
|
||||
"""
|
||||
A has all True options, merges first (forward merge) with lowest prio. This
|
||||
is the normal behavior for a low-prio cmdset. Passthrough should happen.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
cmdset_f = a + b + c + d # forward, A low prio
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertFalse(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_highprio_block_passthrough(self):
|
||||
"""
|
||||
A has all True options, other cmdsets has False. A merges last with high
|
||||
prio. A should retain its option values and override the others
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
c.no_exits = False
|
||||
b.no_objs = False
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + b + a # reverse, high prio
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_highprio_block_passthrough(self):
|
||||
"""
|
||||
A has all True options, other cmdsets has False. A merges last with high
|
||||
prio. This situation should never happen, but logic should hold - the highest
|
||||
prio's options should survive the merge process.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
c.no_exits = False
|
||||
b.no_channels = False
|
||||
b.no_objs = False
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = a + b + c + d # forward, high prio, never happens
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_lowprio_block(self):
|
||||
"""
|
||||
A has all True options, other cmdsets has False. A merges last with low
|
||||
prio. This should result in its values being blocked and come out False.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
c.no_exits = False
|
||||
c.no_channels = False
|
||||
b.no_objs = False
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = a + b + c + d # forward, A low prio
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_lowprio_block_partial(self):
|
||||
"""
|
||||
A has all True options, other cmdsets has False excet C which has a None
|
||||
for `no_channels`. A merges last with low
|
||||
prio. This should result in its values being blocked and come out False
|
||||
except for no_channels which passes through.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
c.no_exits = False
|
||||
c.no_channels = None # passthrough
|
||||
b.no_objs = False
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = a + b + c + d # forward, A low prio
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertFalse(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
a.priority = 0
|
||||
b.priority = 0
|
||||
|
||||
def test_option_transfer__reverse_highprio_sameprio_order_last(self):
|
||||
"""
|
||||
A has all True options and highest prio, D has False and lowest prio,
|
||||
others are passthrough. B has the same prio as A, with passthrough.
|
||||
|
||||
Since A is merged last, this should give prio to A's options
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 2
|
||||
c.priority = 0
|
||||
d.priority = 0
|
||||
d.priority = -1
|
||||
d.no_channels = False
|
||||
d.no_exits = False
|
||||
d.no_objs = None
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + b + a # reverse, A same prio, merged after b
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 8)
|
||||
|
||||
def test_option_transfer__reverse_highprio_sameprio_order_first(self):
|
||||
"""
|
||||
A has all True options and highest prio, D has False and lowest prio,
|
||||
others are passthrough. B has the same prio as A, with passthrough.
|
||||
|
||||
While B, with None-values, is merged after A, A's options should have
|
||||
replaced those of D at that point, and since B has passthrough the
|
||||
final result should contain A's True options.
|
||||
|
||||
Note that despite A having duplicates=True, there is no duplication in
|
||||
the DB + A merger since they have different priorities.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 2
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
d.no_channels = False
|
||||
d.no_exits = False
|
||||
d.no_objs = False
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + a + b # reverse, A same prio, merged before b
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_lowprio_block(self):
|
||||
"""
|
||||
A has all True options, other cmdsets has False. A merges last with low
|
||||
prio. This usually doesn't happen- it should merge last. But logic should
|
||||
hold and the low-prio cmdset's values should be blocked and come out False.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
c.no_exits = False
|
||||
d.no_channels = False
|
||||
b.no_objs = False
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + b + a # reverse, A low prio, never happens
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
|
||||
class TestOptionTransferFalse(TestCase):
|
||||
"""
|
||||
Test cmdset-merge transfer of the cmdset-special options
|
||||
(no_exits/channels/objs/duplicates etc)
|
||||
|
||||
cmdset A has all False options
|
||||
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.cmdset_a = _CmdSetA()
|
||||
self.cmdset_b = _CmdSetB()
|
||||
self.cmdset_c = _CmdSetC()
|
||||
self.cmdset_d = _CmdSetD()
|
||||
self.cmdset_a.priority = 0
|
||||
self.cmdset_b.priority = 0
|
||||
self.cmdset_c.priority = 0
|
||||
self.cmdset_d.priority = 0
|
||||
self.cmdset_a.no_exits = False
|
||||
self.cmdset_a.no_objs = False
|
||||
self.cmdset_a.no_channels = False
|
||||
self.cmdset_a.duplicates = False
|
||||
|
||||
def test_option_transfer__reverse_sameprio_passthrough(self):
|
||||
"""
|
||||
A has all False options, merges last (normal reverse merge), same prio.
|
||||
The options should pass through to F since none of the other cmdsets
|
||||
care to change the setting from their default None.
|
||||
|
||||
Since A has duplicates=False, the result is a unique union of 4 cmds.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
cmdset_f = d + c + b + a # reverse, same-prio
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_sameprio_passthrough(self):
|
||||
"""
|
||||
A has all False options, merges first (forward merge), same prio. This
|
||||
should pass those options through since the other all have options set
|
||||
to None. The exception is `duplicates` since that is determined by
|
||||
the two last mergers in the chain both being .
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
cmdset_f = a + b + c + d # forward, same-prio
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_highprio_passthrough(self):
|
||||
"""
|
||||
A has all False options, merges last (normal reverse merge) with the
|
||||
highest prio. This should also pass through.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
cmdset_f = d + c + b + a # reverse, A top priority
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_highprio_passthrough(self):
|
||||
"""
|
||||
A has all False options, merges first (forward merge). This is a bit
|
||||
synthetic since it will never happen in practice, but logic should
|
||||
still make it pass through.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
cmdset_f = a + b + c + d # forward, A top priority. This never happens in practice.
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_lowprio_passthrough(self):
|
||||
"""
|
||||
A has all False options, merges last (normal reverse merge) with the lowest
|
||||
prio. This never happens (it would always merge first) but logic should hold
|
||||
and pass through since the other cmdsets have None.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
cmdset_f = d + c + b + a # reverse, A low prio. This never happens in practice.
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_lowprio_passthrough(self):
|
||||
"""
|
||||
A has all False options, merges first (forward merge) with lowest prio. This
|
||||
is the normal behavior for a low-prio cmdset. Passthrough should happen.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
cmdset_f = a + b + c + d # forward, A low prio
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_highprio_block_passthrough(self):
|
||||
"""
|
||||
A has all False options, other cmdsets has True. A merges last with high
|
||||
prio. A should retain its option values and override the others
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
c.no_exits = True
|
||||
b.no_objs = True
|
||||
d.duplicates = True
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + b + a # reverse, high prio
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_highprio_block_passthrough(self):
|
||||
"""
|
||||
A has all False options, other cmdsets has True. A merges last with high
|
||||
prio. This situation should never happen, but logic should hold - the highest
|
||||
prio's options should survive the merge process.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
c.no_exits = True
|
||||
b.no_channels = True
|
||||
b.no_objs = True
|
||||
d.duplicates = True
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = a + b + c + d # forward, high prio, never happens
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_lowprio_block(self):
|
||||
"""
|
||||
A has all False options, other cmdsets has True. A merges last with low
|
||||
prio. This should result in its values being blocked and come out False.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
c.no_exits = True
|
||||
c.no_channels = True
|
||||
b.no_objs = True
|
||||
d.duplicates = True
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = a + b + c + d # forward, A low prio
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_lowprio_block_partial(self):
|
||||
"""
|
||||
A has all False options, other cmdsets has True excet C which has a None
|
||||
for `no_channels`. A merges last with low
|
||||
prio. This should result in its values being blocked and come out True
|
||||
except for no_channels which passes through.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
c.no_exits = True
|
||||
c.no_channels = None # passthrough
|
||||
b.no_objs = True
|
||||
d.duplicates = True
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = a + b + c + d # forward, A low prio
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_sameprio_order_last(self):
|
||||
"""
|
||||
A has all False options and highest prio, D has True and lowest prio,
|
||||
others are passthrough. B has the same prio as A, with passthrough.
|
||||
|
||||
Since A is merged last, this should give prio to A's False options
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 2
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
d.no_channels = True
|
||||
d.no_exits = True
|
||||
d.no_objs = True
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + b + a # reverse, A high prio, merged after b
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_sameprio_order_first(self):
|
||||
"""
|
||||
A has all False options and highest prio, D has True and lowest prio,
|
||||
others are passthrough. B has the same prio as A, with passthrough.
|
||||
|
||||
While B, with None-values, is merged after A, A's options should have
|
||||
replaced those of D at that point, and since B has passthrough the
|
||||
final result should contain A's False options.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 2
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
d.no_channels = True
|
||||
d.no_exits = True
|
||||
d.no_objs = True
|
||||
d.duplicates = False
|
||||
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + a + b # reverse, A high prio, merged before b
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_lowprio_block(self):
|
||||
"""
|
||||
A has all False options, other cmdsets has True. A merges last with low
|
||||
prio. This usually doesn't happen- it should merge last. But logic should
|
||||
hold and the low-prio cmdset's values should be blocked and come out True.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
c.no_exits = True
|
||||
d.no_channels = True
|
||||
b.no_objs = True
|
||||
d.duplicates = True
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + b + a # reverse, A low prio, never happens
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
|
||||
class TestDuplicateBehavior(TestCase):
|
||||
"""
|
||||
Test behavior of .duplicate option, which is a bit special in that it
|
||||
doesn't propagate.
|
||||
|
||||
`A.duplicates=True` for all tests.
|
||||
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.cmdset_a = _CmdSetA()
|
||||
self.cmdset_b = _CmdSetB()
|
||||
self.cmdset_c = _CmdSetC()
|
||||
self.cmdset_d = _CmdSetD()
|
||||
self.cmdset_a.priority = 0
|
||||
self.cmdset_b.priority = 0
|
||||
self.cmdset_c.priority = 0
|
||||
self.cmdset_d.priority = 0
|
||||
self.cmdset_a.duplicates = True
|
||||
|
||||
def test_reverse_sameprio_duplicate(self):
|
||||
"""
|
||||
Test of `duplicates` transfer which does not propagate. Only
|
||||
A has duplicates=True.
|
||||
|
||||
D + B = DB (no duplication, DB.duplication=None)
|
||||
DB + C = DBC (no duplication, DBC.duplication=None)
|
||||
DBC + A = final (duplication, final.duplication=None)
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
cmdset_f = d + b + c + a # two last mergers duplicates=True
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 8)
|
||||
|
||||
def test_reverse_sameprio_duplicate(self):
|
||||
"""
|
||||
Test of `duplicates` transfer, which does not propagate.
|
||||
C.duplication=True
|
||||
|
||||
D + B = DB (no duplication, DB.duplication=None)
|
||||
DB + C = DBC (duplication, DBC.duplication=None)
|
||||
DBC + A = final (duplication, final.duplication=None)
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
c.duplicates = True
|
||||
cmdset_f = d + b + c + a # two last mergers duplicates=True
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 10)
|
||||
|
||||
def test_forward_sameprio_duplicate(self):
|
||||
"""
|
||||
Test of `duplicates` transfer which does not propagate.
|
||||
C.duplication=True, merges later than A
|
||||
|
||||
D + B = DB (no duplication, DB.duplication=None)
|
||||
DB + A = DBA (duplication, DBA.duplication=None)
|
||||
DBA + C = final (duplication, final.duplication=None)
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
c.duplicates = True
|
||||
cmdset_f = d + b + a + c # two last mergers duplicates=True
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 10)
|
||||
|
||||
def test_reverse_sameprio_duplicate_reverse(self):
|
||||
"""
|
||||
Test of `duplicates` transfer which does not propagate.
|
||||
C.duplication=False (explicit), merges before A. This behavior is the
|
||||
same as if C.duplication=None, since A merges later and takes
|
||||
precedence.
|
||||
|
||||
D + B = DB (no duplication, DB.duplication=None)
|
||||
DB + C = DBC (no duplication, DBC.duplication=None)
|
||||
DBC + A = final (duplication, final.duplication=None)
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
c.duplicates = False
|
||||
cmdset_f = d + b + c + a # a merges last, takes precedence
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 8)
|
||||
|
||||
def test_reverse_sameprio_duplicate_forward(self):
|
||||
"""
|
||||
Test of `duplicates` transfer which does not propagate.
|
||||
C.duplication=False (explicit), merges after A. This just means
|
||||
only A causes duplicates, earlier in the chain.
|
||||
|
||||
D + B = DB (no duplication, DB.duplication=None)
|
||||
DB + A = DBA (duplication, DBA.duplication=None)
|
||||
DBA + C = final (no duplication, final.duplication=None)
|
||||
|
||||
Note that DBA has 8 cmds due to A merging onto DB with duplication,
|
||||
but since C merges onto this with no duplication, the union will hold
|
||||
6 commands, since C has two commands that replaces the 4 duplicates
|
||||
with uniques copies from C.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
c.duplicates = False
|
||||
cmdset_f = d + b + a + c # a merges before c
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 6)
|
||||
|
||||
|
||||
class TestOptionTransferReplace(TestCase):
|
||||
"""
|
||||
Test option transfer through more complex merge types.
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.cmdset_a = _CmdSetA()
|
||||
self.cmdset_b = _CmdSetB()
|
||||
self.cmdset_c = _CmdSetC()
|
||||
self.cmdset_d = _CmdSetD()
|
||||
self.cmdset_a.priority = 0
|
||||
self.cmdset_b.priority = 0
|
||||
self.cmdset_c.priority = 0
|
||||
self.cmdset_d.priority = 0
|
||||
self.cmdset_a.no_exits = True
|
||||
self.cmdset_a.no_objs = True
|
||||
self.cmdset_a.no_channels = True
|
||||
self.cmdset_a.duplicates = True
|
||||
|
||||
def test_option_transfer__replace_reverse_highprio(self):
|
||||
"""
|
||||
A has all options True and highest priority. C has them False and is
|
||||
Replace-type.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 2
|
||||
c.priority = 0
|
||||
c.mergetype = "Replace"
|
||||
c.no_channels = False
|
||||
c.no_exits = False
|
||||
c.no_objs = False
|
||||
c.duplicates = False
|
||||
d.priority = -1
|
||||
|
||||
cmdset_f = d + c + b + a # reverse, A high prio, C Replace
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 7)
|
||||
|
||||
def test_option_transfer__replace_reverse_highprio_from_false(self):
|
||||
"""
|
||||
Inverse of previous test: A has all options False and highest priority.
|
||||
C has them True and is Replace-type.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.no_exits = False
|
||||
a.no_objs = False
|
||||
a.no_channels = False
|
||||
a.duplicates = False
|
||||
|
||||
a.priority = 2
|
||||
b.priority = 2
|
||||
c.priority = 0
|
||||
c.mergetype = "Replace"
|
||||
c.no_channels = True
|
||||
c.no_exits = True
|
||||
c.no_objs = True
|
||||
c.duplicates = True
|
||||
d.priority = -1
|
||||
|
||||
cmdset_f = d + c + b + a # reverse, A high prio, C Replace
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
|
||||
# test cmdhandler functions
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia import DefaultChannel
|
||||
from evennia.utils.create import create_message
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
|
||||
|
||||
class ObjectCreationTest(EvenniaTest):
|
||||
|
|
@ -16,3 +16,34 @@ class ObjectCreationTest(EvenniaTest):
|
|||
msg = create_message("peewee herman", "heh-heh!", header="mail time!")
|
||||
self.assertTrue(msg)
|
||||
self.assertEqual(str(msg), "peewee herman->: heh-heh!")
|
||||
|
||||
|
||||
class ChannelWholistTests(EvenniaTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.default_channel, _ = DefaultChannel.create("coffeetalk", description="A place to talk about coffee.")
|
||||
self.default_channel.connect(self.obj1)
|
||||
|
||||
def test_wholist_shows_subscribed_objects(self):
|
||||
expected = "Obj"
|
||||
result = self.default_channel.wholist
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_wholist_shows_none_when_empty(self):
|
||||
# No one hates dogs
|
||||
empty_channel, _ = DefaultChannel.create("doghaters", description="A place where dog haters unite.")
|
||||
expected = "<None>"
|
||||
result = empty_channel.wholist
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_wholist_does_not_show_muted_objects(self):
|
||||
self.default_channel.mute(self.obj2)
|
||||
expected = "Obj"
|
||||
result = self.default_channel.wholist
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_wholist_shows_connected_object_as_bold(self):
|
||||
self.default_channel.connect(self.char1)
|
||||
expected = "Obj, |wChar|n"
|
||||
result = self.default_channel.wholist
|
||||
self.assertEqual(expected, result)
|
||||
|
|
|
|||
|
|
@ -104,36 +104,34 @@ tutorial
|
|||
# ... and describe it.
|
||||
#
|
||||
@desc
|
||||
|gWelcome to the Evennia tutorial!|n
|
||||
|gWelcome to the Evennia tutorial-world!|n
|
||||
|
||||
This small quest shows some examples of Evennia usage.
|
||||
|
||||
|gDo you want help with how to play? Write |yintro|g to get an introduction to
|
||||
Evennia and the basics of playing!|n
|
||||
|
||||
To get into the mood of this miniature quest, imagine you are an adventurer
|
||||
out to find fame and fortune. You have heard rumours of an old castle ruin by
|
||||
the coast. In its depth a warrior princess was buried together with her
|
||||
powerful magical weapon - a valuable prize, if it's true. Of course this is a
|
||||
chance to adventure that you cannot turn down!
|
||||
|
||||
You reach the coast in the midst of a raging thunderstorm. With wind and rain
|
||||
screaming in your face you stand where the moor meet the sea along a high,
|
||||
rocky coast ...
|
||||
|
||||
Try '|yintro|n' for usage help. During the quest, write '|ytutorial|n' to get
|
||||
behind-the-scenes help anywhere, and '|ygive up|n' to abandon the quest.
|
||||
|
||||
|gwrite 'begin' to start your quest!|n
|
||||
|
||||
|
||||
The following tutorial consists of a small single-player quest
|
||||
area. The various rooms are designed to show off some of the power
|
||||
and possibilities of the Evennia mud creation system. At any time
|
||||
during this tutorial you can use the |wtutorial|n (or |wtut|n)
|
||||
command to get some background info about the room or certain objects
|
||||
to see what is going on "behind the scenes".
|
||||
|
||||
|
||||
To get into the mood of this miniature quest, imagine you are an
|
||||
adventurer out to find fame and fortune. You have heard rumours of an
|
||||
old castle ruin by the coast. In its depth a warrior princess was
|
||||
buried together with her powerful magical weapon - a valuable prize,
|
||||
if it's true. Of course this is a chance to adventure that you
|
||||
cannot turn down!
|
||||
|
||||
You reach the coast in the midst of a raging thunderstorm. With wind
|
||||
and rain screaming in your face you stand where the moor meet the sea
|
||||
along a high, rocky coast ...
|
||||
|
||||
|
||||
|g(write 'start' or 'begin' to start the tutorial. Try 'tutorial'
|
||||
to get behind-the-scenes help anywhere.)|n
|
||||
#
|
||||
# Show that the tutorial command works ...
|
||||
#
|
||||
@set here/tutorial_info =
|
||||
You just tried the tutorial command. Use it in various rooms to see
|
||||
You just tried the |wtutorial|G command. Use it in various rooms to see
|
||||
what's technically going on and what you could try in each room. The
|
||||
intro room assigns some properties to your character, like a simple
|
||||
"health" property used when fighting. Other rooms and puzzles might do
|
||||
|
|
@ -294,14 +292,14 @@ start
|
|||
on the sign.
|
||||
# Set a climbable object for discovering a hidden exit
|
||||
#
|
||||
@create/drop gnarled old trees;tree;trees;gnarled : tutorial_world.objects.TutorialClimbable
|
||||
@create/drop gnarled old tree;tree;trees;gnarled : tutorial_world.objects.TutorialClimbable
|
||||
#
|
||||
@desc trees = Only the sturdiest of trees survive at the edge of the
|
||||
moor. A small group of huddling black things has dug in near the
|
||||
@desc tree = Only the sturdiest of trees survive at the edge of the
|
||||
moor. A small huddling black thing has dug in near the
|
||||
cliff edge, eternally pummeled by wind and salt to become an integral
|
||||
part of the gloomy scenery.
|
||||
#
|
||||
@lock trees = get:false()
|
||||
@lock tree = get:false()
|
||||
#
|
||||
@set trees/get_err_msg =
|
||||
The group of old trees have withstood the eternal wind for hundreds
|
||||
|
|
@ -475,7 +473,7 @@ north
|
|||
# regular exits back to the cliff, that is handled by the bridge
|
||||
# typeclass itself.
|
||||
#
|
||||
@dig The old bridge;bridge;tut#05
|
||||
@dig The old bridge;bridge;east;e;tut#05
|
||||
: tutorial_world.rooms.BridgeRoom
|
||||
= old bridge;east;e;bridge;hangbridge
|
||||
#
|
||||
|
|
|
|||
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
|
||||
|
||||
from evennia import DefaultObject, DefaultExit, Command, CmdSet
|
||||
from evennia.utils import search, delay
|
||||
from evennia.utils import search, delay, dedent
|
||||
from evennia.prototypes.spawner import spawn
|
||||
|
||||
# -------------------------------------------------------------
|
||||
|
|
@ -647,7 +647,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
|||
"""called when the object is first created."""
|
||||
super().at_object_creation()
|
||||
|
||||
self.aliases.add(["secret passage", "passage", "crack", "opening", "secret door"])
|
||||
self.aliases.add(["secret passage", "passage", "crack", "opening", "secret"])
|
||||
|
||||
# starting root positions. H1/H2 are the horizontally hanging roots,
|
||||
# V1/V2 the vertically hanging ones. Each can have three positions:
|
||||
|
|
@ -688,6 +688,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
|||
# start a 45 second timer before closing again. We store the deferred so it can be
|
||||
# killed in unittesting.
|
||||
self.deferred = delay(45, self.reset)
|
||||
return True
|
||||
|
||||
def _translate_position(self, root, ipos):
|
||||
"""Translates the position into words"""
|
||||
|
|
@ -740,7 +741,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
|||
"The wall is old and covered with roots that here and there have permeated the stone. "
|
||||
"The roots (or whatever they are - some of them are covered in small nondescript flowers) "
|
||||
"crisscross the wall, making it hard to clearly see its stony surface. Maybe you could "
|
||||
"try to |wshift|n or |wmove|n them.\n"
|
||||
"try to |wshift|n or |wmove|n them (like '|wshift red up|n').\n"
|
||||
]
|
||||
# display the root positions to help with the puzzle
|
||||
for key, pos in self.db.root_pos.items():
|
||||
|
|
@ -833,6 +834,7 @@ class CmdAttack(Command):
|
|||
"stab",
|
||||
"slash",
|
||||
"chop",
|
||||
"bash",
|
||||
"parry",
|
||||
"defend",
|
||||
]
|
||||
|
|
@ -875,7 +877,7 @@ class CmdAttack(Command):
|
|||
tstring = "%s stabs at you with %s. " % (self.caller.key, self.obj.key)
|
||||
ostring = "%s stabs at %s with %s. " % (self.caller.key, target.key, self.obj.key)
|
||||
self.caller.db.combat_parry_mode = False
|
||||
elif cmdstring in ("slash", "chop"):
|
||||
elif cmdstring in ("slash", "chop", "bash"):
|
||||
hit = float(self.obj.db.hit) # un modified due to slash
|
||||
damage = self.obj.db.damage # un modified due to slash
|
||||
string = "You slash with %s. " % self.obj.key
|
||||
|
|
@ -1150,7 +1152,14 @@ class TutorialWeaponRack(TutorialObject):
|
|||
self.db.rack_id = "weaponrack_1"
|
||||
# these are prototype names from the prototype
|
||||
# dictionary above.
|
||||
self.db.get_weapon_msg = "You find |c%s|n."
|
||||
self.db.get_weapon_msg = dedent(
|
||||
"""
|
||||
You find |c%s|n. While carrying this weapon, these actions are available:
|
||||
|wstab/thrust/pierce <target>|n - poke at the enemy. More damage but harder to hit.
|
||||
|wslash/chop/bash <target>|n - swipe at the enemy. Less damage but easier to hit.
|
||||
|wdefend/parry|n - protect yourself and make yourself harder to hit.)
|
||||
""").strip()
|
||||
|
||||
self.db.no_more_weapons_msg = "you find nothing else of use."
|
||||
self.db.available_weapons = ["knife", "dagger", "sword", "club"]
|
||||
|
||||
|
|
|
|||
|
|
@ -69,12 +69,14 @@ class CmdTutorial(Command):
|
|||
target = caller.search(self.args.strip())
|
||||
if not target:
|
||||
return
|
||||
helptext = target.db.tutorial_info
|
||||
if helptext:
|
||||
caller.msg("|G%s|n" % helptext)
|
||||
else:
|
||||
caller.msg("|RSorry, there is no tutorial help available here.|n")
|
||||
helptext = target.db.tutorial_info or ""
|
||||
|
||||
if helptext:
|
||||
helptext = f" |G{helptext}|n"
|
||||
else:
|
||||
helptext = " |RSorry, there is no tutorial help available here.|n"
|
||||
helptext += "\n\n (Write 'give up' if you want to abandon your quest.)"
|
||||
caller.msg(helptext)
|
||||
|
||||
# for the @detail command we inherit from MuxCommand, since
|
||||
# we want to make use of MuxCommand's pre-parsing of '=' in the
|
||||
|
|
@ -200,6 +202,26 @@ class CmdTutorialLook(default_cmds.CmdLook):
|
|||
looking_at_obj.at_desc(looker=caller)
|
||||
return
|
||||
|
||||
class CmdTutorialGiveUp(default_cmds.MuxCommand):
|
||||
"""
|
||||
Give up the tutorial-world quest and return to Limbo, the start room of the
|
||||
server.
|
||||
|
||||
"""
|
||||
key = "give up"
|
||||
aliases = ['abort']
|
||||
|
||||
def func(self):
|
||||
outro_room = OutroRoom.objects.all()
|
||||
if outro_room:
|
||||
outro_room = outro_room[0]
|
||||
else:
|
||||
self.caller.msg("That didn't work (seems like a bug). "
|
||||
"Try to use the |wteleport|n command instead.")
|
||||
return
|
||||
|
||||
self.caller.move_to(outro_room)
|
||||
|
||||
|
||||
class TutorialRoomCmdSet(CmdSet):
|
||||
"""
|
||||
|
|
@ -216,6 +238,7 @@ class TutorialRoomCmdSet(CmdSet):
|
|||
self.add(CmdTutorial())
|
||||
self.add(CmdTutorialSetDetail())
|
||||
self.add(CmdTutorialLook())
|
||||
self.add(CmdTutorialGiveUp())
|
||||
|
||||
|
||||
class TutorialRoom(DefaultRoom):
|
||||
|
|
@ -362,6 +385,31 @@ SUPERUSER_WARNING = (
|
|||
#
|
||||
# -------------------------------------------------------------
|
||||
|
||||
class CmdEvenniaIntro(Command):
|
||||
"""
|
||||
Start the Evennia intro wizard.
|
||||
|
||||
Usage:
|
||||
intro
|
||||
|
||||
"""
|
||||
key = "intro"
|
||||
|
||||
def func(self):
|
||||
from .intro_menu import init_menu
|
||||
# quell also superusers
|
||||
if self.caller.account:
|
||||
self.caller.account.execute_cmd("quell")
|
||||
self.caller.msg("(Auto-quelling)")
|
||||
init_menu(self.caller)
|
||||
|
||||
|
||||
class CmdSetEvenniaIntro(CmdSet):
|
||||
key = "Evennia Intro StartSet"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdEvenniaIntro())
|
||||
|
||||
|
||||
class IntroRoom(TutorialRoom):
|
||||
"""
|
||||
|
|
@ -381,6 +429,7 @@ class IntroRoom(TutorialRoom):
|
|||
"This assigns the health Attribute to "
|
||||
"the account."
|
||||
)
|
||||
self.cmdset.add(CmdSetEvenniaIntro, permanent=True)
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
|
|
@ -396,8 +445,12 @@ class IntroRoom(TutorialRoom):
|
|||
|
||||
if character.is_superuser:
|
||||
string = "-" * 78 + SUPERUSER_WARNING + "-" * 78
|
||||
character.msg("|r%s|n" % string.format(name=character.key, quell="|w@quell|r"))
|
||||
|
||||
character.msg("|r%s|n" % string.format(name=character.key, quell="|wquell|r"))
|
||||
else:
|
||||
# quell user
|
||||
if character.account:
|
||||
character.account.execute_cmd("quell")
|
||||
character.msg("(Auto-quelling while in tutorial-world)")
|
||||
|
||||
# -------------------------------------------------------------
|
||||
#
|
||||
|
|
@ -617,7 +670,7 @@ class BridgeCmdSet(CmdSet):
|
|||
"""This groups the bridge commands. We will store it on the room."""
|
||||
|
||||
key = "Bridge commands"
|
||||
priority = 1 # this gives it precedence over the normal look/help commands.
|
||||
priority = 2 # this gives it precedence over the normal look/help commands.
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"""Called at first cmdset creation"""
|
||||
|
|
@ -679,7 +732,7 @@ class BridgeRoom(WeatherRoom):
|
|||
self.db.east_exit = "gate"
|
||||
self.db.fall_exit = "cliffledge"
|
||||
# add the cmdset on the room.
|
||||
self.cmdset.add_default(BridgeCmdSet)
|
||||
self.cmdset.add(BridgeCmdSet, permanent=True)
|
||||
# since the default Character's at_look() will access the room's
|
||||
# return_description (this skips the cmdset) when
|
||||
# first entering it, we need to explicitly turn off the room
|
||||
|
|
@ -1108,3 +1161,8 @@ class OutroRoom(TutorialRoom):
|
|||
if obj.typeclass_path.startswith("evennia.contrib.tutorial_world"):
|
||||
obj.delete()
|
||||
character.tags.clear(category="tutorial_world")
|
||||
|
||||
def at_object_leave(self, character, destination):
|
||||
if character.account:
|
||||
character.account.execute_cmd("unquell")
|
||||
|
||||
|
|
|
|||
|
|
@ -1981,12 +1981,12 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
# whisper mode
|
||||
msg_type = "whisper"
|
||||
msg_self = (
|
||||
'{self} whisper to {all_receivers}, "{speech}"' if msg_self is True else msg_self
|
||||
'{self} whisper to {all_receivers}, "|n{speech}|n"' if msg_self is True else msg_self
|
||||
)
|
||||
msg_receivers = msg_receivers or '{object} whispers: "{speech}"'
|
||||
msg_receivers = msg_receivers or '{object} whispers: "|n{speech}|n"'
|
||||
msg_location = None
|
||||
else:
|
||||
msg_self = '{self} say, "{speech}"' if msg_self is True else msg_self
|
||||
msg_self = '{self} say, "|n{speech}|n"' if msg_self is True else msg_self
|
||||
msg_location = msg_location or '{object} says, "{speech}"'
|
||||
msg_receivers = msg_receivers or message
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ There main function is `spawn(*prototype)`, where the `prototype`
|
|||
is a dictionary like this:
|
||||
|
||||
```python
|
||||
from evennia.prototypes import prototypes
|
||||
from evennia.prototypes import prototypes, spawner
|
||||
|
||||
prot = {
|
||||
"prototype_key": "goblin",
|
||||
|
|
@ -22,7 +22,10 @@ prot = {
|
|||
"tags": ["mob", "evil", ('greenskin','mob')]
|
||||
"attrs": [("weapon", "sword")]
|
||||
}
|
||||
# spawn something with the prototype
|
||||
goblin = spawner.spawn(prot)
|
||||
|
||||
# make this into a db-saved prototype (optional)
|
||||
prot = prototypes.create_prototype(prot)
|
||||
|
||||
```
|
||||
|
|
@ -82,13 +85,13 @@ import random
|
|||
|
||||
{
|
||||
"prototype_key": "goblin_wizard",
|
||||
"prototype_parent": GOBLIN,
|
||||
"prototype_parent": "GOBLIN",
|
||||
"key": "goblin wizard",
|
||||
"spells": ["fire ball", "lighting bolt"]
|
||||
}
|
||||
|
||||
GOBLIN_ARCHER = {
|
||||
"prototype_parent": GOBLIN,
|
||||
"prototype_parent": "GOBLIN",
|
||||
"key": "goblin archer",
|
||||
"attack_skill": (random, (5, 10))"
|
||||
"attacks": ["short bow"]
|
||||
|
|
@ -104,7 +107,7 @@ ARCHWIZARD = {
|
|||
|
||||
GOBLIN_ARCHWIZARD = {
|
||||
"key" : "goblin archwizard"
|
||||
"prototype_parent": (GOBLIN_WIZARD, ARCHWIZARD),
|
||||
"prototype_parent": ("GOBLIN_WIZARD", "ARCHWIZARD"),
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ LIMBO_DESC = _(
|
|||
"""
|
||||
Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if you need
|
||||
help, want to contribute, report issues or just join the community.
|
||||
As Account #1 you can create a demo/tutorial area with |w@batchcommand tutorial_world.build|n.
|
||||
As Account #1 you can create a demo/tutorial area with '|wbatchcommand tutorial_world.build|n'.
|
||||
"""
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -397,7 +397,7 @@ if WEBSERVER_ENABLED:
|
|||
w_interface = WEBSOCKET_CLIENT_INTERFACE
|
||||
w_ifacestr = ""
|
||||
if w_interface not in ("0.0.0.0", "::") or len(WEBSERVER_INTERFACES) > 1:
|
||||
w_ifacestr = "-%s" % interface
|
||||
w_ifacestr = "-%s" % w_interface
|
||||
port = WEBSOCKET_CLIENT_PORT
|
||||
|
||||
class Websocket(WebSocketServerFactory):
|
||||
|
|
|
|||
|
|
@ -239,16 +239,16 @@ class PortalSessionHandler(SessionHandler):
|
|||
def server_connect(self, protocol_path="", config=dict()):
|
||||
"""
|
||||
Called by server to force the initialization of a new protocol
|
||||
instance. Server wants this instance to get a unique sessid
|
||||
and to be connected back as normal. This is used to initiate
|
||||
irc/rss etc connections.
|
||||
instance. Server wants this instance to get a unique sessid and to be
|
||||
connected back as normal. This is used to initiate irc/rss etc
|
||||
connections.
|
||||
|
||||
Args:
|
||||
protocol_path (st): Full python path to the class factory
|
||||
protocol_path (str): Full python path to the class factory
|
||||
for the protocol used, eg
|
||||
'evennia.server.portal.irc.IRCClientFactory'
|
||||
config (dict): Dictionary of configuration options, fed as
|
||||
`**kwargs` to protocol class' __init__ method.
|
||||
`**kwarg` to protocol class `__init__` method.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If The correct factory class is not found.
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ This implements the following telnet OOB communication protocols:
|
|||
- GMCP (Generic Mud Communication Protocol) as per
|
||||
http://www.ironrealms.com/rapture/manual/files/FeatGMCP-txt.html#Generic_MUD_Communication_Protocol%28GMCP%29
|
||||
|
||||
Following the lead of KaVir's protocol snippet, we first check if
|
||||
client supports MSDP and if not, we fallback to GMCP with a MSDP
|
||||
header where applicable.
|
||||
----
|
||||
|
||||
"""
|
||||
import re
|
||||
|
|
|
|||
|
|
@ -182,6 +182,16 @@ class AjaxWebClient(resource.Resource):
|
|||
csessid = self.get_client_sessid(request)
|
||||
|
||||
remote_addr = request.getClientIP()
|
||||
|
||||
if remote_addr in settings.UPSTREAM_IPS and request.getHeader("x-forwarded-for"):
|
||||
addresses = [x.strip() for x in request.getHeader("x-forwarded-for").split(",")]
|
||||
addresses.reverse()
|
||||
|
||||
for addr in addresses:
|
||||
if addr not in settings.UPSTREAM_IPS:
|
||||
remote_addr = addr
|
||||
break
|
||||
|
||||
host_string = "%s (%s:%s)" % (
|
||||
_SERVERNAME,
|
||||
request.getRequestHostname(),
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ TIMESTEP with a chance given by CHANCE_OF_ACTION by in the order given
|
|||
(no randomness) and allows for setting up a more complex chain of
|
||||
commands (such as creating an account and logging in).
|
||||
|
||||
----
|
||||
|
||||
"""
|
||||
# Dummy runner settings
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ both one or two arguments interchangeably. It also accepts nodes
|
|||
that takes `**kwargs`.
|
||||
|
||||
The menu tree itself is available on the caller as
|
||||
`caller.ndb._menutree`. This makes it a convenient place to store
|
||||
`caller.ndb._evmenu`. This makes it a convenient place to store
|
||||
temporary state variables between nodes, since this NAttribute is
|
||||
deleted when the menu is exited.
|
||||
|
||||
|
|
@ -165,11 +165,114 @@ your default cmdset. Run it with this module, like `testmenu evennia.utils.evmen
|
|||
|
||||
----
|
||||
|
||||
|
||||
## Menu generation from template string
|
||||
|
||||
In evmenu.py is a helper function `parse_menu_template` that parses a
|
||||
template-string and outputs a menu-tree dictionary suitable to pass into
|
||||
EvMenu:
|
||||
::
|
||||
|
||||
menutree = evmenu.parse_menu_template(caller, menu_template, goto_callables)
|
||||
EvMenu(caller, menutree)
|
||||
|
||||
For maximum flexibility you can inject normally-created nodes in the menu tree
|
||||
before passing it to EvMenu. If that's not needed, you can also create a menu
|
||||
in one step with:
|
||||
::
|
||||
|
||||
evmenu.template2menu(caller, menu_template, goto_callables)
|
||||
|
||||
The `goto_callables` is a mapping `{"funcname": callable, ...}`, where each
|
||||
callable must be a module-global function on the form
|
||||
`funcname(caller, raw_string, **kwargs)` (like any goto-callable). The
|
||||
`menu_template` is a multi-line string on the following form:
|
||||
::
|
||||
|
||||
## node start
|
||||
|
||||
This is the text of the start node.
|
||||
The text area can have multiple lines, line breaks etc.
|
||||
|
||||
Each option below is one of these forms
|
||||
key: desc -> gotostr_or_func
|
||||
key: gotostr_or_func
|
||||
>: gotostr_or_func
|
||||
> glob/regex: gotostr_or_func
|
||||
|
||||
## options
|
||||
|
||||
# comments are only allowed from beginning of line.
|
||||
# Indenting is not necessary, but good for readability
|
||||
|
||||
1: Option number 1 -> node1
|
||||
2: Option number 2 -> node2
|
||||
next: This steps next -> go_back()
|
||||
# the -> can be ignored if there is no desc
|
||||
back: go_back(from_node=start)
|
||||
abort: abort
|
||||
|
||||
## node node1
|
||||
|
||||
Text for Node1. Enter a message!
|
||||
<return> to go back.
|
||||
|
||||
## options
|
||||
|
||||
# Starting the option-line with >
|
||||
# allows to perform different actions depending on
|
||||
# what is inserted.
|
||||
|
||||
# this catches everything starting with foo
|
||||
> foo*: handle_foo_message()
|
||||
|
||||
# regex are also allowed (this catches number inputs)
|
||||
> [0-9]+?: handle_numbers()
|
||||
|
||||
# this catches the empty return
|
||||
>: start
|
||||
|
||||
# this catches everything else
|
||||
> *: handle_message(from_node=node1)
|
||||
|
||||
## node node2
|
||||
|
||||
Text for Node2. Just go back.
|
||||
|
||||
## options
|
||||
|
||||
>: start
|
||||
|
||||
# node abort
|
||||
|
||||
This exits the menu since there is no `## options` section.
|
||||
|
||||
Each menu node is defined by a `# node <name>` containing the text of the node,
|
||||
followed by `## options` Also `## NODE` and `## OPTIONS` work. No python code
|
||||
logics is allowed in the template, this code is not evaluated but parsed. More
|
||||
advanced dynamic usage requires a full node-function (which can be added to the
|
||||
generated dict, as said).
|
||||
|
||||
Adding `(..)` to a goto treats it as a callable and it must then be included in
|
||||
the `goto_callable` mapping. Only named keywords (or no args at all) are
|
||||
allowed, these will be added to the `**kwargs` going into the callable. Quoting
|
||||
strings is only needed if wanting to pass strippable spaces, otherwise the
|
||||
key:values will be converted to strings/numbers with literal_eval before passed
|
||||
into the callable.
|
||||
|
||||
The `> ` option takes a glob or regex to perform different actions depending on user
|
||||
input. Make sure to sort these in increasing order of generality since they
|
||||
will be tested in sequence.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import random
|
||||
import re
|
||||
import inspect
|
||||
|
||||
from ast import literal_eval
|
||||
from fnmatch import fnmatch
|
||||
|
||||
from inspect import isfunction, getargspec
|
||||
from django.conf import settings
|
||||
from evennia import Command, CmdSet
|
||||
|
|
@ -179,6 +282,9 @@ from evennia.utils.ansi import strip_ansi
|
|||
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
|
||||
from evennia.commands import cmdhandler
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
# read from protocol NAWS later?
|
||||
_MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
|
|
@ -189,11 +295,10 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
|
|||
|
||||
# Return messages
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
_ERR_NOT_IMPLEMENTED = _(
|
||||
"Menu node '{nodename}' is either not implemented or " "caused an error. Make another choice."
|
||||
"Menu node '{nodename}' is either not implemented or caused an error. "
|
||||
"Make another choice or try 'q' to abort."
|
||||
)
|
||||
_ERR_GENERAL = _("Error in menu node '{nodename}'.")
|
||||
_ERR_NO_OPTION_DESC = _("No description.")
|
||||
|
|
@ -227,6 +332,19 @@ class EvMenuError(RuntimeError):
|
|||
pass
|
||||
|
||||
|
||||
class EvMenuGotoAbortMessage(RuntimeError):
|
||||
"""
|
||||
This can be raised by a goto-callable to abort the goto flow. The message
|
||||
stored with the executable will be sent to the caller who will remain on
|
||||
the current node. This can be used to pass single-line returns without
|
||||
re-running the entire node with text and options.
|
||||
|
||||
Example:
|
||||
raise EvMenuGotoMessage("That makes no sense.")
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
#
|
||||
# Menu command and command set
|
||||
|
|
@ -243,6 +361,10 @@ class CmdEvMenuNode(Command):
|
|||
aliases = [_CMD_NOMATCH]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Menu"
|
||||
auto_help_display_key = "<menu commands>"
|
||||
|
||||
def get_help(self):
|
||||
return "Menu commands are explained within the menu."
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
|
|
@ -271,28 +393,28 @@ class CmdEvMenuNode(Command):
|
|||
caller = self.caller
|
||||
# we store Session on the menu since this can be hard to
|
||||
# get in multisession environemtns if caller is an Account.
|
||||
menu = caller.ndb._menutree
|
||||
menu = caller.ndb._evmenu
|
||||
if not menu:
|
||||
if _restore(caller):
|
||||
return
|
||||
orig_caller = caller
|
||||
caller = caller.account if hasattr(caller, "account") else None
|
||||
menu = caller.ndb._menutree if caller else None
|
||||
menu = caller.ndb._evmenu if caller else None
|
||||
if not menu:
|
||||
if caller and _restore(caller):
|
||||
return
|
||||
caller = self.session
|
||||
menu = caller.ndb._menutree
|
||||
menu = caller.ndb._evmenu
|
||||
if not menu:
|
||||
# can't restore from a session
|
||||
err = "Menu object not found as %s.ndb._menutree!" % orig_caller
|
||||
err = "Menu object not found as %s.ndb._evmenu!" % orig_caller
|
||||
orig_caller.msg(
|
||||
err
|
||||
) # don't give the session as a kwarg here, direct to original
|
||||
raise EvMenuError(err)
|
||||
# we must do this after the caller with the menu has been correctly identified since it
|
||||
# can be either Account, Object or Session (in the latter case this info will be superfluous).
|
||||
caller.ndb._menutree._session = self.session
|
||||
caller.ndb._evmenu._session = self.session
|
||||
# we have a menu, use it.
|
||||
menu.parse_input(self.raw_string)
|
||||
|
||||
|
|
@ -324,7 +446,7 @@ class EvMenuCmdSet(CmdSet):
|
|||
# -------------------------------------------------------------
|
||||
|
||||
|
||||
class EvMenu(object):
|
||||
class EvMenu:
|
||||
"""
|
||||
This object represents an operational menu. It is initialized from
|
||||
a menufile.py instruction.
|
||||
|
|
@ -425,9 +547,9 @@ class EvMenu(object):
|
|||
EvMenuError: If the start/end node is not found in menu tree.
|
||||
|
||||
Notes:
|
||||
While running, the menu is stored on the caller as `caller.ndb._menutree`. Also
|
||||
While running, the menu is stored on the caller as `caller.ndb._evmenu`. Also
|
||||
the current Session (from the Command, so this is still valid in multisession
|
||||
environments) is available through `caller.ndb._menutree._session`. The `_menutree`
|
||||
environments) is available through `caller.ndb._evmenu._session`. The `_evmenu`
|
||||
property is a good one for storing intermediary data on between nodes since it
|
||||
will be automatically deleted when the menu closes.
|
||||
|
||||
|
|
@ -478,7 +600,7 @@ class EvMenu(object):
|
|||
self.test_nodetext = ""
|
||||
|
||||
# assign kwargs as initialization vars on ourselves.
|
||||
if set(
|
||||
reserved_clash = set(
|
||||
(
|
||||
"_startnode",
|
||||
"_menutree",
|
||||
|
|
@ -492,22 +614,26 @@ class EvMenu(object):
|
|||
"cmdset_mergetype",
|
||||
"auto_quit",
|
||||
)
|
||||
).intersection(set(kwargs.keys())):
|
||||
).intersection(set(kwargs.keys()))
|
||||
if reserved_clash:
|
||||
raise RuntimeError(
|
||||
"One or more of the EvMenu `**kwargs` is reserved by EvMenu for internal use."
|
||||
f"One or more of the EvMenu `**kwargs` ({list(reserved_clash)}) is reserved by EvMenu for internal use."
|
||||
)
|
||||
for key, val in kwargs.items():
|
||||
setattr(self, key, val)
|
||||
|
||||
if self.caller.ndb._menutree:
|
||||
if self.caller.ndb._evmenu:
|
||||
# an evmenu already exists - we try to close it cleanly. Note that this will
|
||||
# not fire the previous menu's end node.
|
||||
try:
|
||||
self.caller.ndb._menutree.close_menu()
|
||||
self.caller.ndb._evmenu.close_menu()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# store ourself on the object
|
||||
self.caller.ndb._evmenu = self
|
||||
|
||||
# DEPRECATED - for backwards-compatibility
|
||||
self.caller.ndb._menutree = self
|
||||
|
||||
if persistent:
|
||||
|
|
@ -527,7 +653,7 @@ class EvMenu(object):
|
|||
caller.attributes.add("_menutree_saved", (self.__class__, (menudata,), calldict))
|
||||
caller.attributes.add("_menutree_saved_startnode", (startnode, startnode_input))
|
||||
except Exception as err:
|
||||
caller.msg(_ERROR_PERSISTENT_SAVING.format(error=err), session=self._session)
|
||||
self.msg(_ERROR_PERSISTENT_SAVING.format(error=err))
|
||||
logger.log_trace(_TRACE_PERSISTENT_SAVING)
|
||||
persistent = False
|
||||
|
||||
|
|
@ -537,11 +663,19 @@ class EvMenu(object):
|
|||
menu_cmdset.priority = int(cmdset_priority)
|
||||
self.caller.cmdset.add(menu_cmdset, permanent=persistent)
|
||||
|
||||
reserved_startnode_kwargs = set(("nodename", "raw_string"))
|
||||
startnode_kwargs = {}
|
||||
if isinstance(startnode_input, (tuple, list)) and len(startnode_input) > 1:
|
||||
startnode_input, startnode_kwargs = startnode_input[:2]
|
||||
if not isinstance(startnode_kwargs, dict):
|
||||
raise EvMenuError("startnode_input must be either a str or a tuple (str, dict).")
|
||||
clashing_kwargs = reserved_startnode_kwargs.intersection(set(startnode_kwargs.keys()))
|
||||
if clashing_kwargs:
|
||||
raise RuntimeError(
|
||||
f"Evmenu startnode_inputs includes kwargs {tuple(clashing_kwargs)} that "
|
||||
"clashes with EvMenu's internal usage."
|
||||
)
|
||||
|
||||
# start the menu
|
||||
self.goto(self._startnode, startnode_input, **startnode_kwargs)
|
||||
|
||||
|
|
@ -631,7 +765,7 @@ class EvMenu(object):
|
|||
ret = callback(self.caller)
|
||||
except EvMenuError:
|
||||
errmsg = _ERR_GENERAL.format(nodename=callback)
|
||||
self.caller.msg(errmsg, self._session)
|
||||
self.msg(errmsg)
|
||||
logger.log_trace()
|
||||
raise
|
||||
|
||||
|
|
@ -656,20 +790,21 @@ class EvMenu(object):
|
|||
try:
|
||||
node = self._menutree[nodename]
|
||||
except KeyError:
|
||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||
self.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename))
|
||||
raise EvMenuError
|
||||
try:
|
||||
kwargs["_current_nodename"] = nodename
|
||||
ret = self._safe_call(node, raw_string, **kwargs)
|
||||
if isinstance(ret, (tuple, list)) and len(ret) > 1:
|
||||
nodetext, options = ret[:2]
|
||||
else:
|
||||
nodetext, options = ret, None
|
||||
except KeyError:
|
||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||
self.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename))
|
||||
logger.log_trace()
|
||||
raise EvMenuError
|
||||
except Exception:
|
||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
|
||||
self.msg(_ERR_GENERAL.format(nodename=nodename))
|
||||
logger.log_trace()
|
||||
raise
|
||||
|
||||
|
|
@ -679,8 +814,27 @@ class EvMenu(object):
|
|||
|
||||
return nodetext, options
|
||||
|
||||
def msg(self, txt):
|
||||
"""
|
||||
This is a central point for sending return texts to the caller. It
|
||||
allows for a central point to add custom messaging when creating custom
|
||||
EvMenu overrides.
|
||||
|
||||
Args:
|
||||
txt (str): The text to send.
|
||||
|
||||
Notes:
|
||||
By default this will send to the same session provided to EvMenu
|
||||
(if `session` kwarg was provided to `EvMenu.__init__`). It will
|
||||
also send it with a `type=menu` for the benefit of OOB/webclient.
|
||||
|
||||
"""
|
||||
self.caller.msg(text=(txt, {"type": "menu"}), session=self._session)
|
||||
|
||||
def run_exec(self, nodename, raw_string, **kwargs):
|
||||
"""
|
||||
NOTE: This is deprecated. Use `goto` directly instead.
|
||||
|
||||
Run a function or node as a callback (with the 'exec' option key).
|
||||
|
||||
Args:
|
||||
|
|
@ -723,7 +877,7 @@ class EvMenu(object):
|
|||
ret, kwargs = ret[:2]
|
||||
except EvMenuError as err:
|
||||
errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string.rstrip(), err)
|
||||
self.caller.msg("|r%s|n" % errmsg)
|
||||
self.msg("|r%s|n" % errmsg)
|
||||
logger.log_trace(errmsg)
|
||||
return
|
||||
|
||||
|
|
@ -904,12 +1058,14 @@ class EvMenu(object):
|
|||
# avoid multiple calls from different sources
|
||||
self._quitting = True
|
||||
self.caller.cmdset.remove(EvMenuCmdSet)
|
||||
del self.caller.ndb._menutree
|
||||
del self.caller.ndb._evmenu
|
||||
if self._persistent:
|
||||
self.caller.attributes.remove("_menutree_saved")
|
||||
self.caller.attributes.remove("_menutree_saved_startnode")
|
||||
if self.cmd_on_exit is not None:
|
||||
self.cmd_on_exit(self.caller, self)
|
||||
# special for template-generated menues
|
||||
del self.caller.db._evmenu_template_contents
|
||||
|
||||
def print_debug_info(self, arg):
|
||||
"""
|
||||
|
|
@ -968,7 +1124,7 @@ class EvMenu(object):
|
|||
)
|
||||
+ "\n |y... END MENU DEBUG|n"
|
||||
)
|
||||
self.caller.msg(debugtxt)
|
||||
self.msg(debugtxt)
|
||||
|
||||
def parse_input(self, raw_string):
|
||||
"""
|
||||
|
|
@ -985,30 +1141,35 @@ class EvMenu(object):
|
|||
"""
|
||||
cmd = strip_ansi(raw_string.strip().lower())
|
||||
|
||||
if cmd in self.options:
|
||||
# this will take precedence over the default commands
|
||||
# below
|
||||
goto, goto_kwargs, execfunc, exec_kwargs = self.options[cmd]
|
||||
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||
elif self.auto_look and cmd in ("look", "l"):
|
||||
self.display_nodetext()
|
||||
elif self.auto_help and cmd in ("help", "h"):
|
||||
self.display_helptext()
|
||||
elif self.auto_quit and cmd in ("quit", "q", "exit"):
|
||||
self.close_menu()
|
||||
elif self.debug_mode and cmd.startswith("menudebug"):
|
||||
self.print_debug_info(cmd[9:].strip())
|
||||
elif self.default:
|
||||
goto, goto_kwargs, execfunc, exec_kwargs = self.default
|
||||
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||
else:
|
||||
self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session)
|
||||
try:
|
||||
if self.options and cmd in self.options:
|
||||
# this will take precedence over the default commands
|
||||
# below
|
||||
goto, goto_kwargs, execfunc, exec_kwargs = self.options[cmd]
|
||||
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||
elif self.auto_look and cmd in ("look", "l"):
|
||||
self.display_nodetext()
|
||||
elif self.auto_help and cmd in ("help", "h"):
|
||||
self.display_helptext()
|
||||
elif self.auto_quit and cmd in ("quit", "q", "exit"):
|
||||
self.close_menu()
|
||||
elif self.debug_mode and cmd.startswith("menudebug"):
|
||||
self.print_debug_info(cmd[9:].strip())
|
||||
elif self.default:
|
||||
goto, goto_kwargs, execfunc, exec_kwargs = self.default
|
||||
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||
else:
|
||||
self.msg(_HELP_NO_OPTION_MATCH)
|
||||
except EvMenuGotoAbortMessage as err:
|
||||
# custom interrupt from inside a goto callable - print the message and
|
||||
# stay on the current node.
|
||||
self.msg(str(err))
|
||||
|
||||
def display_nodetext(self):
|
||||
self.caller.msg(self.nodetext, session=self._session)
|
||||
self.msg(self.nodetext)
|
||||
|
||||
def display_helptext(self):
|
||||
self.caller.msg(self.helptext, session=self._session)
|
||||
self.msg(self.helptext)
|
||||
|
||||
# formatters - override in a child class
|
||||
|
||||
|
|
@ -1460,219 +1621,288 @@ def get_input(caller, prompt, callback, session=None, *args, **kwargs):
|
|||
|
||||
# -------------------------------------------------------------
|
||||
#
|
||||
# test menu strucure and testing command
|
||||
# Menu generation from menu template string
|
||||
#
|
||||
# -------------------------------------------------------------
|
||||
|
||||
_RE_NODE = re.compile(r"##\s*?NODE\s+?(?P<nodename>\S[\S\s]*?)$", re.I + re.M)
|
||||
_RE_OPTIONS_SEP = re.compile(r"##\s*?OPTIONS\s*?$", re.I + re.M)
|
||||
_RE_CALLABLE = re.compile(r"\S+?\(\)", re.I + re.M)
|
||||
_RE_CALLABLE = re.compile(
|
||||
r"(?P<funcname>\S+?)(?:\((?P<kwargs>[\S\s]+?)\)|\(\))", re.I + re.M
|
||||
)
|
||||
|
||||
def _generate_goto(caller, **kwargs):
|
||||
return kwargs.get("name", "test_dynamic_node"), {"name": "replaced!"}
|
||||
_HELP_NO_OPTION_MATCH = _("Choose an option or try 'help'.")
|
||||
|
||||
_OPTION_INPUT_MARKER = ">"
|
||||
_OPTION_ALIAS_MARKER = ";"
|
||||
_OPTION_SEP_MARKER = ":"
|
||||
_OPTION_CALL_MARKER = "->"
|
||||
_OPTION_COMMENT_START = "#"
|
||||
|
||||
|
||||
def test_start_node(caller):
|
||||
menu = caller.ndb._menutree
|
||||
text = """
|
||||
This is an example menu.
|
||||
# Input/option/goto handler functions that allows for dynamically generated
|
||||
# nodes read from the menu template.
|
||||
|
||||
If you enter anything except the valid options, your input will be
|
||||
recorded and you will be brought to a menu entry showing your
|
||||
input.
|
||||
def _process_callable(caller, goto, goto_callables, raw_string,
|
||||
current_nodename, kwargs):
|
||||
"""
|
||||
Central helper for parsing a goto-callable (`funcname(**kwargs)`) out of
|
||||
the right-hand-side of the template options and map this to an actual
|
||||
callable registered with the template generator. This involves parsing the
|
||||
func-name and running literal-eval on its kwargs.
|
||||
|
||||
Select options or use 'quit' to exit the menu.
|
||||
"""
|
||||
match = _RE_CALLABLE.match(goto)
|
||||
if match:
|
||||
gotofunc = match.group("funcname")
|
||||
gotokwargs = match.group("kwargs") or ""
|
||||
if gotofunc in goto_callables:
|
||||
for kwarg in gotokwargs.split(","):
|
||||
if kwarg and "=" in kwarg:
|
||||
key, value = [part.strip() for part in kwarg.split("=", 1)]
|
||||
if key in ("evmenu_goto", "evmenu_gotomap", "_current_nodename",
|
||||
"evmenu_current_nodename", "evmenu_goto_callables"):
|
||||
raise RuntimeError(
|
||||
f"EvMenu template error: goto-callable '{goto}' uses a "
|
||||
f"kwarg ({kwarg}) that is reserved for the EvMenu templating "
|
||||
"system. Rename the kwarg.")
|
||||
try:
|
||||
key = literal_eval(key)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
value = literal_eval(value)
|
||||
except ValueError:
|
||||
pass
|
||||
kwargs[key] = value
|
||||
|
||||
The menu was initialized with two variables: %s and %s.
|
||||
""" % (
|
||||
menu.testval,
|
||||
menu.testval2,
|
||||
)
|
||||
goto = goto_callables[gotofunc](caller, raw_string, **kwargs)
|
||||
if goto is None:
|
||||
return goto, {"generated_nodename": current_nodename}
|
||||
return goto, {"generated_nodename": goto}
|
||||
|
||||
options = (
|
||||
{
|
||||
"key": ("|yS|net", "s"),
|
||||
"desc": "Set an attribute on yourself.",
|
||||
"exec": lambda caller: caller.attributes.add("menuattrtest", "Test value"),
|
||||
"goto": "test_set_node",
|
||||
},
|
||||
{
|
||||
"key": ("|yL|nook", "l"),
|
||||
"desc": "Look and see a custom message.",
|
||||
"goto": "test_look_node",
|
||||
},
|
||||
{"key": ("|yV|niew", "v"), "desc": "View your own name", "goto": "test_view_node"},
|
||||
{
|
||||
"key": ("|yD|nynamic", "d"),
|
||||
"desc": "Dynamic node",
|
||||
"goto": (_generate_goto, {"name": "test_dynamic_node"}),
|
||||
},
|
||||
{
|
||||
"key": ("|yQ|nuit", "quit", "q", "Q"),
|
||||
"desc": "Quit this menu example.",
|
||||
"goto": "test_end_node",
|
||||
},
|
||||
{"key": "_default", "goto": "test_displayinput_node"},
|
||||
)
|
||||
|
||||
def _generated_goto_func(caller, raw_string, **kwargs):
|
||||
"""
|
||||
This rerouter handles normal direct goto func call matches.
|
||||
|
||||
key : ... -> goto_callable(**kwargs)
|
||||
|
||||
"""
|
||||
goto = kwargs["evmenu_goto"]
|
||||
goto_callables = kwargs["evmenu_goto_callables"]
|
||||
current_nodename = kwargs["evmenu_current_nodename"]
|
||||
return _process_callable(caller, goto, goto_callables, raw_string,
|
||||
current_nodename, kwargs)
|
||||
|
||||
|
||||
def _generated_input_goto_func(caller, raw_string, **kwargs):
|
||||
"""
|
||||
This goto-func acts as a rerouter for >-type line parsing (by acting as the
|
||||
_default option). The patterns discovered in the menu maps to different
|
||||
*actual* goto-funcs. We map to those here.
|
||||
|
||||
>pattern: ... -> goto_callable
|
||||
|
||||
"""
|
||||
gotomap = kwargs["evmenu_gotomap"]
|
||||
goto_callables = kwargs["evmenu_goto_callables"]
|
||||
current_nodename = kwargs["evmenu_current_nodename"]
|
||||
raw_string = raw_string.strip("\n") # strip is necessary to catch empty return
|
||||
|
||||
# start with glob patterns
|
||||
for pattern, goto in gotomap.items():
|
||||
if fnmatch(raw_string.lower(), pattern):
|
||||
return _process_callable(caller, goto, goto_callables, raw_string,
|
||||
current_nodename, kwargs)
|
||||
# no glob pattern match; try regex
|
||||
for pattern, goto in gotomap.items():
|
||||
if pattern and re.match(pattern, raw_string.lower(), flags=re.I + re.M):
|
||||
return _process_callable(caller, goto, goto_callables, raw_string,
|
||||
current_nodename, kwargs)
|
||||
# no match, show error
|
||||
raise EvMenuGotoAbortMessage(_HELP_NO_OPTION_MATCH)
|
||||
|
||||
|
||||
def _generated_node(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Every node in the templated menu will be this node, but with dynamically
|
||||
changing text/options. It must be a global function like this because
|
||||
otherwise we could not make the templated-menu persistent.
|
||||
|
||||
"""
|
||||
text, options = caller.db._evmenu_template_contents[kwargs["_current_nodename"]]
|
||||
return text, options
|
||||
|
||||
|
||||
def test_look_node(caller):
|
||||
text = "This is a custom look location!"
|
||||
options = {
|
||||
"key": ("|yL|nook", "l"),
|
||||
"desc": "Go back to the previous menu.",
|
||||
"goto": "test_start_node",
|
||||
}
|
||||
return text, options
|
||||
def parse_menu_template(caller, menu_template, goto_callables=None):
|
||||
"""
|
||||
Parse menu-template string. The main function of the EvMenu templating system.
|
||||
|
||||
Args:
|
||||
caller (Object or Account): Entity using the menu.
|
||||
menu_template (str): Menu described using the templating format.
|
||||
goto_callables (dict, optional): Mapping between call-names and callables
|
||||
on the form `callable(caller, raw_string, **kwargs)`. These are what is
|
||||
available to use in the `menu_template` string.
|
||||
|
||||
def test_set_node(caller):
|
||||
text = (
|
||||
Returns:
|
||||
dict: A `{"node": nodefunc}` menutree suitable to pass into EvMenu.
|
||||
|
||||
"""
|
||||
def _validate_kwarg(goto, kwarg):
|
||||
"""
|
||||
The attribute 'menuattrtest' was set to
|
||||
|
||||
|w%s|n
|
||||
|
||||
(check it with examine after quitting the menu).
|
||||
|
||||
This node's has only one option, and one of its key aliases is the
|
||||
string "_default", meaning it will catch any input, in this case
|
||||
to return to the main menu. So you can e.g. press <return> to go
|
||||
back now.
|
||||
"""
|
||||
% caller.db.menuattrtest, # optional help text for this node
|
||||
Validate goto-callable kwarg is on correct form.
|
||||
"""
|
||||
This is the help entry for this node. It is created by returning
|
||||
the node text as a tuple - the second string in that tuple will be
|
||||
used as the help text.
|
||||
""",
|
||||
)
|
||||
if not "=" in kwarg:
|
||||
raise RuntimeError(
|
||||
f"EvMenu template error: goto-callable '{goto}' has a "
|
||||
f"non-kwarg argument ({kwarg}). All callables in the "
|
||||
"template must have only keyword-arguments, or no "
|
||||
"args at all.")
|
||||
key, _ = [part.strip() for part in kwarg.split("=", 1)]
|
||||
if key in ("evmenu_goto", "evmenu_gotomap", "_current_nodename",
|
||||
"evmenu_current_nodename", "evmenu_goto_callables"):
|
||||
raise RuntimeError(
|
||||
f"EvMenu template error: goto-callable '{goto}' uses a "
|
||||
f"kwarg ({kwarg}) that is reserved for the EvMenu templating "
|
||||
"system. Rename the kwarg.")
|
||||
|
||||
options = {"key": ("back (default)", "_default"), "goto": "test_start_node"}
|
||||
return text, options
|
||||
|
||||
|
||||
def test_view_node(caller, **kwargs):
|
||||
text = (
|
||||
def _parse_options(nodename, optiontxt, goto_callables):
|
||||
"""
|
||||
Your name is |g%s|n!
|
||||
|
||||
click |lclook|lthere|le to trigger a look command under MXP.
|
||||
This node's option has no explicit key (nor the "_default" key
|
||||
set), and so gets assigned a number automatically. You can infact
|
||||
-always- use numbers (1...N) to refer to listed options also if you
|
||||
don't see a string option key (try it!).
|
||||
"""
|
||||
% caller.key
|
||||
)
|
||||
if kwargs.get("executed_from_dynamic_node", False):
|
||||
# we are calling this node as a exec, skip return values
|
||||
caller.msg("|gCalled from dynamic node:|n \n {}".format(text))
|
||||
return
|
||||
else:
|
||||
options = {"desc": "back to main", "goto": "test_start_node"}
|
||||
return text, options
|
||||
|
||||
|
||||
def test_displayinput_node(caller, raw_string):
|
||||
text = (
|
||||
Parse option section into option dict.
|
||||
"""
|
||||
You entered the text:
|
||||
options = []
|
||||
optiontxt = optiontxt[0].strip() if optiontxt else ""
|
||||
optionlist = [optline.strip() for optline in optiontxt.split("\n")]
|
||||
inputparsemap = {}
|
||||
|
||||
"|w%s|n"
|
||||
for inum, optline in enumerate(optionlist):
|
||||
if optline.startswith(_OPTION_COMMENT_START) or _OPTION_SEP_MARKER not in optline:
|
||||
# skip comments or invalid syntax
|
||||
continue
|
||||
key = ""
|
||||
desc = ""
|
||||
pattern = None
|
||||
|
||||
... which could now be handled or stored here in some way if this
|
||||
was not just an example.
|
||||
key, goto = [part.strip() for part in optline.split(_OPTION_SEP_MARKER, 1)]
|
||||
|
||||
This node has an option with a single alias "_default", which
|
||||
makes it hidden from view. It catches all input (except the
|
||||
in-menu help/quit commands) and will, in this case, bring you back
|
||||
to the start node.
|
||||
# desc -> goto
|
||||
if _OPTION_CALL_MARKER in goto:
|
||||
desc, goto = [part.strip() for part in goto.split(_OPTION_CALL_MARKER, 1)]
|
||||
|
||||
# validate callable
|
||||
match = _RE_CALLABLE.match(goto)
|
||||
if match:
|
||||
kwargs = match.group("kwargs")
|
||||
if kwargs:
|
||||
for kwarg in kwargs.split(','):
|
||||
_validate_kwarg(goto, kwarg)
|
||||
|
||||
# parse key [;aliases|pattern]
|
||||
key = [part.strip() for part in key.split(_OPTION_ALIAS_MARKER)]
|
||||
if not key:
|
||||
# fall back to this being the Nth option
|
||||
key = [f"{inum + 1}"]
|
||||
main_key = key[0]
|
||||
|
||||
if main_key.startswith(_OPTION_INPUT_MARKER):
|
||||
# if we have a pattern, build the arguments for _default later
|
||||
pattern = main_key[len(_OPTION_INPUT_MARKER):].strip()
|
||||
inputparsemap[pattern] = goto
|
||||
else:
|
||||
# a regular goto string/callable target
|
||||
option = {
|
||||
"key": key,
|
||||
"goto": (
|
||||
_generated_goto_func,
|
||||
{
|
||||
"evmenu_goto": goto,
|
||||
"evmenu_current_nodename": nodename,
|
||||
"evmenu_goto_callables": goto_callables,
|
||||
},
|
||||
),
|
||||
}
|
||||
if desc:
|
||||
option["desc"] = desc
|
||||
options.append(option)
|
||||
|
||||
if inputparsemap:
|
||||
# if this exists we must create a _default entry too
|
||||
options.append(
|
||||
{
|
||||
"key": "_default",
|
||||
"goto": (
|
||||
_generated_input_goto_func,
|
||||
{
|
||||
"evmenu_gotomap": inputparsemap,
|
||||
"evmenu_current_nodename": nodename,
|
||||
"evmenu_goto_callables": goto_callables,
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return options
|
||||
|
||||
def _parse(caller, menu_template, goto_callables):
|
||||
"""
|
||||
Parse the menu string format into a node tree.
|
||||
"""
|
||||
nodetree = {}
|
||||
splits = _RE_NODE.split(menu_template)
|
||||
splits = splits[1:] if splits else []
|
||||
|
||||
# from evennia import set_trace;set_trace(term_size=(140,120))
|
||||
content_map = {}
|
||||
for node_ind in range(0, len(splits), 2):
|
||||
nodename, nodetxt = splits[node_ind], splits[node_ind + 1]
|
||||
text, *optiontxt = _RE_OPTIONS_SEP.split(nodetxt, maxsplit=2)
|
||||
options = _parse_options(nodename, optiontxt, goto_callables)
|
||||
content_map[nodename] = (text, options)
|
||||
nodetree[nodename] = _generated_node
|
||||
caller.db._evmenu_template_contents = content_map
|
||||
|
||||
return nodetree
|
||||
|
||||
return _parse(caller, menu_template, goto_callables)
|
||||
|
||||
|
||||
def template2menu(
|
||||
caller,
|
||||
menu_template,
|
||||
goto_callables=None,
|
||||
startnode="start",
|
||||
persistent=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
% raw_string.rstrip()
|
||||
)
|
||||
options = {"key": "_default", "goto": "test_start_node"}
|
||||
return text, options
|
||||
Helper function to generate and start an EvMenu based on a menu template
|
||||
string. This will internall call `parse_menu_template` and run a default
|
||||
EvMenu with its results.
|
||||
|
||||
Args:
|
||||
caller (Object or Account): The entity using the menu.
|
||||
menu_template (str): The menu-template string describing the content
|
||||
and structure of the menu. It can also be the python-path to, or a module
|
||||
containing a `MENU_TEMPLATE` global variable with the template.
|
||||
goto_callables (dict, optional): Mapping of callable-names to
|
||||
module-global objects to reference by name in the menu-template.
|
||||
Must be on the form `callable(caller, raw_string, **kwargs)`.
|
||||
startnode (str, optional): The name of the startnode, if not 'start'.
|
||||
persistent (bool, optional): If the generated menu should be persistent.
|
||||
**kwargs: All kwargs will be passed into EvMenu.
|
||||
|
||||
def _test_call(caller, raw_input, **kwargs):
|
||||
mode = kwargs.get("mode", "exec")
|
||||
|
||||
caller.msg(
|
||||
"\n|y'{}' |n_test_call|y function called with\n "
|
||||
'caller: |n{}\n |yraw_input: "|n{}|y" \n kwargs: |n{}\n'.format(
|
||||
mode, caller, raw_input.rstrip(), kwargs
|
||||
)
|
||||
)
|
||||
|
||||
if mode == "exec":
|
||||
kwargs = {"random": random.random()}
|
||||
caller.msg("function modify kwargs to {}".format(kwargs))
|
||||
else:
|
||||
caller.msg("|ypassing function kwargs without modification.|n")
|
||||
|
||||
return "test_dynamic_node", kwargs
|
||||
|
||||
|
||||
def test_dynamic_node(caller, **kwargs):
|
||||
text = """
|
||||
This is a dynamic node with input:
|
||||
{}
|
||||
""".format(
|
||||
kwargs
|
||||
)
|
||||
options = (
|
||||
{
|
||||
"desc": "pass a new random number to this node",
|
||||
"goto": ("test_dynamic_node", {"random": random.random()}),
|
||||
},
|
||||
{
|
||||
"desc": "execute a func with kwargs",
|
||||
"exec": (_test_call, {"mode": "exec", "test_random": random.random()}),
|
||||
},
|
||||
{"desc": "dynamic_goto", "goto": (_test_call, {"mode": "goto", "goto_input": "test"})},
|
||||
{
|
||||
"desc": "exec test_view_node with kwargs",
|
||||
"exec": ("test_view_node", {"executed_from_dynamic_node": True}),
|
||||
"goto": "test_dynamic_node",
|
||||
},
|
||||
{"desc": "back to main", "goto": "test_start_node"},
|
||||
)
|
||||
|
||||
return text, options
|
||||
|
||||
|
||||
def test_end_node(caller):
|
||||
text = """
|
||||
This is the end of the menu and since it has no options the menu
|
||||
will exit here, followed by a call of the "look" command.
|
||||
"""
|
||||
return text, None
|
||||
|
||||
|
||||
class CmdTestMenu(Command):
|
||||
"""
|
||||
Test menu
|
||||
|
||||
Usage:
|
||||
testmenu <menumodule>
|
||||
|
||||
Starts a demo menu from a menu node definition module.
|
||||
Returns:
|
||||
EvMenu: The generated EvMenu.
|
||||
|
||||
"""
|
||||
|
||||
key = "testmenu"
|
||||
|
||||
def func(self):
|
||||
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: testmenu menumodule")
|
||||
return
|
||||
# start menu
|
||||
EvMenu(
|
||||
self.caller,
|
||||
self.args.strip(),
|
||||
startnode="test_start_node",
|
||||
persistent=True,
|
||||
cmdset_mergetype="Replace",
|
||||
testval="val",
|
||||
testval2="val2",
|
||||
)
|
||||
goto_callables = goto_callables or {}
|
||||
menu_tree = parse_menu_template(caller, menu_template, goto_callables)
|
||||
return EvMenu(
|
||||
caller,
|
||||
menu_tree,
|
||||
persistent=persistent,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
|
|||
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
|
||||
from anything import Anything
|
||||
from django.test import TestCase
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.utils import evmenu
|
||||
from evennia.utils import ansi
|
||||
from mock import MagicMock
|
||||
|
|
@ -229,7 +231,7 @@ class TestEvMenu(TestCase):
|
|||
|
||||
class TestEvMenuExample(TestEvMenu):
|
||||
|
||||
menutree = "evennia.utils.evmenu"
|
||||
menutree = "evennia.utils.tests.data.evmenu_example"
|
||||
startnode = "test_start_node"
|
||||
kwargs = {"testval": "val", "testval2": "val2"}
|
||||
debug_output = False
|
||||
|
|
@ -262,3 +264,79 @@ class TestEvMenuExample(TestEvMenu):
|
|||
def test_kwargsave(self):
|
||||
self.assertTrue(hasattr(self.menu, "testval"))
|
||||
self.assertTrue(hasattr(self.menu, "testval2"))
|
||||
|
||||
|
||||
def _callnode1(caller, raw_string, **kwargs):
|
||||
return "node1"
|
||||
|
||||
|
||||
def _callnode2(caller, raw_string, **kwargs):
|
||||
return "node2"
|
||||
|
||||
|
||||
class TestMenuTemplateParse(EvenniaTest):
|
||||
"""Test menu templating helpers"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.menu_template = """
|
||||
## node start
|
||||
|
||||
Neque ea alias perferendis molestiae eligendi. Debitis exercitationem
|
||||
exercitationem quas blanditiis quisquam officia ut. Fugit aut fugit enim quia
|
||||
non. Earum et excepturi animi ex esse accusantium et. Id adipisci eos enim
|
||||
ratione.
|
||||
|
||||
## options
|
||||
|
||||
1: first option -> node1
|
||||
2: second option -> node2
|
||||
next: node1
|
||||
|
||||
## node node1
|
||||
|
||||
Node 1
|
||||
|
||||
## options
|
||||
|
||||
fwd: node2
|
||||
call1: callnode1()
|
||||
call2: callnode2(foo=bar, bar=22, goo="another test")
|
||||
>: start
|
||||
|
||||
## node node2
|
||||
|
||||
Text of node 2
|
||||
|
||||
## options
|
||||
|
||||
> foo*: node1
|
||||
> [0-9]+?: node2
|
||||
> back: start
|
||||
|
||||
"""
|
||||
self.goto_callables = {"callnode1": _callnode1, "callnode2": _callnode2}
|
||||
|
||||
def test_parse_menu_template(self):
|
||||
"""EvMenu template testing"""
|
||||
|
||||
menutree = evmenu.parse_menu_template(self.char1, self.menu_template,
|
||||
self.goto_callables)
|
||||
self.assertEqual(menutree, {"start": Anything, "node1": Anything, "node2": Anything})
|
||||
|
||||
def test_template2menu(self):
|
||||
evmenu.template2menu(self.char1, self.menu_template, self.goto_callables)
|
||||
|
||||
def test_parse_menu_fail(self):
|
||||
template = """
|
||||
## NODE
|
||||
|
||||
Text
|
||||
|
||||
## OPTIONS
|
||||
|
||||
next: callnode2(invalid)
|
||||
"""
|
||||
with self.assertRaises(RuntimeError):
|
||||
evmenu.parse_menu_template(self.char1, template, self.goto_callables)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
let goldenlayout = (function () {
|
||||
|
||||
var myLayout;
|
||||
var knownTypes = ["all", "untagged"];
|
||||
var knownTypes = ["all", "untagged", "testing"];
|
||||
var untagged = [];
|
||||
|
||||
var newTabConfig = {
|
||||
|
|
@ -404,10 +404,12 @@ let goldenlayout = (function () {
|
|||
|
||||
|
||||
//
|
||||
// returns an array of pane divs that the given message should be sent to
|
||||
//
|
||||
var onText = function (args, kwargs) {
|
||||
var routeMessage = function (args, kwargs) {
|
||||
// If the message is not itself tagged, we"ll assume it
|
||||
// should go into any panes with "all" and "untagged" set
|
||||
var divArray = [];
|
||||
var msgtype = "untagged";
|
||||
|
||||
if ( kwargs && "type" in kwargs ) {
|
||||
|
|
@ -419,37 +421,47 @@ let goldenlayout = (function () {
|
|||
}
|
||||
}
|
||||
|
||||
let messageDelivered = false;
|
||||
let components = myLayout.root.getItemsByType("component");
|
||||
|
||||
components.forEach( function (component) {
|
||||
if( component.hasId("inputComponent") ) { return; } // ignore the input component
|
||||
if( component.hasId("inputComponent") ) { return; } // ignore input components
|
||||
|
||||
let textDiv = component.container.getElement().children(".content");
|
||||
let attrTypes = textDiv.attr("types");
|
||||
let destDiv = component.container.getElement().children(".content");
|
||||
let attrTypes = destDiv.attr("types");
|
||||
let paneTypes = attrTypes ? attrTypes.split(" ") : [];
|
||||
let updateMethod = textDiv.attr("updateMethod");
|
||||
let txt = args[0];
|
||||
|
||||
// is this message type listed in this pane"s types (or is this pane catching "all")
|
||||
if( paneTypes.includes(msgtype) || paneTypes.includes("all") ) {
|
||||
routeMsg( textDiv, txt, updateMethod );
|
||||
messageDelivered = true;
|
||||
divArray.push(destDiv);
|
||||
}
|
||||
|
||||
// is this pane catching "upmapped" messages?
|
||||
// And is this message type listed in the untagged types array?
|
||||
if( paneTypes.includes("untagged") && untagged.includes(msgtype) ) {
|
||||
routeMsg( textDiv, txt, updateMethod );
|
||||
messageDelivered = true;
|
||||
divArray.push(destDiv);
|
||||
}
|
||||
});
|
||||
|
||||
if ( messageDelivered ) {
|
||||
return true;
|
||||
}
|
||||
// unhandled message
|
||||
return false;
|
||||
return divArray;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
//
|
||||
var onText = function (args, kwargs) {
|
||||
// are any panes set to receive this text message?
|
||||
var divs = routeMessage(args, kwargs);
|
||||
|
||||
var msgHandled = false;
|
||||
divs.forEach( function (div) {
|
||||
let updateMethod = div.attr("updateMethod");
|
||||
let txt = args[0];
|
||||
|
||||
// yes, so add this text message to the target div
|
||||
routeMsg( div, txt, updateMethod );
|
||||
msgHandled = true;
|
||||
});
|
||||
|
||||
return msgHandled;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -536,6 +548,7 @@ let goldenlayout = (function () {
|
|||
getGL: function () { return myLayout; },
|
||||
addKnownType: addKnownType,
|
||||
onTabCreate: onTabCreate,
|
||||
routeMessage: routeMessage,
|
||||
}
|
||||
}());
|
||||
window.plugin_handler.add("goldenlayout", goldenlayout);
|
||||
|
|
|
|||
|
|
@ -1,53 +1,119 @@
|
|||
/*
|
||||
* Evennia example Webclient multimedia outputs plugin
|
||||
*
|
||||
* Evennia Webclient multimedia outputs plugin
|
||||
* PLUGIN ORDER PREREQS:
|
||||
* loaded after:
|
||||
* webclient_gui.js
|
||||
* option2.js
|
||||
* loaded before:
|
||||
*
|
||||
* in evennia python code:
|
||||
*
|
||||
* To use, in evennia python code:
|
||||
* target.msg( image="URL" )
|
||||
* target.msg( audio="URL" )
|
||||
* target.msg( video="URL" )
|
||||
* or, if you prefer tagged routing:
|
||||
* target.msg( image=("URL",{'type':'tag'}) )
|
||||
*
|
||||
*
|
||||
* Note: users probably don't _want_ more than one pane to end up with multimedia tags...
|
||||
* But to allow proper tagged message routing, this plugin doesn't explicitly deny it.
|
||||
*/
|
||||
let multimedia_plugin = (function () {
|
||||
//
|
||||
var image = function (args, kwargs) {
|
||||
var mwin = $("#messagewindow");
|
||||
mwin.append("<img src='"+ args[0] +"'/>");
|
||||
mwin.scrollTop(mwin[0].scrollHeight);
|
||||
let options = window.options;
|
||||
if( !("mm_image" in options) || options["mm_image"] === false ) { return; }
|
||||
|
||||
var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
|
||||
mwins.forEach( function (mwin) {
|
||||
mwin.append("<img src='"+ args[0] +"'/>");
|
||||
mwin.scrollTop(mwin[0].scrollHeight);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
var audio = function (args, kwargs) {
|
||||
let options = window.options;
|
||||
if( !("mm_audio" in options) || options["mm_audio"] === false ) { return; }
|
||||
|
||||
// create an HTML5 audio control (only .mp3 is fully compatible with all major browsers)
|
||||
var mwin = $("#messagewindow");
|
||||
mwin.append("<audio controls='' autoplay='' style='height:17px;width:175px'>" +
|
||||
"<source src='"+ args[0] +"'/>" +
|
||||
"</audio>");
|
||||
mwin.scrollTop(mwin[0].scrollHeight);
|
||||
var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
|
||||
mwins.forEach( function (mwin) {
|
||||
mwin.append("<audio controls='' autoplay='' style='height:17px;width:175px'>" +
|
||||
"<source src='"+ args[0] +"'/>" +
|
||||
"</audio>");
|
||||
mwin.scrollTop(mwin[0].scrollHeight);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
var video = function (args, kwargs) {
|
||||
let options = window.options;
|
||||
if( !("mm_video" in options) || options["mm_video"] === false ) { return; }
|
||||
|
||||
// create an HTML5 video element (only h264 .mp4 is compatible with all major browsers)
|
||||
var mwin = $("#messagewindow");
|
||||
mwin.append("<video controls='' autoplay=''>" +
|
||||
"<source src='"+ args[0] +"'/>" +
|
||||
"</video>");
|
||||
mwin.scrollTop(mwin[0].scrollHeight);
|
||||
var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
|
||||
mwins.forEach( function (mwin) {
|
||||
mwin.append("<video controls='' autoplay=''>" +
|
||||
"<source src='"+ args[0] +"'/>" +
|
||||
"</video>");
|
||||
mwin.scrollTop(mwin[0].scrollHeight);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
var onOptionsUI = function (parentdiv) {
|
||||
let options = window.options;
|
||||
var checked;
|
||||
|
||||
checked = options["mm_image"] ? "checked='checked'" : "";
|
||||
var mmImage = $( [ "<label>",
|
||||
"<input type='checkbox' data-setting='mm_image' " + checked + "'>",
|
||||
" Enable multimedia image (png/gif/etc) messages",
|
||||
"</label>"
|
||||
].join("") );
|
||||
|
||||
checked = options["mm_audio"] ? "checked='checked'" : "";
|
||||
var mmAudio = $( [ "<label>",
|
||||
"<input type='checkbox' data-setting='mm_audio' " + checked + "'>",
|
||||
" Enable multimedia audio (mp3) messages",
|
||||
"</label>"
|
||||
].join("") );
|
||||
|
||||
checked = options["mm_video"] ? "checked='checked'" : "";
|
||||
var mmVideo = $( [ "<label>",
|
||||
"<input type='checkbox' data-setting='mm_video' " + checked + "'>",
|
||||
" Enable multimedia video (h264 .mp4) messages",
|
||||
"</label>"
|
||||
].join("") );
|
||||
mmImage.on("change", window.plugins["options2"].onOptionCheckboxChanged);
|
||||
mmAudio.on("change", window.plugins["options2"].onOptionCheckboxChanged);
|
||||
mmVideo.on("change", window.plugins["options2"].onOptionCheckboxChanged);
|
||||
|
||||
parentdiv.append(mmImage);
|
||||
parentdiv.append(mmAudio);
|
||||
parentdiv.append(mmVideo);
|
||||
}
|
||||
|
||||
//
|
||||
// Mandatory plugin init function
|
||||
var init = function () {
|
||||
Evennia = window.Evennia;
|
||||
Evennia.emitter.on('image', image); // capture "image" commands
|
||||
Evennia.emitter.on('audio', audio); // capture "audio" commands
|
||||
Evennia.emitter.on('video', video); // capture "video" commands
|
||||
let options = window.options;
|
||||
options["mm_image"] = true;
|
||||
options["mm_audio"] = true;
|
||||
options["mm_video"] = true;
|
||||
|
||||
let Evennia = window.Evennia;
|
||||
Evennia.emitter.on("image", image); // capture "image" commands
|
||||
Evennia.emitter.on("audio", audio); // capture "audio" commands
|
||||
Evennia.emitter.on("video", video); // capture "video" commands
|
||||
console.log('Multimedia plugin initialized');
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
onOptionsUI: onOptionsUI,
|
||||
}
|
||||
})();
|
||||
plugin_handler.add('multimedia_plugin', multimedia_plugin);
|
||||
|
||||
plugin_handler.add("multimedia_plugin", multimedia_plugin);
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ let options2 = (function () {
|
|||
onLoggedIn: onLoggedIn,
|
||||
onOptionsUI: onOptionsUI,
|
||||
onPrompt: onPrompt,
|
||||
onOptionCheckboxChanged: onOptionCheckboxChanged,
|
||||
}
|
||||
})();
|
||||
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 -->
|
||||
<!--
|
||||
<script src="https://unpkg.com/split.js@1.5.9/dist/split.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script>
|
||||
-->
|
||||
<script type="text/javascript" src="https://golden-layout.com/files/latest/js/goldenlayout.min.js"></script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue