From a1427fdb8411cc742d94107e1b9289f76630bbf0 Mon Sep 17 00:00:00 2001 From: Brenden Tuck Date: Thu, 4 Aug 2022 10:22:38 -0400 Subject: [PATCH 1/6] Add inventory tagging --- evennia/commands/default/general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 98ad671e7a..d3eb913a71 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -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): From 922e75ebb0ea84ff5ad550e92c232eb7a42bea20 Mon Sep 17 00:00:00 2001 From: Brenden Tuck Date: Sat, 13 Aug 2022 21:11:46 -0400 Subject: [PATCH 2/6] Add a simple raw HTML message renderer --- .../web/static/webclient/js/plugins/html.js | 63 ++++ .../static/webclient/js/plugins/text2html.js | 348 ++++++++++++++++++ evennia/web/templates/webclient/base.html | 2 + 3 files changed, 413 insertions(+) create mode 100644 evennia/web/static/webclient/js/plugins/html.js create mode 100644 evennia/web/static/webclient/js/plugins/text2html.js diff --git a/evennia/web/static/webclient/js/plugins/html.js b/evennia/web/static/webclient/js/plugins/html.js new file mode 100644 index 0000000000..3ae2666cbf --- /dev/null +++ b/evennia/web/static/webclient/js/plugins/html.js @@ -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="
...etc...
" ) + * + * or, if you prefer tagged routing (RECOMMENDED): + * target.msg( html=("
...etc...
",{'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 = $( [ "" + ].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); diff --git a/evennia/web/static/webclient/js/plugins/text2html.js b/evennia/web/static/webclient/js/plugins/text2html.js new file mode 100644 index 0000000000..f0593b1d46 --- /dev/null +++ b/evennia/web/static/webclient/js/plugins/text2html.js @@ -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   sequences + var handleSpaces = function (text) { + // TODO should probably get smart about replacing spaces inside "normal" text + return text.replace( /\t/g, "    ").replace( / /g, " "); + } + + + // 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, "&"); + text = text.replace(//g, ">"); + text = text.replace(/"/g, """); + text = text.replace(/'/g, "'"); + 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's with no HTML height + if( string === "" ) { + html += "
 
"; + continue; + } + + html += "
"; + + 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("|", "|"); // avoid htmlEscape mangling literal |'s + if( span.length > 0 ) { + html += "" + htmlEscape(span) + ""; + } + + // "standard" span array of [xxxtext1, xxtext2, xxtext3], where x's are pipe-codes + for( let n=1; n" + htmlEscape(remainder) + ""; + } + + html += "
"; + } + + 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 = $( [ "" + ].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); diff --git a/evennia/web/templates/webclient/base.html b/evennia/web/templates/webclient/base.html index 96af22340c..ca8460c242 100644 --- a/evennia/web/templates/webclient/base.html +++ b/evennia/web/templates/webclient/base.html @@ -100,6 +100,8 @@ JQuery available. + + {% endblock %} From f6dc910f1edb80acf8e34b9fb6bd1a6d6791428f Mon Sep 17 00:00:00 2001 From: Brenden Tuck Date: Wed, 17 Aug 2022 18:58:41 -0400 Subject: [PATCH 3/6] Fix the webclient pathing and include new html/text2html docs --- docs/source/Components/Webclient.md | 68 ++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/docs/source/Components/Webclient.md b/docs/source/Components/Webclient.md index da2e3a5506..ea24a81972 100644 --- a/docs/source/Components/Webclient.md +++ b/docs/source/Components/Webclient.md @@ -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 = [ + "", + "", + "", + "
123
456
" + ] + 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"> Remember, plugins are load-order dependent, so make sure the new `