diff --git a/.gitignore b/.gitignore
index adf7a8202c..a91f40817a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,3 +50,4 @@ build_server_xmlconf
/node_modules/
/log/
/build-archive/
+/bin/
diff --git a/app/android/client/build.gradle b/app/android/client/build.gradle
index 6fa80a9a75..407855e0ba 100644
--- a/app/android/client/build.gradle
+++ b/app/android/client/build.gradle
@@ -10,8 +10,8 @@ android {
applicationId namespace
minSdk 21
targetSdk 32
- versionCode 1048005
- versionName "1.48.5"
+ versionCode 1049000
+ versionName "1.49"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
targetSdkVersion 32
minSdkVersion 21
diff --git a/app/android/client/src/main/play/release-notes/en-US/default.txt b/app/android/client/src/main/play/release-notes/en-US/default.txt
index 70b8142595..0a67955601 100644
--- a/app/android/client/src/main/play/release-notes/en-US/default.txt
+++ b/app/android/client/src/main/play/release-notes/en-US/default.txt
@@ -1,3 +1,5 @@
-PUT RELEASE ANNOUNCEMENT LINES HERE
+The outfit merchants display images of their products now. In order not to fall behind, item sellers have polished their shop signs, to highlight item names.
+
+This in turn prompted Gwen at Deniarn Accessories to cut her prices in half. All in all, it is a good time for some late Christmas shopping.
Please see the complete change log for further details: https://arianne-project.org/game/stendhal.html#changes
diff --git a/app/android/client/src/main/res/values/strings.xml b/app/android/client/src/main/res/values/strings.xml
index fe759d152f..4bc5fd3b6d 100644
--- a/app/android/client/src/main/res/values/strings.xml
+++ b/app/android/client/src/main/res/values/strings.xml
@@ -5,6 +5,6 @@
Stendhal
stendhalprod8gps5y99pu
prod
- 1.48.5
+ 1.49
diff --git a/app/neutralinojs/neutralino.config.json b/app/neutralinojs/neutralino.config.json
index 5113267710..e41d306327 100644
--- a/app/neutralinojs/neutralino.config.json
+++ b/app/neutralinojs/neutralino.config.json
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json",
"applicationId": "org.stendhalgame.client",
- "version": "1.48.5",
+ "version": "1.49",
"defaultMode": "window",
"port": 0,
"documentRoot": "/resources/",
diff --git a/bin/runserver.bat b/bin/runserver.bat
index a6d9623195..07f0694868 100644
--- a/bin/runserver.bat
+++ b/bin/runserver.bat
@@ -1,6 +1,6 @@
@echo off
-set STENDHAL_VERSION=1.48.5
+set STENDHAL_VERSION=1.49
set SERVER_JAR=stendhal-server-%STENDHAL_VERSION%.jar
:: change to server directory
diff --git a/bin/runserver.sh b/bin/runserver.sh
index 1a2e475093..c725321791 100755
--- a/bin/runserver.sh
+++ b/bin/runserver.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
-STENDHAL_VERSION="1.48.5"
+STENDHAL_VERSION="1.49"
SERVER_JAR="stendhal-server-${STENDHAL_VERSION}.jar"
# change to server directory
diff --git a/build.ant.properties b/build.ant.properties
index 427f93c1f9..b75035a282 100644
--- a/build.ant.properties
+++ b/build.ant.properties
@@ -92,9 +92,9 @@ version_server = http://arianne.sourceforge.net/stendhal.version
# current version of stendhal
version.old = 1.48
-version = 1.48.5
+version = 1.49
# FIXME: dynamic method to set this property?
-version.android = 1048005
+version.android = 1049000
# javac options
javac.deprecation = true
diff --git a/buildtools/mkdocs/mkdocs.yml b/buildtools/mkdocs/mkdocs.yml
index 1483f53630..c3b158d27a 100644
--- a/buildtools/mkdocs/mkdocs.yml
+++ b/buildtools/mkdocs/mkdocs.yml
@@ -1,4 +1,4 @@
-site_name: Stendhal 1.48.5 Reference Documentation
+site_name: Stendhal 1.49 Reference Documentation
site_description: A fun, friendly, & free multiplayer online adventure game.
copyright: Copyright © 2003-2025 Stendhal
site_url: https://stendhalgame.org/
diff --git a/doc/CHANGES.txt b/doc/CHANGES.txt
index e14942632c..20be3f04f1 100644
--- a/doc/CHANGES.txt
+++ b/doc/CHANGES.txt
@@ -12,7 +12,7 @@ Changelog
*bug fixes*
- fixed invalid head outfit index of Mr Ross
-- fixed silent NPC fixed paths
+- fixed silent NPC paths
- fixed desktop webview client window freezing on Windows when closed
- fixed nativehelper extension process not exiting cleanly on Windows
- set User-Agent string for Wikipedia API requests
diff --git a/src/games/stendhal/client/update/HttpClient.java b/src/games/stendhal/client/update/HttpClient.java
index f4143f3956..e065295cb6 100644
--- a/src/games/stendhal/client/update/HttpClient.java
+++ b/src/games/stendhal/client/update/HttpClient.java
@@ -118,7 +118,7 @@ public class HttpClient {
try {
connection = (HttpURLConnection) url.openConnection();
// Wikipedia API now requires User-Agent string
- connection.setRequestProperty("User-Agent", "StendhalClient/1.48.5 (https://arianne-project.org/contact.html)");
+ connection.setRequestProperty("User-Agent", "StendhalClient/1.49 (https://arianne-project.org/contact.html)");
connection.setConnectTimeout(myTimeout);
connection.setInstanceFollowRedirects(true);
connection.setUseCaches(false);
diff --git a/src/games/stendhal/common/Debug.java b/src/games/stendhal/common/Debug.java
index 9ff2b7ed8f..6b4849bb7d 100644
--- a/src/games/stendhal/common/Debug.java
+++ b/src/games/stendhal/common/Debug.java
@@ -26,7 +26,7 @@ public class Debug {
/** version. */
// Note: This line is updated by build.xml using a regexp so be sure to adjust it in case you modify this line.
- public static final String VERSION = "1.48.5";
+ public static final String VERSION = "1.49";
/** pre release suffix */
// Note: This line is updated by build.xml using a regexp so be sure to adjust it in case you modify this line.
diff --git a/src/js/css/main.css b/src/js/css/main.css
index 86d9b999e4..91de30c7e9 100644
--- a/src/js/css/main.css
+++ b/src/js/css/main.css
@@ -708,3 +708,16 @@ img#click-indicator {
.pad-contents {
padding: 0.5em;
}
+
+table.shoptable {
+ border-collapse: collapse;
+}
+
+table.shoptable th, table.shoptable td {
+ border: 1px solid var(--text-color);
+ padding: 2px;
+}
+
+td.number {
+ text-align: right;
+}
diff --git a/src/js/stendhal/EventRegistry.ts b/src/js/stendhal/EventRegistry.ts
index f4b2448c7b..6a92619cc1 100644
--- a/src/js/stendhal/EventRegistry.ts
+++ b/src/js/stendhal/EventRegistry.ts
@@ -24,14 +24,11 @@ import { PlayerLoggedOnEvent } from "./event/PlayerLoggedOnEvent";
import { PlayerLoggedOutEvent } from "./event/PlayerLoggedOutEvent";
import { ProgressStatusEvent } from "./event/ProgressStatusEvent";
import { RPEvent } from "./event/RPEvent";
+import { ShowItemListEvent } from "./event/ShowItemListEvent";
+import { ShowOutfitListEvent } from "./event/ShowOutfitListEvent";
import { SoundEvent } from "./event/SoundEvent";
import { TradeEvent } from "./event/TradeEvent";
import { ViewChangeEvent } from "./event/ViewChangeEvent";
-
-import { ui } from "./ui/UI";
-
-import { DialogContentComponent } from "./ui/toolkit/DialogContentComponent";
-
import { Chat } from "./util/Chat";
@@ -155,120 +152,8 @@ export class EventRegistry {
}
}); // reached_achievement
- this.register("show_item_list", {
- execute: function(rpobject: RPObject) {
- let title = "Items";
- let caption = "";
- let items = [];
-
- if (this.hasOwnProperty("title")) {
- title = this["title"];
- }
- if (this.hasOwnProperty("caption")) {
- caption = this["caption"];
- }
- if (this.hasOwnProperty("content")) {
- for (var obj in this["content"]) {
- if (this["content"].hasOwnProperty(obj)) {
- var slotObj = this["content"][obj];
- var data = this["content"][obj]["a"];
- const i = {
- clazz: data["class"],
- subclass: data["subclass"],
- img: data["class"] + "/" + data["subclass"] + ".png",
- price: data["price"],
- desc: data["description_info"]
- }
-
- // seller shops prefix prices with "-"
- if (i.price.startsWith("-")) {
- i.price = i.price.substr(1);
- }
- items.push(i);
- }
- }
- }
-
- const content = new class extends DialogContentComponent {} ("empty-div-template");
- content.componentElement.classList.add("shopsign");
- const captionElement = document.createElement("div");
- captionElement.className = "horizontalgroup shopcaption";
- captionElement.textContent = caption + "\nItem\t-\tPrice\t-\tDescription";
- content.componentElement.appendChild(captionElement);
- const itemList = document.createElement("div");
- itemList.className = "shoplist";
- content.componentElement.appendChild(itemList);
-
- // TODO: organize in columns & show item sprites
- for (const i of items) {
- const row = document.createElement("div");
- row.className = "horizontalgroup shoprow";
- const img = document.createElement("div");
- img.className = "shopcol";
- img.appendChild(stendhal.data.sprites.get(stendhal.paths.sprites + "/items/" + i.img));
- row.appendChild(img);
- const price = document.createElement("div");
- price.className = "shopcol";
- price.textContent = i.price;
- row.appendChild(price);
- const desc = document.createElement("div");
- desc.className = "shopcol shopcolr";
- desc.textContent = i.desc;
- row.appendChild(desc);
- itemList.appendChild(row);
- }
-
- stendhal.ui.globalInternalWindow.set(
- ui.createSingletonFloatingWindow(title, content, 20, 20));
- }
- }); // show_item_list
-
- this.register("show_outfit_list", {
- execute: function(rpobject: RPObject) {
- let title = "Outfits";
- let caption = "";
- let outfits = [];
-
- if (this.hasOwnProperty("title")) {
- title = this["title"];
- }
- if (this.hasOwnProperty("caption")) {
- caption = this["caption"];
- }
- if (this.hasOwnProperty("outfits")) {
- for (let o of this["outfits"].split(":")) {
- o = o.split(";");
- if (o.length > 2) {
- outfits.push([o[0], o[1], o[2]]);
- }
- }
- }
- if (this.hasOwnProperty("show_base")) {
- //Chat.log("normal", this["show_base"]);
- }
-
- const content = new class extends DialogContentComponent {} ("empty-div-template");
- content.componentElement.classList.add("shopsign");
- const captionElement = document.createElement("div");
- captionElement.className = "horizontalgroup shopcaption";
- captionElement.textContent = caption;
- content.componentElement.appendChild(captionElement);
- const itemList = document.createElement("div");
- itemList.className = "shoplist";
- content.componentElement.appendChild(itemList);
-
- // TODO: organize in columns & show outfit sprites
- for (const o of outfits) {
- const row = document.createElement("div");
- row.className = "horizontalgroup shoprow";
- row.textContent = o[0] + ": " + o[2];
- itemList.appendChild(row);
- }
-
- stendhal.ui.globalInternalWindow.set(
- ui.createSingletonFloatingWindow(title, content, 20, 20));
- }
- }); // show_outfit_list
+ this.register("show_item_list", new ShowItemListEvent());
+ this.register("show_outfit_list", new ShowOutfitListEvent());
this.register("sound_event", new SoundEvent());
diff --git a/src/js/stendhal/event/ShowItemListEvent.ts b/src/js/stendhal/event/ShowItemListEvent.ts
new file mode 100644
index 0000000000..7b3c1f26d1
--- /dev/null
+++ b/src/js/stendhal/event/ShowItemListEvent.ts
@@ -0,0 +1,122 @@
+/***************************************************************************
+ * Copyright © 2024 - 2025 Faiumoni e. V. *
+ ***************************************************************************
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU Affero General Public License as *
+ * published by the Free Software Foundation; either version 3 of the *
+ * License, or (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+import { RPEvent } from "./RPEvent";
+
+import { ui } from "../ui/UI";
+import { DialogContentComponent } from "../ui/toolkit/DialogContentComponent";
+import { Chat } from "../util/Chat";
+
+declare var stendhal: any;
+
+
+export class ShowItemListEvent extends RPEvent {
+
+ title!: string;
+ caption!: string;
+ content!: any;
+
+ execute(_entity: any) {
+ let title = "Items";
+ let caption = "";
+ let items = [];
+
+ if (this.hasOwnProperty("title")) {
+ title = this["title"];
+ }
+ if (this.hasOwnProperty("caption")) {
+ caption = this["caption"];
+ }
+ if (this.hasOwnProperty("content")) {
+ for (let obj in this["content"]) {
+ if (this["content"].hasOwnProperty(obj)) {
+ let data = this["content"][obj]["a"];
+ let item = {
+ clazz: data["class"],
+ subclass: data["subclass"],
+ img: data["class"] + "/" + data["subclass"] + ".png",
+ price: data["price"],
+ desc: data["description_info"]
+ }
+
+ // seller shops prefix prices with "-"
+ if (item.price.startsWith("-")) {
+ item.price = item.price.substr(1);
+ }
+ items.push(item);
+ }
+ }
+ }
+
+ const content = new class extends DialogContentComponent { }("empty-div-template");
+ content.componentElement.classList.add("shopsign");
+ // Create table
+ const table = document.createElement("table");
+ table.className = "shoptable";
+ content.componentElement.appendChild(table);
+
+ // Caption
+ const tableCaption = document.createElement("caption");
+ tableCaption.className = "shopcaption";
+ tableCaption.textContent = caption;
+ table.appendChild(tableCaption);
+
+ // Header
+ const thead = document.createElement("thead");
+ const headerRow = document.createElement("tr");
+
+ const headers = ["Item", "Price", "Description"];
+ for (const text of headers) {
+ const th = document.createElement("th");
+ th.textContent = text;
+ headerRow.appendChild(th);
+ }
+
+ thead.appendChild(headerRow);
+ table.appendChild(thead);
+
+ // Body
+ const tbody = document.createElement("tbody");
+
+ for (const i of items) {
+ const row = document.createElement("tr");
+
+ // Item (image)
+ const itemCell = document.createElement("td");
+ itemCell.appendChild(
+ stendhal.data.sprites.get(
+ stendhal.paths.sprites + "/items/" + i.img
+ )
+ );
+ row.appendChild(itemCell);
+
+ // Price
+ const priceCell = document.createElement("td");
+ priceCell.className = "number";
+ priceCell.textContent = i.price;
+ row.appendChild(priceCell);
+
+ // Description
+ const descCell = document.createElement("td");
+ descCell.innerHTML = Chat.formatLogEntry(i.desc);
+ row.appendChild(descCell);
+
+ tbody.appendChild(row);
+ }
+
+ table.appendChild(tbody);
+
+ stendhal.ui.globalInternalWindow.set(
+ ui.createSingletonFloatingWindow(title, content, 20, 20));
+ }
+
+}
diff --git a/src/js/stendhal/event/ShowOutfitListEvent.ts b/src/js/stendhal/event/ShowOutfitListEvent.ts
new file mode 100644
index 0000000000..dbaf651b4f
--- /dev/null
+++ b/src/js/stendhal/event/ShowOutfitListEvent.ts
@@ -0,0 +1,146 @@
+/***************************************************************************
+ * Copyright © 2024 - 2025 Faiumoni e. V. *
+ ***************************************************************************
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU Affero General Public License as *
+ * published by the Free Software Foundation; either version 3 of the *
+ * License, or (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+import { RPEvent } from "./RPEvent";
+
+import { ui } from "../ui/UI";
+import { DialogContentComponent } from "../ui/toolkit/DialogContentComponent";
+
+declare var stendhal: any;
+
+
+export class ShowOutfitListEvent extends RPEvent {
+
+ title!: string;
+ caption!: string;
+ outfits!: string;
+
+ execute(_entity: any) {
+ let title = "Outfits";
+ let caption = "";
+ let outfits = [];
+
+ if (this.hasOwnProperty("title")) {
+ title = this["title"];
+ }
+ if (this.hasOwnProperty("caption")) {
+ caption = this["caption"];
+ }
+ if (this.hasOwnProperty("outfits")) {
+ for (let o of this["outfits"].split(":")) {
+ let parts = o.split(";");
+ if (parts.length > 2) {
+ outfits.push({
+ name: parts[0],
+ outfit: parts[1],
+ price: parts[2]
+ });
+ }
+ }
+ }
+ if (this.hasOwnProperty("show_base")) {
+ //Chat.log("normal", this["show_base"]);
+ }
+
+ const content = new class extends DialogContentComponent { }("empty-div-template");
+ content.componentElement.classList.add("shopsign");
+
+ const table = document.createElement("table");
+ table.className = "shoptable";
+ content.componentElement.appendChild(table);
+
+ // Caption
+ const tableCaption = document.createElement("caption");
+ tableCaption.className = "shopcaption";
+ tableCaption.textContent = caption;
+ table.appendChild(tableCaption);
+
+ // Header
+ const thead = document.createElement("thead");
+ const headerRow = document.createElement("tr");
+
+ const headers = ["Name", "Outfit", "Price"];
+ for (const text of headers) {
+ const th = document.createElement("th");
+ th.textContent = text;
+ headerRow.appendChild(th);
+ }
+
+ thead.appendChild(headerRow);
+ table.appendChild(thead);
+
+ // Body
+ const tbody = document.createElement("tbody");
+
+ for (const outfit of outfits) {
+ const row = document.createElement("tr");
+
+ // Name
+ const nameCell = document.createElement("td");
+ nameCell.textContent = outfit.name;
+ row.appendChild(nameCell);
+
+ // Image
+ const imageCell = document.createElement("td");
+ let url = ShowOutfitListEvent.outfitToUrl(outfit.outfit);
+ const img1 = document.createElement("img");
+ const img2 = document.createElement("img");
+ img1.src = url;
+ img2.src = url + "&offset=0";
+ imageCell.append(img1);
+ imageCell.append(img2);
+ row.appendChild(imageCell);
+
+ // Price
+ const priceCell = document.createElement("td");
+ priceCell.className = "number";
+ priceCell.textContent = outfit.price;
+ row.appendChild(priceCell);
+
+ tbody.appendChild(row);
+ }
+
+ table.appendChild(tbody);
+
+ stendhal.ui.globalInternalWindow.set(
+ ui.createSingletonFloatingWindow(title, content, 20, 20));
+ }
+
+ private static outfitToUrl(outfit?: string): string {
+ if (!outfit) {
+ return "";
+ }
+
+ // Required order
+ const order = ["body", "dress", "head", "mouth", "eyes", "mask", "hair", "hat", "detail"];
+
+ // Parse input into a map
+ let values: Record = {};
+
+ for (let part of outfit.split(",")) {
+ let [key, value] = part.split("=");
+ values[key] = Number(value);
+ }
+
+ // Build path
+ let parts = order.map(key => {
+ let value = values[key] ?? 0;
+ return `${key}-${value}-0`;
+ });
+
+ let url =
+ "https://stendhalgame.org/createoutfit.php?rewritten=true&dialog=shop&outfit=" +
+ parts.join("_");
+
+ return url;
+ }
+}
diff --git a/src/js/stendhal/ui/component/ChatLogComponent.ts b/src/js/stendhal/ui/component/ChatLogComponent.ts
index d562354df6..d69e6d9640 100644
--- a/src/js/stendhal/ui/component/ChatLogComponent.ts
+++ b/src/js/stendhal/ui/component/ChatLogComponent.ts
@@ -14,6 +14,7 @@ import { Component } from "../toolkit/Component";
import { MenuItem } from "../../action/MenuItem";
import { UIComponentEnum } from "../UIComponentEnum";
import { singletons } from "../../SingletonRepo";
+import { Chat } from "../../util/Chat";
declare var stendhal: any;
@@ -106,13 +107,13 @@ export class ChatLogComponent extends Component {
/**
* Adds a line of text.
*
- * @param type {string}
+ * @param type
* Message type.
- * @param message {string}
+ * @param message
* Text to be added.
- * @param orator {string}
+ * @param orator
* Name of entity making the expression (default: `undefined`).
- * @param timestamp {boolean}
+ * @param timestamp
* If `false`, suppresses prepending message with timestamp (default: `true`).
*/
public addLine(type: string, message: string, orator?: string, timestamp=true) {
@@ -131,7 +132,7 @@ export class ChatLogComponent extends Component {
const rcol = document.createElement("div");
rcol.className = "logcolR log" + type;
- rcol.innerHTML += this.formatLogEntry(message);
+ rcol.innerHTML += Chat.formatLogEntry(message);
const row = document.createElement("div");
row.className = "logrow";
@@ -192,122 +193,7 @@ export class ChatLogComponent extends Component {
this.add(row);
}
- /**
- * Formats displayed text.
- *
- * @param message {string}
- * Text to parse for special characters.
- * @return {string}
- * Text formatted with keywords & item names highlighted.
- */
- private formatLogEntry(message: string): string {
- if (!message) {
- return "";
- }
- let res = "";
- let delims = [" ", ",", ".", "!", "?", ":", ";"];
- let length = message.length;
- let inHighlight = false, inUnderline = false,
- inHighlightQuote = false, inUnderlineQuote = false;
- for (let i = 0; i < length; i++) {
- let c = message[i];
- if (c === "\\") {
- let n = message[i + 1];
- res += n;
- i++;
-
- // Highlight Start?
- } else if (c === "#") {
- if (inHighlight) {
- res += c;
- continue;
- }
- let n = message[i + 1];
- if (n === "#") {
- res += c;
- i++;
- continue;
- }
- if (n === "'") {
- inHighlightQuote = true;
- i++;
- }
- inHighlight = true;
- res += "";
-
- // Underline start?
- } else if (c === "§") {
- if (inUnderline) {
- res += c;
- continue;
- }
- let n = message[i + 1];
- if (n === "§") {
- res += c;
- i++;
- continue;
- }
- if (n === "'") {
- inUnderlineQuote = true;
- i++;
- }
- inUnderline = true;
- res += "";
-
- // End Highlight and Underline?
- } else if (c === "'") {
- if (inUnderlineQuote) {
- inUnderline = false;
- inUnderlineQuote = false;
- res += "";
- continue;
- }
- if (inHighlightQuote) {
- inHighlight = false;
- inHighlightQuote = false;
- res += "";
- continue;
- }
- res += c;
-
- // HTML escape
- } else if (c === "<") {
- res += "<";
-
- // End of word
- } else if (delims.indexOf(c) > -1) {
- let n = message[i + 1];
- if (c === " " || n === " " || n == undefined) {
- if (inUnderline && !inUnderlineQuote && !inHighlightQuote) {
- inUnderline = false;
- res += "" + c;
- continue;
- }
- if (inHighlight && !inUnderlineQuote && !inHighlightQuote) {
- inHighlight = false;
- res += "" + c;
- continue;
- }
- }
- res += c;
-
- // Normal characters
- } else {
- res += c;
- }
- }
-
- // Close opened formattings
- if (inUnderline) {
- res += "";
- }
- if (inHighlight) {
- res += "";
- }
-
- return res;
- }
/**
* Removes all text from log.
diff --git a/src/js/stendhal/util/Chat.ts b/src/js/stendhal/util/Chat.ts
index 7542391971..8bda707e37 100644
--- a/src/js/stendhal/util/Chat.ts
+++ b/src/js/stendhal/util/Chat.ts
@@ -99,4 +99,121 @@ export class Chat {
Chat.log("client", message);
}
}
+
+ /**
+ * Formats displayed text.
+ *
+ * @param message {string}
+ * Text to parse for special characters.
+ * @return {string}
+ * Text formatted with keywords & item names highlighted.
+ */
+ public static formatLogEntry(message: string): string {
+ if (!message) {
+ return "";
+ }
+ let res = "";
+ let delims = [" ", ",", ".", "!", "?", ":", ";"];
+ let length = message.length;
+ let inHighlight = false, inUnderline = false,
+ inHighlightQuote = false, inUnderlineQuote = false;
+ for (let i = 0; i < length; i++) {
+ let c = message[i];
+
+ if (c === "\\") {
+ let n = message[i + 1];
+ res += n;
+ i++;
+
+ // Highlight Start?
+ } else if (c === "#") {
+ if (inHighlight) {
+ res += c;
+ continue;
+ }
+ let n = message[i + 1];
+ if (n === "#") {
+ res += c;
+ i++;
+ continue;
+ }
+ if (n === "'") {
+ inHighlightQuote = true;
+ i++;
+ }
+ inHighlight = true;
+ res += "";
+
+ // Underline start?
+ } else if (c === "§") {
+ if (inUnderline) {
+ res += c;
+ continue;
+ }
+ let n = message[i + 1];
+ if (n === "§") {
+ res += c;
+ i++;
+ continue;
+ }
+ if (n === "'") {
+ inUnderlineQuote = true;
+ i++;
+ }
+ inUnderline = true;
+ res += "";
+
+ // End Highlight and Underline?
+ } else if (c === "'") {
+ if (inUnderlineQuote) {
+ inUnderline = false;
+ inUnderlineQuote = false;
+ res += "";
+ continue;
+ }
+ if (inHighlightQuote) {
+ inHighlight = false;
+ inHighlightQuote = false;
+ res += "";
+ continue;
+ }
+ res += c;
+
+ // HTML escape
+ } else if (c === "<") {
+ res += "<";
+
+ // End of word
+ } else if (delims.indexOf(c) > -1) {
+ let n = message[i + 1];
+ if (c === " " || n === " " || n == undefined) {
+ if (inUnderline && !inUnderlineQuote && !inHighlightQuote) {
+ inUnderline = false;
+ res += "" + c;
+ continue;
+ }
+ if (inHighlight && !inUnderlineQuote && !inHighlightQuote) {
+ inHighlight = false;
+ res += "" + c;
+ continue;
+ }
+ }
+ res += c;
+
+ // Normal characters
+ } else {
+ res += c;
+ }
+ }
+
+ // Close opened formattings
+ if (inUnderline) {
+ res += "";
+ }
+ if (inHighlight) {
+ res += "";
+ }
+
+ return res;
+ }
}