Merge branch 'develop' of github.com:evennia/evennia into develop

This commit is contained in:
Griatch 2022-08-28 14:53:54 +02:00
commit 880f5cd659
7 changed files with 472 additions and 14 deletions

View file

@ -2,7 +2,7 @@
Evennia comes with a MUD client accessible from a normal web browser. During development you can try
it at `http://localhost:4001/webclient`. The client consists of several parts, all under
`evennia/web/webclient/`:
`evennia/web`:
`templates/webclient/webclient.html` and `templates/webclient/base.html` are the very simplistic
django html templates describing the webclient layout.
@ -18,7 +18,7 @@ be used also if swapping out the gui front end.
various plugins, and uses the Evennia object library for all in/out.
`static/webclient/js/plugins` provides a default set of plugins that implement a "telnet-like"
interface.
interface, and a couple of example plugins to show how you could implement new plugin features.
`static/webclient/css/webclient.css` is the CSS file for the client; it also defines things like how
to display ANSI/Xterm256 colors etc.
@ -30,17 +30,17 @@ these.
## Customizing the web client
Like was the case for the website, you override the webclient from your game directory. You need to
add/modify a file in the matching directory location within one of the _overrides directories.
These _override directories are NOT directly used by the web server when the game is running, the
server copies everything web related in the Evennia folder over to `mygame/web/static/` and then
copies in all of your _overrides. This can cause some cases were you edit a file, but it doesn't
add/modify a file in the matching directory locations within your project's `mygame/web/` directories.
These directories are NOT directly used by the web server when the game is running, the
server copies everything web related in the Evennia folder over to `mygame/server/.static/` and then
copies in all of your `mygame/web/` files. This can cause some cases were you edit a file, but it doesn't
seem to make any difference in the servers behavior. **Before doing anything else, try shutting
down the game and running `evennia collectstatic` from the command line then start it back up, clear
your browser cache, and see if your edit shows up.**
Example: To change the utilized plugin list, you need to override base.html by copying
`evennia/web/webclient/templates/webclient/base.html` to
`mygame/web/template_overrides/webclient/base.html` and editing it to add your new plugin.
Example: To change the list of in-use plugins, you need to override base.html by copying
`evennia/web/templates/webclient/base.html` to
`mygame/web/templates/webclient/base.html` and editing it to add your new plugin.
# Evennia Web Client API (from evennia.js)
* `Evennia.init( opts )`
@ -96,6 +96,8 @@ manager for drag-n-drop windows, text routing and more.
keys to peruse.
* `hotbuttons.js` Defines onGotOptions. A Disabled-by-default plugin that defines a button bar with
user-assignable commands.
* `html.js` A basic plugin to allow the client to handle "raw html" messages from the server, this
allows the server to send native HTML messages like >div style='s'<styled text>/div<
* `iframe.js` Defines onOptionsUI. A goldenlayout-only plugin to create a restricted browsing sub-
window for a side-by-side web/text interface, mostly an example of how to build new HTML
"components" for goldenlayout.
@ -108,8 +110,50 @@ from the server and display them as inline HTML.
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.
* `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.
* `text2html.js` Provides a new message handler type: `text2html`, similar to the multimedia and html
plugins. This plugin provides a way to offload rendering the regular pipe-styled ASCII messages
to the client. This allows the server to do less work, while also allowing the client a place to
customize this conversion process. To use this plugin you will need to override the current commands
in Evennia, changing any place where a raw text output message is generated and turn it into a
`text2html` message. For example: `target.msg("my text")` becomes: `target.msg(text2html=("my text"))`
(even better, use a webclient pane routing tag: `target.msg(text2html=("my text", {"type": "sometag"}))`)
`text2html` messages should format and behave identically to the server-side generated text2html() output.
# A side note on html messages vrs text2html messages
So...lets say you have a desire to make your webclient output more like standard webpages...
For telnet clients, you could collect a bunch of text lines together, with ASCII formatted borders, etc.
Then send the results to be rendered client-side via the text2html plugin.
But for webclients, you could format a message directly with the html plugin to render the whole thing as an
HTML table, like so:
```
# Server Side Python Code:
if target.is_webclient():
# This can be styled however you like using CSS, just add the CSS file to web/static/webclient/css/...
table = [
"<table>",
"<tr><td>1</td><td>2</td><td>3</td></tr>",
"<tr><td>4</td><td>5</td><td>6</td></tr>",
"</table>"
]
target.msg( html=( "".join(table), {"type": "mytag"}) )
else:
# This will use the client to render this as "plain, simple" ASCII text, the same
# as if it was rendered server-side via the Portal's text2html() functions
table = [
"#############",
"# 1 # 2 # 3 #",
"#############",
"# 4 # 5 # 6 #",
"#############"
]
target.msg( html2html=( "\n".join(table), {"type": "mytag"}) )
```
# Writing your own Plugins
@ -131,7 +175,7 @@ output and the one starting input window. This is done by modifying your server
goldenlayout_default_config.js.
Start by creating a new
`mygame/web/static_overrides/webclient/js/plugins/goldenlayout_default_config.js` file, and adding
`mygame/web/static/webclient/js/plugins/goldenlayout_default_config.js` file, and adding
the following JSON variable:
```
@ -222,7 +266,7 @@ type="text/javascript"></script>
Remember, plugins are load-order dependent, so make sure the new `<script>` tag comes before the
goldenlayout.js
Next, create a new plugin file `mygame/web/static_overrides/webclient/js/plugins/myplugin.js` and
Next, create a new plugin file `mygame/web/static/webclient/js/plugins/myplugin.js` and
edit it.
```

View file

@ -390,7 +390,7 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
"{}|n".format(utils.crop(raw_ansi(item.db.desc or ""), width=50) or ""),
)
string = f"|wYou are carrying:\n{table}"
self.caller.msg(string)
self.caller.msg(text=(string, {"type": "inventory"}))
class CmdGet(COMMAND_DEFAULT_CLASS):

View file

@ -26,7 +26,7 @@ class CharacterCmdset(default_cmds.Character_CmdSet):
```
Then reload to make the bew commands available. Note that they only work
Then reload to make the new commands available. Note that they only work
on rooms with the typeclass `ExtendedRoom`. Create new rooms with the right
typeclass or use the `typeclass` command to swap existing rooms.

View file

@ -21,6 +21,7 @@ __pycache__
# Other
*.swp
*.log
*.log.*
*.pid
*.restart
*.db3

View file

@ -0,0 +1,63 @@
/*
* Evennia server-side-generated "raw" HTML message handler plugin
*
* PLUGIN ORDER PREREQS:
* loaded after:
* webclient_gui.js
* option2.js
* loaded before:
*
*
* To use, at minimum, in evennia python code:
* target.msg( html="<div><span>...etc...</span></div>" )
*
* or, if you prefer tagged routing (RECOMMENDED):
* target.msg( html=("<div><span>...etc...</span></div>",{'type':'tag'}) )
*
*/
let html_plugin = (function () {
//
var html = function (args, kwargs) {
let options = window.options;
if( !("html" in options) || options["html"] === false ) { return; }
var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
mwins.forEach( function (mwin) {
mwin.append(args[0]);
mwin.scrollTop(mwin[0].scrollHeight);
});
}
//
var onOptionsUI = function (parentdiv) {
let options = window.options;
var checked;
checked = options["html"] ? "checked='checked'" : "";
var mmHtml = $( [ "<label>",
"<input type='checkbox' data-setting='html' " + checked + "'>",
" Prefer server-side generated direct-HTML messages over old-school ASCII text",
"</label>"
].join("") );
mmHtml.on("change", window.plugins["options2"].onOptionCheckboxChanged);
parentdiv.append(mmHtml);
}
//
// Mandatory plugin init function
var init = function () {
let options = window.options;
options["html"] = true;
let Evennia = window.Evennia;
Evennia.emitter.on("html", html); // capture "image" commands
console.log('HTML plugin initialized');
}
return {
init: init,
onOptionsUI: onOptionsUI,
}
})();
plugin_handler.add("html_plugin", html_plugin);

View file

@ -0,0 +1,348 @@
/*
*
* Evennia Webclient text2html component
*
* This is used in conjunction with the main evennia.js library, which
* handles all the communication with the Server.
*
*/
//
// Global Parser for text2html
//
var text2html_plugin = (function () {
"use strict"
let asciiESC = String.fromCharCode(27); // string literal for ASCII ESC bytes
let foreground = "color-102"; // state tracker for foreground css
let background = ""; // state tracker for background css
let underline = ""; // state tracker for underlines css
let pipecodes = /^(\|\[?[0-5][0-5][0-5])|(\|\[[0-9][0-9]?m)|(\|\[?=[a-z])|(\|[![]?[unrgybmcwxhRGYBMCWXH_/>*^-])/;
// example ^|000 or ^|[22m or ^|=a or ^|_
let csslookup = {
"|n": "normal",
"|r": "color-009",
"|g": "color-010",
"|y": "color-011",
"|b": "color-012",
"|m": "color-013",
"|c": "color-014",
"|w": "color-015",
"|x": "color-008",
"|R": "color-001",
"|G": "color-002",
"|Y": "color-003",
"|B": "color-004",
"|M": "color-005",
"|C": "color-006",
"|W": "color-007",
"|X": "color-000",
"|[r": "bgcolor-196",
"|[g": "bgcolor-046",
"|[y": "bgcolor-226",
"|[b": "bgcolor-021",
"|[m": "bgcolor-201",
"|[c": "bgcolor-051",
"|[w": "bgcolor-231",
"|[x": "bgcolor-102",
"|[R": "bgcolor-001",
"|[G": "bgcolor-002",
"|[Y": "bgcolor-003",
"|[B": "bgcolor-004",
"|[M": "bgcolor-005",
"|[C": "bgcolor-006",
"|[W": "bgcolor-007",
"|[X": "bgcolor-000",
"|=a": "color-016",
"|=b": "color-232",
"|=c": "color-233",
"|=d": "color-234",
"|=e": "color-235",
"|=f": "color-236",
"|=g": "color-237",
"|=h": "color-238",
"|=i": "color-239",
"|=j": "color-240",
"|=k": "color-241",
"|=l": "color-242",
"|=m": "color-243",
"|=n": "color-244",
"|=o": "color-245",
"|=p": "color-246",
"|=q": "color-247",
"|=r": "color-248",
"|=s": "color-249",
"|=t": "color-250",
"|=u": "color-251",
"|=v": "color-252",
"|=w": "color-253",
"|=x": "color-254",
"|=y": "color-255",
"|=z": "color-231",
"|[=a": "bgcolor-016",
"|[=b": "bgcolor-232",
"|[=c": "bgcolor-233",
"|[=d": "bgcolor-234",
"|[=e": "bgcolor-235",
"|[=f": "bgcolor-236",
"|[=g": "bgcolor-237",
"|[=h": "bgcolor-238",
"|[=i": "bgcolor-239",
"|[=j": "bgcolor-240",
"|[=k": "bgcolor-241",
"|[=l": "bgcolor-242",
"|[=m": "bgcolor-243",
"|[=n": "bgcolor-244",
"|[=o": "bgcolor-245",
"|[=p": "bgcolor-246",
"|[=q": "bgcolor-247",
"|[=r": "bgcolor-248",
"|[=s": "bgcolor-249",
"|[=t": "bgcolor-250",
"|[=u": "bgcolor-251",
"|[=v": "bgcolor-252",
"|[=w": "bgcolor-253",
"|[=x": "bgcolor-254",
"|[=y": "bgcolor-255",
"|[=z": "bgcolor-231",
// not sure what these nexts ones are actually supposed to map to
"|[0m": "normal",
"|[1m": "normal",
"|[22m": "normal",
"|[36m": "color-006",
"|[37m": "color-015",
}
function ascii (l) {
let a = new String(l); // force string
return a.charCodeAt(0);
}
// dumb convert any leading or trailing spaces/tabs to &nbsp; sequences
var handleSpaces = function (text) {
// TODO should probably get smart about replacing spaces inside "normal" text
return text.replace( /\t/g, "&nbsp;&nbsp;&nbsp;&nbsp;").replace( / /g, "&nbsp;");
}
// javascript doesn't have a native sprintf-like function
function zfill(string, size) {
while (string.length < size) string = "0" + string;
return string;
}
// given string starting with a pipecode |xx
// return the css (if any) and text remainder
var pipe2css = function(pipecode) {
let regx = "";
let css = "color-102";
regx = /^(\|\[?[nrgybmcwxhRGYBMCWX])/;
css = pipecode.match( regx );
if( css != null ) {
return csslookup[ css[1] ];
}
regx = /^(\|\[?=[a-z])/;
css = pipecode.match( regx );
if( css != null ) {
return csslookup[ css[1] ];
}
regx = /^(\|\[[0-9][0-9]?m)/;
css = pipecode.match( regx );
if( css != null ) {
return csslookup[ css[1] ];
}
regx = /^(\|n)/;
css = pipecode.match( regx );
if( css != null ) {
return "normal";
}
regx = /^(\|u)/;
css = pipecode.match( regx );
if( css != null ) {
return "underline";
}
regx = /^\|([0-5][0-5][0-5])/;
css = pipecode.match( regx );
if( css != null ) {
return "color-" + zfill( (parseInt(css[1], 6) + 16).toString(), 3);
}
regx = /^\|\[([0-5][0-5][0-5])/;
css = pipecode.match( regx );
if( css != null ) {
return "bgcolor-" + zfill( (parseInt(css[1], 6) + 16).toString(), 3);
}
return css;
}
// convert any HTML sensitive characters to &code; format
var htmlEscape = function (text) {
text = text.replace(/&/g, "&amp;");
text = text.replace(/</g, "&lt;");
text = text.replace(/>/g, "&gt;");
text = text.replace(/"/g, "&quot;");
text = text.replace(/'/g, "&apos;");
text = handleSpaces(text);
return text;
}
// track stateful CSS
var trackCSS = function (css) {
if( (typeof css !== 'string') && ! (css instanceof String) ) {
css = "";
}
if( css.startsWith( "color-" ) ) {
foreground = css;
}
if( css.startsWith( "bgcolor-" ) ) {
background = css;
}
if( css === "underline" ) {
underline = css;
}
if( css === "normal" ) {
foreground = "color-102";
background = "";
underline = "";
}
return foreground + " " + background + " " + underline ;
}
/*
*
*/
var parse2HTML = function (text) {
let html = "";
foreground = "color-102"; // state tracker for foreground css
background = ""; // state tracker for background css
underline = ""; // state tracker for underlines css
// HACK: parse TELNET ASCII byte-by-byte, convert ESC's to |'s -- serverside "raw" bug?
// Bug is further proven out by the fact that |'s don't come through as doubles.
let hack = new RegExp( String.fromCharCode(27) );
if( text.match( hack ) ) {
let chars = text.split(''); // to characters
for( let n=0; n<chars.length; n++ ) {
if( chars[n] === '|' ) {
console.log( 'Got Pipe' );
chars[n] = "&#124;";
}
if( ascii(chars[n]) === 27 ) {
chars[n] = '|';
}
}
text = chars.join(''); // from character strings
}
let strings = text.split( /(\n|\|[\/])/ ); // newline or pipe-slash
for( let x=0; x<strings.length; x++ ) {
// hack double pipes -- convert them temporarily to HTML -- &#124;
var string = strings[x].replace( /\|\|/g, "&#124;" );
// special case for blank lines, avoid <div>'s with no HTML height
if( string === "" ) {
html += "<div>&nbsp;</div>";
continue;
}
html += "<div>";
let spans = string.split( "|" );
// this split means there are 2 possible cases
// possibly-0-length leading span with "default" styling (may be the ONLY span)
let span = spans[0].replaceAll("&#124;", "|"); // avoid htmlEscape mangling literal |'s
if( span.length > 0 ) {
html += "<span class='color-102'>" + htmlEscape(span) + "</span>";
}
// "standard" span array of [xxxtext1, xxtext2, xxtext3], where x's are pipe-codes
for( let n=1; n<spans.length; n++ ) {
span = "|" + spans[n].replaceAll("&#124;", "|"); // avoid htmlEscape mangling |'s
let pipecode = "";
let remainder = span;
let tags = span.match( pipecodes ); // match against large pipecode regex
if( tags != null ) {
pipecode = tags[0]; // matched text is the pipecode
remainder = span.replace( pipecode, "" ); // everything but the pipecode
}
let css = trackCSS( pipe2css( pipecode ) ); // find css associated with pipe-code
html += "<span class='" + css + "'>" + htmlEscape(remainder) + "</span>";
}
html += "</div>";
}
return html;
}
// The Main text2html message capture fuction -- calls parse2HTML()
var text2html = function (args, kwargs) {
let options = window.options;
if( !("text2html" in options) || options["text2html"] === false ) { return; }
var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
mwins.forEach( function (mwin) {
mwin.append( parse2HTML( args[0]) ); // the heavy workload
mwin.scrollTop(mwin[0].scrollHeight);
});
}
//
var onOptionsUI = function (parentdiv) {
let options = window.options;
var checked;
checked = options["text2html"] ? "checked='checked'" : "";
var text2html = $( [ "<label>",
"<input type='checkbox' data-setting='text2html' " + checked + "'>",
" Enable client-side Evennia ASCII message rendering",
"</label>"
].join("") );
text2html.on("change", window.plugins["options2"].onOptionCheckboxChanged);
parentdiv.append(text2html);
}
//
// Mandatory plugin init function
var init = function () {
let options = window.options;
options["text2html"] = true;
let Evennia = window.Evennia;
Evennia.emitter.on("text2html", text2html); // capture "text2html" outfunc events
console.log('Text2Html plugin initialized');
}
return {
init: init,
onOptionsUI: onOptionsUI,
parse2HTML: parse2HTML,
}
})();
plugin_handler.add("text2html_plugin", text2html_plugin);

View file

@ -100,6 +100,8 @@ JQuery available.
<script src={% static "webclient/js/plugins/default_in.js" %} language="javascript" type="text/javascript"></script>
<script src={% static "webclient/js/plugins/default_out.js" %} language="javascript" type="text/javascript"></script>
<script src={% static "webclient/js/plugins/multimedia.js" %} language="javascript" type="text/javascript"></script>
<script src={% static "webclient/js/plugins/html.js" %} language="javascript" type="text/javascript"></script>
<script src={% static "webclient/js/plugins/text2html.js" %} language="javascript" type="text/javascript"></script>
{% endblock %}
<script src="https://cdn.rawgit.com/ejci/favico.js/master/favico-0.3.10.min.js" language="javascript" type="text/javascript" charset="utf-8"></script>