Resource Projection Tooltips
Resource Projection Tooltips
About
Resource Projection Tooltips is a UI mod for Europa Universalis V, published on the Steam Workshop. EU5's resource tooltips hand you a value, a cap and a full breakdown of every modifier feeding the monthly change, and then stop right before the useful part: how long that resource actually lasts. The mod adds that one line to seven resources and to the Economy panel, in whichever direction matters for each one. Gold, legitimacy, prestige and stability warn you while they drain, manpower and sailors tell you when they'll be full again, and war exhaustion warns you while it climbs. It started as something I wanted for my own campaign and grew into a full piece of UI work. All of it is written in Paradox's Jomini GUI scripting language and their executable localization layer, with no engine access at all: 139 lines inserted into two vanilla files, zero vanilla lines modified, and zero vanilla localization keys shadowed.
Project Info
- Role: Solo Developer
- Team Size: 1
- Time frame: August 2026, a couple of days
- Engine: Clausewitz Engine, Jomini GUI - Europa Universalis V 1.3.11
Acerca de
Resource Projection Tooltips es un mod de UI para Europa Universalis V, publicado en el Steam Workshop. Los tooltips de recursos de EU5 te dan un valor, un tope y el desglose completo de cada modificador que alimenta el cambio mensual, y se paran justo antes de la parte útil: cuánto dura de verdad ese recurso. El mod añade esa línea a siete recursos y al panel de Economía, en la dirección que importa en cada caso. El oro, la legitimidad, el prestigio y la estabilidad avisan mientras se agotan, los soldados y los marineros te dicen cuándo volverán a estar llenos, y el desgaste de guerra avisa mientras sube. Empezó siendo algo que quería para mi propia partida y acabó convirtiéndose en una pieza de UI completa. Está escrito entero en el lenguaje de scripting GUI Jomini de Paradox y en su capa de localización ejecutable, sin ningún acceso al motor: 139 líneas insertadas en dos archivos vanilla, cero líneas vanilla modificadas y cero claves de localización vanilla pisadas.
Información
- Rol: Solo Developer
- Equipo: 1
- Duración: Agosto de 2026, un par de días
- Motor: Clausewitz Engine, Jomini GUI - Europa Universalis V 1.3.11
Subscribe on the Steam Workshop!
About the project
This started as a mod for me, not for anyone else.
EU5's gold tooltip tells you exactly how much money you have and exactly how much you're bleeding every month, and then it stops. It has both halves of the sum and it won't do the sum. I was doing that division in my head every few minutes, and doing it badly, so I wrote the line the tooltip wouldn't print.
It took an evening. Then, once it worked for gold, it was pretty obvious the same line was missing everywhere else — manpower, sailors, stability, war exhaustion — and that the interesting part wasn't the arithmetic, it was everything the game does to stop you writing it. So I kept going, and it turned into a proper piece of UI work: seven resources, the Economy panel, all of it in Paradox's own GUI scripting language, without editing a single line of vanilla.
Everything above and below the red box is vanilla. The mod adds the conclusion.
Localization is a programming language
That was the first surprise. Here's the vanilla gold tooltip, verbatim:
GOLD_RESOURCE_TT_TEXT: "We have [Player.GetCurrencyValue('gold')|V]@gold! and it changes monthly by [InGameTopbar.GetPlayerCurrencyChangeFormatted('gold')]@gold!."
That isn't a string with holes in it. The [...] are live function calls into the game, |V is a formatting flag and @gold! splices in a sprite. And they do maths.
Which means the entire first version of this mod lived inside a .yml file. Better still, vanilla already ships a runway calculation, for employment, so I didn't have to invent the idiom, just copy it:
[Divide_CFixedPoint(Multiply_CFixedPoint(<value>, '(CFixedPoint)-1'), <rate>)|0V] months
The formula is treasury ÷ −(monthly change). That × −1 is what turns a negative rate into a positive duration, and that sign convention ended up being the guard for the whole mod: if the sign doesn't work out, the line doesn't draw.
Nice detail: constants are written as quoted casts ('(CFixedPoint)-1'), and CFixedPoint exists because floats aren't bit-identical across CPUs, which would desync lockstep multiplayer.
Direction belongs to the resource, not the widget
With gold done, the second direction was obvious: (max − value) ÷ change for resources filling up. Manpower and sailors use that one, and read "fully replenished in".
This is where the mod almost stayed permanently gold-shaped. For every resource but one, filling up is good news. For war exhaustion, going up is the danger, so it's calculated like manpower and painted red like a warning. That single exception is what forced polarity to be a property of each resource rather than a decision baked into the widget.
Stability had its own trap: it goes below zero (hud_topbar.gui:622,633 test < 0 and < -50), so zero isn't a floor to hit, it's a threshold to cross. That's why it reads "turns negative" rather than "exhausted".
Same line, opposite direction: manpower is filling, so the projection is a delivery date rather than a warning.
Truncated, and a 50-year ceiling
Two decisions about units, both settled by looking at what the simulation actually does rather than what read nicely:
Truncate, don't round. 97.79 displays as 97. You have 97 complete months; the 98th is partial. A warning that rounds up is telling you that you have longer than you do.
A 50-year ceiling. War exhaustion climbing at +0.01 a month reported "reaches maximum in 105 years 7 months". Arithmetically correct and completely useless, because the campaign ends in 1836. Above 600 months the line just goes quiet.
The years-and-months arithmetic runs on integers:
M = FixedPointToInt(value ÷ −change)
years = Divide_int32(M, 12) (truncates)
rem = M − (years × 12)
Singular and plural come from SelectLocalization, which is exactly what vanilla does with its own YEARS_COUNT_SINGULAR / _PLURAL.
The bug that nearly shipped: with positive income, M comes out negative, and a negative number satisfies "less than 1" beautifully. Unguarded, a perfectly healthy treasury would have announced "exhausted within the month." So the check that the resource is genuinely draining lives inside each tier, not wrapped around them. Same story for division by zero, which in this language returns 0 and therefore also satisfies "less than 1".
When to leave localization and drop into the GUI layer
The first version branched inside the .yml itself, with SelectLocalization(condition, 'KEY_TRUE', 'KEY_FALSE'), a pattern vanilla uses 38 times.
That ran out of road the moment I wanted urgency tiers (within the month / months / years). Branching three ways means nesting SelectLocalization inside itself, repeating a 200-character expression in every branch. It wasn't ugly, it was unmaintainable.
So the decision of whether a line shows moved up into the GUI layer, where each state is its own visible and the whole thing stays flat and diffable. All it needed was a shared type with a named slot:
types ReckoningTypes {
type reckoning_projection = vbox {
layoutpolicy_horizontal = expanding
block "reckoning_content" {}
}
}
And the unexpected prize: moving the state into the widget meant the mod stopped overriding any vanilla localization key at all. It now defines only RECKONING_* keys, and since localization overrides work per key rather than per file, it coexists happily with any other text or translation mod.
Each call site ends up looking like this:
reckoning_projection = {
blockoverride "reckoning_content" {
TooltipTextBlock = {
visible = "[And(And(And(LessThan_CFixedPoint(...)))]"
blockoverride "text" { text = "RECKONING_GOLD_IMMINENT" }
}
TooltipTextBlock = {
visible = "[And(And(And(LessThan_CFixedPoint(...)))]"
blockoverride "text" { text = "RECKONING_GOLD_MONTHS" }
}
TooltipTextBlock = {
visible = "[And(And(And(LessThan_CFixedPoint(...)))]"
blockoverride "text" { text = "RECKONING_GOLD_YEARS" }
}
}
}
One note on placement: Paradox has a purpose-built extension point for exactly this, an empty block "extra_tooltip_content" {} on ContextualTooltipType. I used it, then dropped it, because it appends: the line came out underneath the income and expense tables, and a conclusion two scrolling tables away from its premises isn't a conclusion. It goes in inline instead. I only allowed myself that because I already had to own the file anyway — if the hook had been enough on its own, the worse placement would have been worth it.
Two kinds of data access that look identical
Reusing the widget in the Economy panel took three attempts, and produced the single most useful finding of the project.
Attempt 1: copy gold's expressions straight across. All three failed, because InGameTopbar is the topbar widget's own promote and appears zero times in economy_lateralview.gui. The symptom was textbook: all three tiers rendered at once, because in this language a visible condition that fails resolves as visible.
Attempt 2: use EconomyView.GetEstimatedBalance, which is literally the figure printed directly above. Nothing rendered:
No context supplied (Use SetDataContext), wanted context of type 'EconomyView'
Attempt 3: Player.GetCurrencyBalance('gold'). Works.
The general conclusion: the language has two kinds of accessor that are written identically. Free promotes (Player, GetPlayer) resolve anywhere; datacontexts (EconomyView, InGameTopbar, Country) only resolve where an ancestor widget supplies them. Nothing in the syntax tells them apart. You find out from the log.
The same projection in the Economy panel, sitting under the balance figure it comes from. Same widget, different accessor.
Everything here fails open
That panel worked. It looked right, in red, in the right place. It was also emitting about 70 errors per second:
FetchData failed for 'ExtraTooltipInfo.GetTintColor' - gui/shared/main_menu_cooltip_types.gui
No context supplied (Use SetDataContext), wanted context of type 'ExtraTooltipInfo'
TooltipTextBlock drags in tooltip_text_block_template, which binds fonttintcolor to ExtraTooltipInfo.GetTintColor — a context only a tooltip supplies. Used inside a panel, it fails every frame. The fix is to drop that binding, and only on the panel's path:
blockoverride "tooltip_text_color" {}
Which is the lesson I take from the language as a whole: correctness and appearance are independent. A failed visible renders. A failed tint renders in the default colour. One bad expression blanks an entire localization key, not just its own fragment. A broken widget looks exactly like a working one, so error.log isn't a debugging tool here, it's the only source of truth. It gives you the file, the line and the expression. It stayed open for the whole build.
What it doesn't do
- No diplomats or complacency. Diplomats use
GetDiplomacy.GetMaxNumOfDiplomatsinstead ofGetCurrencyMaxValueand their template has no anchor point to insert at — two special cases for one resource. Complacency is a situational mechanic I don't understand well enough to word without lying about it. - English only.
- It conflicts with any mod that overrides
topbar_tooltips.guioreconomy_lateralview.gui. That one is inherent: there's no way to add a widget inside a tooltip template without owning the file.
Update 1.1 — making it look native
28 August 2026
After publishing it, I went back and looked at the line as part of EU5's interface rather than as an isolated feature. The calculations were right, but the presentation still gave the mod away: it said “Treasury” where the game normally lets the gold icon do the talking, repeated icons in front of already-named resources, and coloured units such as “years” and “months” even though the vanilla interface reserves that emphasis for the numbers.
Version 1.1 turns those observations into shared rules:
- Gold now follows the grammar of the vanilla balance line: “We will run out of [gold icon] in 4 years and 1 month”, without imposing “Ducats” or “Treasury” on countries whose currency may have a different name.
- Named resources such as Manpower are linked concepts and retain their nested hover explanations, but no longer get a redundant icon in front.
- Only the figure is highlighted. The words “year”, “month” and their plurals keep the normal text style; dangerous projections use red numbers and replenishment uses the vanilla value style.
- The sub-month case follows the same rule: “within 1 month”, without pretending to have day-level precision that the game's monthly calculation cannot support.
Gold leaves the currency name to the icon and highlights only 7 and 10. Hovering the linked Manpower name opens the vanilla explanation without losing the projection.
The same wording and formatting rules inside the Economy panel.
I didn't patch those decisions into seven resources separately. I put them in the private generator's central resource table and the shared localization keys, so gold, legitimacy, prestige, stability, manpower, sailors and war exhaustion all follow the same grammar in both the topbar and the Economy panel. The two regenerated .gui files remained byte-for-byte identical to the previous release: the shipped update changes presentation and localization, not calculations, conditions or layout.
I regression-tested gold in both surfaces, manpower, the nested tooltips and the error viewer on a clean installation. The projections stayed correct and the log remained free of mod-attributable errors.
¡Suscríbete en el Steam Workshop!
Sobre el proyecto
Esto empezó como un mod para mí, no para nadie más.
El tooltip del oro de EU5 te dice exactamente cuánto dinero tienes y exactamente cuánto pierdes cada mes, y ahí se para. Tiene las dos mitades de la cuenta y no hace la cuenta. Yo estaba haciendo esa división de cabeza cada dos por tres, y mal, así que escribí la línea que el tooltip no imprimía.
Me llevó una tarde. Y luego, una vez funcionaba con el oro, quedó bastante claro que a todo lo demás le faltaba la misma línea (soldados, marineros, estabilidad, desgaste de guerra) y que la parte interesante no era la aritmética, sino todo lo que el juego hace para que no puedas escribirla. Así que seguí, y acabó siendo una pieza de UI en condiciones: siete recursos, el panel de Economía, y todo en el propio lenguaje de scripting GUI de Paradox, sin tocar una sola línea de vanilla.
Todo lo que está por encima y por debajo del recuadro rojo es vanilla. El mod añade la conclusión.
La localización es un lenguaje de programación
Esa fue la primera sorpresa. Este es el tooltip del oro de vanilla, tal cual:
GOLD_RESOURCE_TT_TEXT: "We have [Player.GetCurrencyValue('gold')|V]@gold! and it changes monthly by [InGameTopbar.GetPlayerCurrencyChangeFormatted('gold')]@gold!."
Eso no es una cadena con huecos. Los [...] son llamadas en vivo a funciones del juego, |V es un flag de formato y @gold! mete un sprite. Y hacen operaciones matemáticas.
Lo que significa que toda la primera versión del mod vivió dentro de un .yml. Y mejor todavía: vanilla ya trae un cálculo de autonomía, para el empleo, así que no tuve que inventarme el idioma, solo copiarlo:
[Divide_CFixedPoint(Multiply_CFixedPoint(<valor>, '(CFixedPoint)-1'), <ritmo>)|0V] months
La fórmula es tesorería ÷ −(cambio mensual). Ese × −1 es lo que convierte un ritmo negativo en una duración positiva, y ese convenio de signos acabó siendo el guardián de todo el mod: si el signo no cuadra, la línea no se dibuja.
Detalle bonito: las constantes se escriben como casts entre comillas ('(CFixedPoint)-1'), y CFixedPoint existe porque los floats no son idénticos bit a bit entre CPUs distintas, lo que rompería el multijugador en lockstep.
La dirección es del recurso, no del widget
Con el oro resuelto, la segunda dirección era obvia: (máximo − valor) ÷ cambio para los recursos que se llenan. Soldados y marineros usan esa, y leen "fully replenished in".
Y aquí es donde el mod estuvo a punto de quedarse con forma de oro para siempre. En todos los recursos menos uno, llenarse es una buena noticia. En el desgaste de guerra subir es el peligro, así que se calcula igual que los soldados pero se pinta en rojo como una advertencia. Esa única excepción es lo que obligó a que la polaridad fuese un dato de cada recurso y no una decisión del widget.
La estabilidad tenía su propia trampa: baja de cero (hud_topbar.gui:622,633 comprueban < 0 y < -50), así que el cero no es un suelo contra el que chocar, es un umbral que se cruza. Por eso dice "turns negative" y no "exhausted".
Misma línea, dirección contraria: los soldados se están llenando, así que la proyección es una fecha de entrega, no una advertencia.
Truncados, y un techo de 50 años
Dos decisiones sobre las unidades, y las dos salieron de mirar lo que hace la simulación en vez de lo que quedaba bonito:
Truncar, no redondear. 97.79 se muestra como 97. Tienes 97 meses completos; el 98 está a medias. Una advertencia que redondea hacia arriba te está diciendo que tienes más margen del que tienes.
Un techo de 50 años. El desgaste de guerra subiendo a +0.01 al mes daba "reaches maximum in 105 years 7 months". Aritméticamente correcto y completamente inútil, porque la campaña se acaba en 1836. Por encima de 600 meses la línea simplemente calla.
La aritmética de años y meses va con enteros:
M = FixedPointToInt(valor ÷ −cambio)
años = Divide_int32(M, 12) (trunca)
resto = M − (años × 12)
Y el singular y el plural salen de SelectLocalization, que es exactamente lo que hace vanilla con sus YEARS_COUNT_SINGULAR / _PLURAL.
El bug que casi se cuela: con ingresos positivos, M sale negativo, y un número negativo cumple perfectamente eso de "menos de 1". Sin protección, una tesorería sanísima habría anunciado "exhausted within the month." Por eso la comprobación de que el recurso está bajando de verdad va dentro de cada nivel, no envolviéndolos. Lo mismo con la división por cero, que en este lenguaje devuelve 0 y por lo tanto también cumple "menos de 1".
Cuándo dejar la localización y bajar al GUI
La primera versión ramificaba dentro del propio .yml, con SelectLocalization(condición, 'CLAVE_SI', 'CLAVE_NO'), que es un patrón que vanilla usa 38 veces.
Eso se acabó en cuanto quise niveles de urgencia (dentro de un mes / meses / años). Ramificar tres veces significa anidar SelectLocalization dentro de sí mismo repitiendo una expresión de 200 caracteres en cada rama. No es que quedara feo, es que dejaba de ser mantenible.
Así que la decisión de si se muestra una línea se subió a la capa de GUI, donde cada estado es un visible aparte y el conjunto se queda plano y diffeable. Solo hizo falta un tipo compartido con un hueco con nombre:
types ReckoningTypes {
type reckoning_projection = vbox {
layoutpolicy_horizontal = expanding
block "reckoning_content" {}
}
}
Y el premio inesperado: al mover el estado al widget, el mod dejó de sobrescribir claves de localización de vanilla. Ahora solo define claves RECKONING_*, y como la localización se sobrescribe por clave y no por archivo, convive sin problema con cualquier otro mod de texto o de traducción.
Cada punto de uso queda así:
reckoning_projection = {
blockoverride "reckoning_content" {
TooltipTextBlock = {
visible = "[And(And(And(LessThan_CFixedPoint(...)))]"
blockoverride "text" { text = "RECKONING_GOLD_IMMINENT" }
}
TooltipTextBlock = {
visible = "[And(And(And(LessThan_CFixedPoint(...)))]"
blockoverride "text" { text = "RECKONING_GOLD_MONTHS" }
}
TooltipTextBlock = {
visible = "[And(And(And(LessThan_CFixedPoint(...)))]"
blockoverride "text" { text = "RECKONING_GOLD_YEARS" }
}
}
}
Un apunte de colocación: Paradox tiene un punto de extensión pensado justo para esto, un block "extra_tooltip_content" {} vacío en ContextualTooltipType. Lo usé, y lo quité, porque añade al final: la línea salía por debajo de las tablas de ingresos y gastos, y una conclusión a dos tablas de scroll de sus premisas no es una conclusión. Va insertada en línea. Solo me lo permití porque de todas formas ya tenía que ser dueño del archivo; si el hook hubiera bastado por sí solo, me habría tragado la peor colocación.
Dos formas de acceder a datos que se escriben igual
Reutilizar el widget en el panel de Economía costó tres intentos, y de ahí salió el hallazgo más útil de todo el proyecto.
Intento 1: copiar las expresiones del oro tal cual. Fallaron las tres, porque InGameTopbar es el promote del propio widget de la barra superior y aparece cero veces en economy_lateralview.gui. Y el síntoma fue de manual: se dibujaron los tres niveles a la vez, porque en este lenguaje una condición visible que falla se resuelve como visible.
Intento 2: usar EconomyView.GetEstimatedBalance, que es literalmente la cifra impresa justo encima. No se dibujó nada:
No context supplied (Use SetDataContext), wanted context of type 'EconomyView'
Intento 3: Player.GetCurrencyBalance('gold'). Funciona.
La conclusión general: el lenguaje tiene dos tipos de accessor que se escriben igual. Los promotes libres (Player, GetPlayer) resuelven en cualquier sitio; los datacontexts (EconomyView, InGameTopbar, Country) solo resuelven donde un widget antecesor los proporciona. Nada en la sintaxis los distingue. Te enteras por el log.
La misma proyección en el panel de Economía, justo debajo del balance del que sale. Mismo widget, distinto accessor.
Aquí todos los fallos fallan hacia "sí"
Ese panel funcionaba. Se veía bien, en rojo, en su sitio. También estaba escupiendo unos 70 errores por segundo:
FetchData failed for 'ExtraTooltipInfo.GetTintColor' - gui/shared/main_menu_cooltip_types.gui
No context supplied (Use SetDataContext), wanted context of type 'ExtraTooltipInfo'
TooltipTextBlock arrastra tooltip_text_block_template, que ata fonttintcolor a ExtraTooltipInfo.GetTintColor, un contexto que solo proporciona un tooltip. Metido en un panel, falla en cada frame. Se arregla soltando ese enlace, y solo en la ruta del panel:
blockoverride "tooltip_text_color" {}
Y esa es la lección que me llevo del lenguaje entero: corrección y apariencia son independientes. Un visible que falla se dibuja. Un tinte que falla usa el color por defecto. Una expresión mala vacía la clave de localización entera, no solo su trozo. Un widget roto tiene exactamente la misma pinta que uno que funciona, así que error.log no es una herramienta de depuración, es la única fuente de verdad. Te da el archivo, la línea y la expresión. Estuvo abierto todo el desarrollo.
Lo que no hace
- No cubre diplomáticos ni complacencia. Los diplomáticos usan
GetDiplomacy.GetMaxNumOfDiplomatsen vez deGetCurrencyMaxValuey su plantilla no tiene un punto donde anclar la línea: dos casos especiales para un solo recurso. La complacencia es una mecánica situacional que no entiendo lo bastante bien como para redactarla sin mentir. - Solo en inglés.
- Choca con cualquier mod que sobrescriba
topbar_tooltips.guioeconomy_lateralview.gui. Eso es inherente: no hay forma de meter un widget dentro de una plantilla de tooltip sin ser dueño del archivo.
Actualización 1.1 — que parezca parte del juego
28 de agosto de 2026
Después de publicarlo volví a mirar la línea como parte de la interfaz de EU5, no como una función aislada. Los cálculos eran correctos, pero la presentación todavía delataba que venía de un mod: decía «Tesoro» donde el juego suele dejar que hable el icono de oro, repetía iconos delante de recursos que ya tenían nombre y coloreaba unidades como «años» y «meses» cuando la interfaz vanilla reserva ese énfasis para los números.
La actualización 1.1 convierte esas observaciones en reglas compartidas:
- El oro ahora sigue la gramática del balance vanilla: «Nos quedaremos sin [icono de oro] en 4 años y 1 mes», sin imponer «Ducados» ni «Tesoro» a países cuya moneda puede llamarse de otra forma.
- Los recursos con nombre, como Manpower, aparecen como conceptos enlazados y conservan su explicación anidada al pasar el ratón, pero sin un icono redundante delante.
- Solo se resalta la cifra. Las palabras «año», «mes» y sus plurales mantienen el estilo normal; una proyección peligrosa usa números rojos y una reposición usa el estilo de valor de vanilla.
- El caso de menos de un mes usa la misma regla: «dentro de 1 mes», sin presentar una precisión de días que el cálculo mensual del juego no puede sostener.
El oro deja el nombre de la moneda en manos del icono y solo resalta 7 y 10. Al pasar el ratón por el enlace Manpower se abre la explicación vanilla sin perder la proyección.
La misma frase y las mismas reglas de formato dentro del panel de Economía.
No parcheé estas decisiones recurso por recurso. Las puse en la tabla central del generador privado y en las claves de localización compartidas, así que oro, legitimidad, prestigio, estabilidad, manpower, marineros y agotamiento de guerra obedecen la misma gramática tanto en la barra superior como en el panel de Economía. Los dos archivos .gui regenerados quedaron idénticos byte por byte a la versión anterior: la actualización distribuida cambia presentación y localización, no cálculo, condiciones ni layout.
Volví a probar oro en las dos superficies, manpower, los tooltips anidados y el visor de errores en una instalación limpia. El resultado mantuvo las proyecciones correctas y dejó el log sin errores atribuibles al mod.