You are viewing a potentially older version of this package. View all versions.
Ydalis-SignStyler-1.0.0 icon

SignStyler

Sign editor: formatting toolbar, color picker and live preview.

Date uploaded 2 weeks ago
Version 1.0.0
Download link Ydalis-SignStyler-1.0.0.zip
Downloads 212
Dependency string Ydalis-SignStyler-1.0.0

This mod requires the following mods to function

denikson-BepInExPack_Valheim-5.4.2351 icon
denikson-BepInExPack_Valheim

BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.

Preferred version: 5.4.2351

README

Sign Styler — a Valheim mod (BepInEx 5)

A rebuilt sign-writing interface that makes formatting usable.

What it fixes

Vanilla problem What the mod does
Since patch 0.217.22 the input field renders rich text as you type: <color=red> vanishes the moment you type the >, and a <size=5> shrinks what you typed past legibility — often leaving Ctrl+A and start over as the only way out Rendering is disabled inside the field, so tags stay visible and editable
50 characters max, tags included — <color=red>Hello</color> <size=100>there</size> already fills the budget for 11 visible characters Limit raised to 400 by default, configurable up to 2000
No visual feedback before committing Live rendered preview under the field, plus a visible/total character counter
Tags typed by hand Toolbar: character styles that report what is active under the caret, a direct entry per color target backed by an HSV picker with palette and history, and sliders for every numeric tag
Icons only reachable by pasting a character from a web page, with no way to know which ones the font has An icon board listing every glyph the sign's font can actually draw

Keyboard shortcuts while typing: Ctrl+B, Ctrl+I, Ctrl+U.

Scope

The mod adds nothing to the game. Valheim's signs already accept TextMeshPro rich text and that system works fine; what it lacks is a usable way to write it. So: drive the existing engine, raise the limit that made styling impractical, put an editor in front of it.

Out of scope, deliberately: dynamic content of any kind (counters, clock, player stats, chest contents, placeholders resolved at display time), new tags or effects such as glow or animated rainbow, new prefabs, sign sizes, recipes or fonts, and any custom save data. A sign shows the text you typed, stored in its normal text field.

That restraint is what buys the rest — see the compatibility section below.

The color picker

The toolbar carries one row per color target — Text colour and Highlight — each showing the color that target currently holds and opening the picker already pointed at it. There is no generic color button any more: the choice between the two used to be made after opening the picker, at the bottom of the panel, which is the wrong end of the interaction.

The picker itself is a panel to the right of the sign dialog:

  • a saturation/value square and a hue strip, textures generated at runtime in 64×64 — no asset bundle to ship
  • a two-way bound hex field, with Copy and Paste against the system clipboard
  • an alpha slider with a percentage readout
  • a palette of saved swatches: + save stores the working color, right-click on a chip removes it
  • a recent row, the session history, fed by every apply
  • an apply button that states what it is about to do — Color selection / Color word at caret, or Highlight … on the other target — because with nothing selected the mod applies the tag to the word under the caret rather than refusing
  • Insert empty tag at caret, which writes the tag pair and parks the caret between the halves, for coloring text you have not typed yet

Clicking a swatch loads the color into the picker, it does not apply it — otherwise every color you looked at was a color you had already written into the sign.

The two entries are read back from the sign's own text every time a panel opens: the last <color> and the last <mark> in the string, falling back to opaque black and a fully transparent highlight on a sign that carries neither. So they describe the sign in front of you rather than the one you edited before it. The palette and the recent row are the opposite — deliberately shared for the whole session, since carrying a color from one sign to the next is the point of having them.

What gets written:

  • text target → <color=#RRGGBB>, or #RRGGBBAA when alpha is below 1
  • highlight target → <mark=#RRGGBB(AA)>, at whatever alpha the slider says. An opaque <mark> used to be forced down to 0.4 on the assumption it would hide the text; on a sign it does not — the board draws the highlight behind the glyphs.

If the selection already sits inside a <color> tag, picking a new one rewrites that value instead of nesting a second pair.

The hex field is a clone of the vanilla input field, so it inherits the norse font and skin. Its listeners are cleared on creation so typing in it never submits the panel.

Character styles

B, I, U, S and no parse light up when the style is in force where the caret sits. That state is read back from the tags in the string on every frame the text or the caret moved, not tracked from clicks — the field lets you edit the tags by hand, and a set of switches that only knew about its own clicks would start lying the moment you did.

B lighting up does not mean the sign will look bold: see the tag table below for what the board actually honours.

The icon board

The icons players put on signs — crossed swords over an armoury, a hammer over a workbench — are ordinary glyphs of the sign's own font, one character each. The icons button opens a grid of them to the left of the panel; clicking one inserts it at the caret.

The grid is read from the font asset at runtime, never from a list in the source. The board walks the sign's TMP_FontAsset, its fallback chain and TextMeshPro's project-wide fallbacks, and keeps the codepoints a keyboard cannot type: symbols, arrows, geometric shapes, dingbats, and the private use area, which is where a game keeps its own icon set. Whole scripts are excluded by range — a CJK fallback would bury the icons under thousands of ideographs. Two consequences worth having: a game update that adds or drops an icon shows up without a code change, and a glyph on the grid is by construction one the sign can draw, so nothing comes out as an empty box.

Search and categories. The font decides how many glyphs there are, and on a rich one that is hundreds; paging through them to find a skull is not a way to find a skull. The search box filters the grid as you type, matching Unicode's own character names — sword finds ⚔, skull finds ☠ — with every term required, so crossed sword finds crossed swords and black star does not drag in every star. Typing a codepoint works too, bare or with the U+ you copied along with it. The category button cycles through the Unicode blocks this font turned out to hold, built from the discovered glyphs rather than from a fixed taxonomy, so it can never land on an empty page. The private use area is offered as Game icons, which is the only label true of it.

The names come from an embedded table generated from UnicodeData.txt and the CLDR short names — 5675 entries, shipped as text in the assembly rather than as a C# literal so it stays diffable and costs its bytes once. It labels glyphs; it never decides which ones exist. A glyph Unicode does not name — the game's own icons, which Unicode leaves unnamed by design — still appears on the grid and is still reachable by codepoint. Regenerate it against a newer Unicode release with python tools/gen-glyph-names.py, which downloads from unicode.org and rewrites data/glyph-names.txt; nothing is downloaded at build time and nothing at runtime.

The caption line under the grid names the last icon inserted and gives its codepoint, which is what you paste into a bug report or type on a machine where the mod is not installed.

No emoji on the grid? Then the sign's font has none, and the board is telling you the truth rather than offering buttons that write empty boxes. The BepInEx console reports the count on every open — Icon board: 412 glyphs in Valheim-AveriaSerifLibre, 9 Unicode blocks. Emoji reach a sign only through a font that can draw them; this mod never ships one, and a <sprite> tag would need an asset bundle.

An icon costs one character against the limit, the same as a letter — unlike a <sprite> tag, which is why the board inserts characters and not tags.

Numeric tags and offsets

Each numeric tag has a button that reveals a row holding the tag's name, an editable value, a slider and a remove button. The key behaviour: rather than stacking a new <voffset> on every click, the mod finds the tag already present before the caret, binds to its value and rewrites that fragment in place while you drag. The preview follows in real time — which is what makes offsets usable, because those values are found by eye, not by typing.

The field is there for the values that are not. A size or a rotation is often a number you already know, and dragging to exactly 150% on a 20 → 400 rail is a game of patience. Type it and press Enter; the slider follows. A decimal comma is accepted, a value outside the tag's range is clamped to it rather than written through, and anything unparseable restores the bound value instead of guessing.

size takes two forms, and they do not mean the same thing. <size=150%> scales whatever size the text already has; <size=500> sets a fixed point size, which is what turns the board's auto-scaling off — the trick behind text floating over a sign. The row follows what you type: a bare number asks for points, a number with % asks for a percentage, and the slider's range switches with it (20 → 400 for percent, 1 → 800 for points). Clicking the size button on a sign that already carries one binds to the form that is written there, so an existing <size=500> is not quietly rewritten as 400%. A fresh tag still starts at 150%.

Every other tag has a single unit, so on those the suffix is optional on input: 0.5 and 0.5em both read as 0.5.

Tag Default range Unit
size 20 → 400, or 1 → 800 %, or none for a fixed point size
voffset -3 → 3 em
line-height 30 → 200 %
cspace -0.3 → 1 em
rotate -90 → 90 degrees
alpha 0 → 255 hex #XX

Ranges live in TagSpec.All (src/NumericTags.cs), one line per tag — adding another one (mspace, margin, indent, width, pos) is a one-liner.

The remove button deletes the bound opening tag and its closing counterpart. Binding picks the last tag of that name before the caret whose scope is still open: with two nested <voffset> on the same line it grabs the nearest one to the left, which may not be the one you meant.

What to know about offsets

Sign text is auto-centered and auto-scaled. A voffset on its own barely moves anything, because the engine reframes around it. The combination that works is a fixed size — which disables scaling — plus an offset or line breaks. That is the trick behind text floating above a sign.

voffset also adjusts line height to accommodate the shift. For a clean move without stray spacing, compensate with line-height. Likewise rotate disturbs character spacing and can make glyphs overlap: cspace is what pulls that back.

Installation

  1. Install BepInEx for Valheim (the denikson pack, currently 5.4.23.x).
  2. Drop SignStyler.dll into Valheim/BepInEx/plugins/SignStyler/.
  3. Launch once — the config file appears at BepInEx/config/fr.anthony.signstyler.cfg.

Client-side only. Sign text lives in the sign's ZDO and is rendered by TextMeshPro, so other players see the styled result without installing anything. Only the author of the sign needs the mod.

Building

What you need

  • .NET SDK 8 or later. On Windows: winget install Microsoft.DotNet.SDK.8. That is the only install required — the project pulls Microsoft.NETFramework.ReferenceAssemblies from NuGet, so there is no need for the .NET Framework Developer Pack or a Visual Studio workload.
  • Valheim installed, for the game DLLs under valheim_Data/Managed.
  • BepInEx installed at least once, for BepInEx.dll and 0Harmony.dll. With a manual install these sit under <game>/BepInEx/core; with Thunderstore Mod Manager, r2modman or Gale they live inside the manager's profile folder instead. The project probes the game folder and the Default profile of all three, and says which one it picked.

No IDE is required. VS Code with the C# extension, or Rider, both handle the project fine.

Build

dotnet build -c Release -p:ValheimDir="C:\Program Files (x86)\Steam\steamapps\common\Valheim"

To stop passing the path every time, copy Directory.Build.props.example to Directory.Build.props and set your own paths there. MSBuild picks it up automatically, and the file is gitignored — do not hardcode personal paths in the csproj, they would be committed to a public repository along with your user name. The first build restores one NuGet package; everything else is referenced straight from your own install, so there is nothing else to download.

When BepInEx comes from a mod manager, pass the profile folder and the project derives core and plugins from it:

dotnet build -c Release -p:ValheimDir="E:\SteamLibrary\steamapps\common\Valheim" -p:ThunderstoreDir="$env:APPDATA\Thunderstore Mod Manager\DataFolder\Valheim\profiles\Default"

To locate the profile: Get-ChildItem "$env:APPDATA\Thunderstore Mod Manager" -Recurse -Filter BepInEx.dll -EA SilentlyContinue | Select-Object -First 3 FullName

-p:BepInExCore="..." still exists as a last resort for an unusual layout.

Hot reload

With ScriptEngine installed, build with -p:HotReload=true and the DLL lands in BepInEx/scripts instead. Press F6 in game to reload it without restarting — the plugin unpatches Harmony and destroys its UI in OnDestroy, which is what makes that safe.

Close and reopen the sign panel after a reload: the toolbar is rebuilt on the next RequestText, not on the reload itself. And keep only one copy of the DLL around — one in plugins and one in scripts means the plugin loads twice.

Deployment

The DeployToValheim target copies the built DLL into the plugins folder sitting next to whichever BepInEx was found — the manager's profile, not the game folder, when you use one. A rebuild is enough, then restart the game.

No assembly publicizer is needed: private members such as TextInput.m_panel are reached through AccessTools.

The game DLLs referenced by the csproj are the usual Unity and TextMeshPro modules, plus assembly_valheim and assembly_utils. One is there for a single line and looks out of place otherwise: UnityEngine.IMGUIModule, for GUIUtility.systemCopyBuffer behind the picker's copy and paste buttons. Nothing in the mod draws IMGUI, and the clipboard is exposed nowhere else.

Configuration

One setting, in BepInEx/config/fr.anthony.signstyler.cfg:

General / CharacterLimit — 400 by default, bounded 50 to 2000. It is read again every time a sign is opened, so a change made through Configuration Manager takes effect without restarting the game.

Everything else drives the game's text engine and is not up for negotiation. Those values are grouped in src/Settings.cs, each read from exactly one place in the code.

Lowering the limit below the length of existing text does not truncate it: the sign keeps its content, but the toolbar refuses any edit that would exceed the new bound.

Tag cheat sheet

<b> <i> <u> <s>          bold / italic / underline / strikethrough
                         note: <b> has no visible effect on a sign — the Norse font
                         ships no bold face, and the fallback weight is null
<color=#FF0>             short hex form, cheapest in characters
<size=150%> <size=24>    percentage of the current size, or a fixed point size — the
                         fixed form is what switches the board's auto-scaling off
<voffset=1em>            vertical shift, enables text floating off the sign
<line-height=80%>        compensates the shift voffset introduces
<cspace=0.2em>           character spacing
<rotate=15>              rotation in degrees, counter-clockwise when positive
<alpha=#80>              opacity, no closing tag
<align=left>             alignment
\n                       line break (2 characters, cheaper than <br>)

Quotes around tag values can be omitted, and so can closing tags — technically invalid, but tolerated, and it saves characters on a tight budget.

Emphasis that actually shows on a board: size, color and character spacing. <b> is a valid tag the font cannot honour, and <i>, <u>, <s> work because TMP derives them geometrically rather than from a font face. A <size=140%> in a lighter tone reads far better than a bold that never renders.

A sign is rendered twice from the same string — the board carries its own TMP component, the hover label is drawn by the HUD with different settings. A tag can therefore apply to one and not the other:

board hover label
<color> applied ignored, always white
<mark> applied, behind the glyphs ignored
<i> <u> <s> applied ignored
<b> ignored (no bold face in the font) ignored
<size>, layout tags applied applied — and they push the piece name and use prompt out of shape, since your text is only the first line of that block

That is vanilla behaviour, not something this mod can reconcile; the two previews under the field show both results side by side, each reproducing what its renderer actually does.

Reproducing a renderer means reading the renderer, not the component. Three places where the two disagree, all found in game and all worth knowing before "fixing" the code that handles them:

  • The board's text widget reports a pale, near-white color. What lands on the plank is solid dark, because TextMeshPro multiplies that vertex color by the material's _FaceColor and the sign's material carries a dark one. The preview does the same multiplication; reading widget.color alone gave a washed-out grey preview of text that is black in game.
  • The board's widget carries overrideColorTags = true, yet the board does honour <color>. The hover label carries false, yet a colored sign still reads plain white when hovered. Both previews are set from the observed result, not from the flag.
  • <mark> is drawn behind the glyphs on a sign and on top of them in a plain UI canvas, which is why the board preview nudges an opaque highlight to translucent — preview only, the stored text keeps exactly what you typed.

Repository layout

SignStyler.csproj              local references to the game's DLLs
pack.ps1                       build plus both publication archives into dist/
manifest.json, icon.png        Thunderstore metadata (256x256 icon)
docs/nexus-description.bbcode  description ready to paste on Nexus
src/                           the mod

Publishing

pack.ps1 produces two archives in dist/:

  • SignStyler-<version>.zip — mirrors the game folder (BepInEx/plugins/SignStyler/). This is what Nexus expects and what Vortex reads as-is.
  • SignStyler-<version>-thunderstore.zip — manifest.json, icon.png, README.md and CHANGELOG.md at the root, the DLL under plugins/.

Before the first release, fill in the name in LICENSE, and the exact BepInEx dependency string in manifest.json — Thunderstore rejects an identifier that does not exist, so copy it from the package page.

On Nexus, set the category, upload a preview image (an in-game screenshot of the panel beats the icon), and flag the mod as client-side only. On Thunderstore, tick the AI Generated category — it is mandatory there, and mods have been pulled for omitting it.

Compatibility with other sign mods

Sign mods fall into three families. Only the first one collides.

Replaces the sign editor — run one or the other. RunicSigns and BetterSigns substitute their own editor for the vanilla text prompt. RunicSigns goes further and makes captions literal: rich text is not executed at all, styling comes from per-sign properties (one color, one alignment, one size for the whole caption, colors snapped to a named palette). It also stores appearance in custom fields, so it has to be installed on the server and on every client at the same version. Different philosophy, same hook — do not run it alongside this mod.

Touches the same input field — redundant. ComfySigns also disables tag rendering in the field and raises the character limit. Nothing breaks, but the last one loaded wins and there is no reason to stack them.

Renders the output — complementary. AdvancedSigns (font, position, size), Glowing Signs (restores the pre-0.214.300 unlit rendering). They act on Sign, this mod acts on TextInput. They read the same vanilla tags this toolbar writes.

Displays dynamic data — unrelated. Signs (jcdcdev), ZenSign, IconSign and similar mods put chest contents or world stats on signs. No shared surface.

The design choice behind all of this: the mod writes nothing but ordinary vanilla sign text into the ZDO. No custom field, no schema, no version handshake. Other players see styled signs without installing anything, the server needs nothing, and uninstalling leaves every sign exactly as it was.

Known fragility

Valheim 1.0 shipped on 9 September 2026. A major release almost always breaks Harmony patches. Before debugging this mod, check that BepInEx and your other mods have caught up with that version.

The mod depends on three points of the game's API, any of which can move on an update:

  1. TextInput.RequestText(TextReceiver, string, int) — patched as a postfix
  2. TextInput.m_panel — read by reflection, falling back to the singleton's own GameObject
  3. The first TMP_InputField found inside that panel, excluding the mod's own clones

All of it is wrapped in try/catch with explicit logging: if the API changes, the plugin complains in the BepInEx console instead of failing to load.

Toolbar placement (above the field), preview placement (below it) and picker placement (to the right, top edge lined up with the toolbar's) are all computed from the panel container's RectTransform after a forced canvas update. The picker is a side panel rather than a block inside the toolbar column precisely because the palette and the history made that column tall enough to run off the top of the screen.

It hangs from its own top edge and is then clamped back inside the canvas, which is why it survives a palette growing by a row. When the panel is taller than the screen the top wins and the palette is what gets cut — the header and the target tabs have to stay reachable. The clamp runs on the frames following an open and whenever the panel's height changes, never on a hidden panel: an inactive RectTransform has no layout to measure.

One limit separates this from a real rich-text editor: the style toggle only recognises an immediate wrapper, so with <b><i>word</i></b> the bold button will not see the <b> through the <i> and will add a second one. That would need a proper tag-tree parser rather than substring comparisons.

The button state is a separate mechanism and does not share that limit: it counts opening and closing tags before the caret, so a style is reported as active whatever it is nested inside.

Prior art

ComfySigns already covers part of this ground: plain-text editing, a limit raised to 999, a configurable font, a draggable panel, visual effects. It has no toolbar and no live preview — that is where this mod adds something. Worth reading as an implementation reference if a game patch breaks something here.

Support

Bug reports and feature requests: GitLab Issues. Pick the Bug template and keep its headings — it asks for the BepInEx log — Valheim/BepInEx/LogOutput.log, or the same path inside the profile folder when using r2modman or Gale. Without it a report is mostly guesswork.

Credits

The code was written by Claude (Anthropic) from my specification, through several rounds of review. Scope, design decisions, review, testing and maintenance are mine.

Disclosed here and flagged AI Generated on Thunderstore, per the platform's rule.

License

MIT.

CHANGELOG

Changelog

This project follows Keep a Changelog and Semantic Versioning.

[Unreleased]

[1.1.0] — 2026-10-05

Added

  • Colour-vision simulation in the picker: a button cycling through normal vision, protanopia, deuteranopia, tritanopia and achromatopsia. The square, the hue strip, the working swatch and every palette chip are drawn as a player with that deficiency sees them (Machado et al. 2009 matrices, applied in linear RGB). Only the display changes — the hex field and the tag written into the sign keep the real colour. The mode lasts for the session.
  • Emoji on the icon board. Valheim's signs fall back through fourteen font assets and one of them is NotoEmoji-Light, so a sign has always been able to draw them; the board simply could not see them. 1337 glyphs from that face, 832 more from NotoSansJP-Thin.

Fixed

  • The icon board was reading the font atlas instead of the font. Valheim's font assets are in TextMeshPro's Dynamic mode, where characterTable holds what has already been rendered rather than what the font contains — so the grid reported the session's history. Observed in game: twenty glyphs, every one an icon the HUD happened to be drawing, on an asset whose table held two entries. The board now asks the font face through FontEngine, which rasterises nothing and leaves the atlas untouched. 2452 glyphs on a vanilla install.
  • Asking the face for coverage was not enough: Font.HasCharacter answers from the character map, which maps codepoints onto glyph 0 — .notdef, the empty box. That filled the grid with 9481 entries, most of them boxes. The board asks for the glyph index and rejects 0.
  • The category label kept naming a block the grid was no longer filtered on, after a sign opened with a different font reset the filter without resetting the caption.

Known

  • The game's own icons, in the private use area, are not found by the font scan — it yields one codepoint there. They still appear through the character-table walk once something has rendered them, which means they come and go with the session. Not chased down yet.

[1.0.0] — 2026-09-28

First release.

Added

  • Plain-text editing in the input field: tags stay visible and deletable instead of being rendered as you type.
  • Character limit raised from 50 to 400, configurable between 50 and 2000.
  • Formatting toolbar: bold, italic, underline, strikethrough, line break, noparse, and a strip-all-tags button. The style buttons light up for whatever is in force under the caret, read back from the tags rather than tracked from clicks, so they stay honest when the string is edited by hand.
  • One direct entry per color target — text color and highlight — each showing the color that target currently holds and opening the picker already pointed at it. Both are read back from the sign's own text every time a panel opens.
  • HSV color picker as a side panel: hex field with clipboard copy and paste, an alpha slider with a percentage readout, a saved palette (+ save, right-click a swatch to remove it), the session history, an apply button that states what it is about to do, and Insert empty tag at caret for coloring text that is not typed yet. Clicking a swatch loads the color rather than applying it.
  • Live sliders for size, voffset, line-height, cspace, rotate and alpha: the existing tag's value is rewritten in place rather than a new tag stacked on top. Each one also takes a typed value, with a clamp to the tag's range and a fallback to the bound value when the input cannot be read.
  • size accepts both of the forms TextMeshPro understands: 150% scales the current size, a bare 500 sets a fixed point size and switches the board's auto-scaling off. The row reads the form from what you type, and binds to the form already written on a sign rather than converting it.
  • Icon board: a grid of every glyph the sign's font can draw that a keyboard cannot type — symbols, arrows, shapes, and whatever the game keeps in the private use area. Built by reading the font asset at runtime rather than from a hardcoded list, so a glyph is only offered when the sign can actually draw it. One click inserts it at the caret, and each icon costs a single character against the limit.
  • Icon board search: a box that filters the grid as you type, matching Unicode's own character names — sword for ⚔, skull for ☠ — or a codepoint, with or without the U+ a player copies along with it. The names ship as an embedded table generated from UnicodeData.txt and the CLDR short names, 5675 of them; it labels the glyphs without having any say in which ones exist, so the grid is still exactly what the font can draw.
  • Icon board categories: a filter cycling through the Unicode blocks the font actually holds — arrows, geometric shapes, dingbats, the game's own icons — built from the discovered glyphs, so it can never land on an empty page. The caption line now names the last glyph inserted alongside its codepoint.
  • Icon board: the glyph cap that stops the font scan was raised from 1024 to 4096, and hitting it is now logged. At 1024 a font holding more icons than that silently lost whichever ones its character table listed last — the emoji planes, on a font walking in codepoint order. The console also reports the glyph and block count on every open.
  • Rendered preview under the field, plus a visible/total character counter. The board preview paints the color the sign's renderer actually produces — the component's color multiplied by the material's face color — rather than the property alone.
  • Text-editor behaviour: word under the caret when nothing is selected, and style buttons that toggle off when the selection is already wrapped.
  • Trailing resets appended on the way into the sign, so an effect left open does not bleed onto the piece name and use prompt under the hover label.