From 0154df91673bfad88116d319ed098693a9326615 Mon Sep 17 00:00:00 2001 From: Shush Date: Tue, 27 Oct 2026 15:37:41 +0200 Subject: [PATCH 1/4] Allow modders to customize health bar offsets --- code/techno.cpp | 4 ++-- code/uicontrol.cpp | 3 +++ code/uicontrol.h | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/code/techno.cpp b/code/techno.cpp index 151fa1552..31bf37adc 100644 --- a/code/techno.cpp +++ b/code/techno.cpp @@ -1579,13 +1579,13 @@ void TechnoClass::Draw_Health_Bar(Point2D const & xpoint, Rect const & cliprect) if (IsSelected) { Draw_Shape(*LogicalSurface, *NormalDrawer, (ShapeSet const *)ObjectTypeClass::SelectShapes, powerup ? 6 : 2, xpoint, cliprect, ShapeFlags_Type(SHAPE_ALPHA|SHAPE_WIN_REL|SHAPE_CENTER)); } - offset = Point2D(-5, -24); + offset = UIControls.InfantryHealthBarPosOffset; health_bar_count = 8; } else { if (IsSelected) { Draw_Shape(*LogicalSurface, *NormalDrawer, (ShapeSet const *)ObjectTypeClass::SelectShapes, (LimpetType.Any() ? 8 : 0) + (powerup ? 4 : 0) + 3, xpoint, cliprect, ShapeFlags_Type(SHAPE_ALPHA|SHAPE_WIN_REL|SHAPE_CENTER)); } - offset = Point2D(-15, -25); + offset = UIControls.UnitHealthBarPosOffset; health_bar_count = 17; } diff --git a/code/uicontrol.cpp b/code/uicontrol.cpp index e394ab39a..b177f0f63 100644 --- a/code/uicontrol.cpp +++ b/code/uicontrol.cpp @@ -56,6 +56,9 @@ bool UIControlsClass::Read_INI(CCINIClass const & ini) NavComQueueLineColor = ini.Get_RGBClass(INGAME, "NavComQueueLineColor", NavComQueueLineColor); NavComQueueLineDropShadowColor = ini.Get_RGBClass(INGAME, "NavComQueueLineDropShadowColor", NavComQueueLineDropShadowColor); + InfantryHealthBarPosOffset = ini.Get_Point(INGAME, "InfantryHealthBarPos", InfantryHealthBarPosOffset); + UnitHealthBarPosOffset = ini.Get_Point(INGAME, "UnitHealthBarPos", UnitHealthBarPosOffset); + UnitGroupNumberOffset = ini.Get_Point(PIPS, "UnitGroupNumberOffset", UnitGroupNumberOffset); InfantryGroupNumberOffset = ini.Get_Point(PIPS, "InfantryGroupNumberOffset", InfantryGroupNumberOffset); BuildingGroupNumberOffset = ini.Get_Point(PIPS, "BuildingGroupNumberOffset", BuildingGroupNumberOffset); diff --git a/code/uicontrol.h b/code/uicontrol.h index dabdbc8ec..acacc37e1 100644 --- a/code/uicontrol.h +++ b/code/uicontrol.h @@ -84,4 +84,8 @@ class UIControlsClass Point2D InfantryWithPipGroupNumberOffset = Point2D(-4, -8); Point2D BuildingWithPipGroupNumberOffset = Point2D(-4, -8); Point2D AircraftWithPipGroupNumberOffset = Point2D(-4, -8); + + // Health bar drawing positions + Point2D InfantryHealthBarPosOffset = Point2D(-5, -24); + Point2D UnitHealthBarPosOffset = Point2D(-15, -25); }; From 0ac9c03d35a2668572abdb14d6499076cb7ebcb1 Mon Sep 17 00:00:00 2001 From: Shush Date: Tue, 27 Oct 2026 16:07:36 +0200 Subject: [PATCH 2/4] Add docs --- manual/changes/health-bar-offsets.md | 26 +++++++++++++++ manual/content/formats/ui-ini.md | 18 +++++++++-- manual/content/keys/infantryhealthbarpos.md | 15 +++++++++ manual/content/keys/unithealthbarpos.md | 17 ++++++++++ manual/content/using/configuration-files.md | 2 +- manual/data/ini-keys.yaml | 36 +++++++++++++++++++++ 6 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 manual/changes/health-bar-offsets.md create mode 100644 manual/content/keys/infantryhealthbarpos.md create mode 100644 manual/content/keys/unithealthbarpos.md diff --git a/manual/changes/health-bar-offsets.md b/manual/changes/health-bar-offsets.md new file mode 100644 index 000000000..006d10e2d --- /dev/null +++ b/manual/changes/health-bar-offsets.md @@ -0,0 +1,26 @@ +--- +title: Place health bars from UI.INI +category: feature +release: 0.2.0 +breaking: true +migration: +- In the `[Ingame]` section of each `UI.INI` the mod ships, find `InfantryHealthBarPos=` and `UnitHealthBarPos=`. +- Swap the two numbers in each value. OpenTS reads the horizontal offset (X) first, while Vinifera writes the vertical offset (Y) first; for example, `InfantryHealthBarPos=-24,-5` becomes `InfantryHealthBarPos=-5,-24`. The same should be done for `UnitHealthBarPos` as well. +targets: +- type: key + id: InfantryHealthBarPos + effect: added +- type: key + id: UnitHealthBarPos + effect: added +- type: format + id: ui-ini + effect: changed +credit: +- Shush +- CCHyper +--- + +Two keys in the `[Ingame]` section of [UI.INI](/formats/ui-ini/#health-bar-positions) set where an object's row of health pips begins: [`InfantryHealthBarPos`](/keys/infantryhealthbarpos/) for infantry and [`UnitHealthBarPos`](/keys/unithealthbarpos/) for vehicles and aircraft. Each takes a horizontal and a vertical offset in pixels; positive values move the row right and down. Their defaults keep the original position. + +Credits to CCHyper for their original Vinifera implementation for this feature. diff --git a/manual/content/formats/ui-ini.md b/manual/content/formats/ui-ini.md index bfdead26e..fdf20fb9a 100644 --- a/manual/content/formats/ui-ini.md +++ b/manual/content/formats/ui-ini.md @@ -1,7 +1,7 @@ --- format_id: ui-ini title: UI.INI -summary: Sets how a selected object's order lines, a firing vehicle's sighting laser and a control group number are drawn. +summary: Sets how a selected object's order lines, a firing vehicle's sighting laser, control group numbers and health bars are drawn. kind: file source_files: - code/uicontrol.cpp @@ -26,7 +26,7 @@ related: id: configuration-files --- -The file is optional and every key has a default, so a file names only what it changes. The order-line and sighting-laser keys go in `[Ingame]`, and the control group number keys go in `[Pips]`. The values below are examples. +The file is optional and every key has a default, so a file names only what it changes. The order-line, sighting-laser and health-bar keys go in `[Ingame]`, and the control group number keys go in `[Pips]`. The values below are examples. ```ini title="UI.INI" [Ingame] @@ -34,6 +34,7 @@ AlwaysShowActionLines=yes MovementLineDashed=yes MovementLineColor=0,255,0 NavComQueueLineThick=yes +InfantryHealthBarPos=-5,-20 [Pips] UnitWithPipGroupNumberOffset=-4,-15 @@ -63,6 +64,19 @@ The defaults are `-4,-4` without pips and `-4,-8` with pips. A value that does n Vinifera reads the same eight keys from `[Ingame]`, so move them to `[Pips]` when reusing a Vinifera file. Setting all eight to `-8,-33` gives the position Twisted Insurrection uses. +## Health bar positions + +Infantry, vehicles and aircraft show their health as a row of pips. Two `[Ingame]` keys set where that row begins: + +| Object | Key | +| --- | --- | +| Infantry | [`InfantryHealthBarPos`](/keys/infantryhealthbarpos/) | +| Vehicle or aircraft | [`UnitHealthBarPos`](/keys/unithealthbarpos/) | + +A structure, and a vehicle with [`IsCoreDefender=yes`](/keys/iscoredefender/#scope-unittype), draw their health as a pip bar along the near edge of their footprint instead and use neither key. + +Each value is two whole numbers, a horizontal and a vertical offset in pixels, separated by a comma. Positive values move the row right and down, while negative values move it left and up. The offset is measured from the object's position on screen. The first pip is centered on that offset point, and each later pip is two pixels further right, so the offset moves the whole row. A value that does not start with two comma-separated numbers keeps the default, and anything after the second number is ignored. + ## When the file is read The game reads the file at startup, after it registers the game archives. It reads the file again each time a scenario or saved game loads, after mounting the archives of the player's side, so a copy inside a side's archive applies to games played as that side. It reads the file once more when a match ends and the menus return, after mounting the [no-side archives](/formats/mix/#theater-side-and-speech-archives). Each read starts from the defaults, so only the keys in the copy read last take effect. diff --git a/manual/content/keys/infantryhealthbarpos.md b/manual/content/keys/infantryhealthbarpos.md new file mode 100644 index 000000000..f70fd82be --- /dev/null +++ b/manual/content/keys/infantryhealthbarpos.md @@ -0,0 +1,15 @@ +--- +key: InfantryHealthBarPos +summary: Where an infantry object's row of health pips is drawn. +see_also: [UnitHealthBarPos] +when_omitted: + kind: value + value: "-5,-24" +--- + +`InfantryHealthBarPos=` sets where an infantry object's row of health pips begins. The value is a horizontal and a vertical offset in pixels; positive values move the row right and down. [Health bar positions](/formats/ui-ini/#health-bar-positions) covers the point the offset is measured from and how the row is laid out. + +```ini title="UI.INI" +[Ingame] +InfantryHealthBarPos=-5,-20 +``` diff --git a/manual/content/keys/unithealthbarpos.md b/manual/content/keys/unithealthbarpos.md new file mode 100644 index 000000000..06ebbe9f8 --- /dev/null +++ b/manual/content/keys/unithealthbarpos.md @@ -0,0 +1,17 @@ +--- +key: UnitHealthBarPos +summary: Where a vehicle's or aircraft's row of health pips is drawn. +see_also: [InfantryHealthBarPos] +when_omitted: + kind: value + value: "-15,-25" +--- + +`UnitHealthBarPos=` sets where a vehicle's or aircraft's row of health pips begins. The value is a horizontal and a vertical offset in pixels; positive values move the row right and down. [Health bar positions](/formats/ui-ini/#health-bar-positions) covers the point the offset is measured from and how the row is laid out. + +A vehicle with [`IsCoreDefender=yes`](/keys/iscoredefender/#scope-unittype) draws a structure's pip bar along the near edge of its footprint instead and does not use the key. + +```ini title="UI.INI" +[Ingame] +UnitHealthBarPos=-15,-20 +``` diff --git a/manual/content/using/configuration-files.md b/manual/content/using/configuration-files.md index c2c1106e6..16b7e8aef 100644 --- a/manual/content/using/configuration-files.md +++ b/manual/content/using/configuration-files.md @@ -25,7 +25,7 @@ The game reads its configuration from these files: - `SUN.INI` stores the player's options. - [`KEYBOARD.INI`](/formats/keyboard-ini/) assigns keys to command names. -- [`UI.INI`](/formats/ui-ini/), which a mod or deployment may ship, sets how order lines and the sighting laser are drawn. +- [`UI.INI`](/formats/ui-ini/), which a mod or deployment may ship, sets how order lines, the sighting laser, control group numbers and health bars are drawn. - Rules, art, sound, theme, and scenario files supply game and mod data. A deployment can rename these files in [`OPENTS.INI`](/formats/opents-ini/#the-files-it-reads): the settings file, `UI.INI`, the tutorial file, and the rules, language rules, multiplayer rules, art, AI, sound, theme, and campaign list files together with their expansion copies. `KEYBOARD.INI` and scenario files cannot be renamed. diff --git a/manual/data/ini-keys.yaml b/manual/data/ini-keys.yaml index 9e64aa358..7a76ef9e8 100644 --- a/manual/data/ini-keys.yaml +++ b/manual/data/ini-keys.yaml @@ -11547,6 +11547,24 @@ InfantryGroupNumberOffset: source: code/uicontrol.cpp guard: null level: UIControlsClass +InfantryHealthBarPos: + key: InfantryHealthBarPos + scopes: + - applies_to: + - UI controls + file: ui.ini + section: + kind: literal + name: Ingame + value_type: point (x,y) + status: generated + _provenance: + default_candidate: null + declared_in: UIControlsClass + member: InfantryHealthBarPosOffset + source: code/uicontrol.cpp + guard: null + level: UIControlsClass InfantryReserve: key: InfantryReserve scopes: @@ -25007,6 +25025,24 @@ UnitGroupNumberOffset: source: code/uicontrol.cpp guard: null level: UIControlsClass +UnitHealthBarPos: + key: UnitHealthBarPos + scopes: + - applies_to: + - UI controls + file: ui.ini + section: + kind: literal + name: Ingame + value_type: point (x,y) + status: generated + _provenance: + default_candidate: null + declared_in: UIControlsClass + member: UnitHealthBarPosOffset + source: code/uicontrol.cpp + guard: null + level: UIControlsClass UnitReload: key: UnitReload scopes: From 8c78a3181e66b63c736ed82a229585c3a1b1567b Mon Sep 17 00:00:00 2001 From: Shush Date: Sat, 31 Oct 2026 09:00:00 +0200 Subject: [PATCH 3/4] Fix credits name --- manual/changes/health-bar-offsets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manual/changes/health-bar-offsets.md b/manual/changes/health-bar-offsets.md index 006d10e2d..1d93a2352 100644 --- a/manual/changes/health-bar-offsets.md +++ b/manual/changes/health-bar-offsets.md @@ -17,7 +17,7 @@ targets: id: ui-ini effect: changed credit: -- Shush +- JoyfulShush - CCHyper --- From 62464878fcf4ae5a2f37fa1e098055f9cd678ca4 Mon Sep 17 00:00:00 2001 From: ZivDero Date: Sat, 14 Nov 2026 13:24:59 +0300 Subject: [PATCH 4/4] Mark health bar keys non-breaking for 0.3.0 and match member names --- code/techno.cpp | 4 ++-- code/uicontrol.cpp | 4 ++-- code/uicontrol.h | 5 ++--- manual/changes/health-bar-offsets.md | 8 ++------ manual/content/formats/ui-ini.md | 2 ++ manual/data/ini-keys.yaml | 4 ++-- 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/code/techno.cpp b/code/techno.cpp index 307abb5df..30c8618b7 100644 --- a/code/techno.cpp +++ b/code/techno.cpp @@ -1579,13 +1579,13 @@ void TechnoClass::Draw_Health_Bar(Point2D const & xpoint, Rect const & cliprect) if (IsSelected) { Draw_Shape(*LogicalSurface, *NormalDrawer, (ShapeSet const *)ObjectTypeClass::SelectShapes, powerup ? 6 : 2, xpoint, cliprect, ShapeFlags_Type(SHAPE_ALPHA|SHAPE_WIN_REL|SHAPE_CENTER)); } - offset = UIControls.InfantryHealthBarPosOffset; + offset = UIControls.InfantryHealthBarPos; health_bar_count = 8; } else { if (IsSelected) { Draw_Shape(*LogicalSurface, *NormalDrawer, (ShapeSet const *)ObjectTypeClass::SelectShapes, (LimpetType.Any() ? 8 : 0) + (powerup ? 4 : 0) + 3, xpoint, cliprect, ShapeFlags_Type(SHAPE_ALPHA|SHAPE_WIN_REL|SHAPE_CENTER)); } - offset = UIControls.UnitHealthBarPosOffset; + offset = UIControls.UnitHealthBarPos; health_bar_count = 17; } diff --git a/code/uicontrol.cpp b/code/uicontrol.cpp index b177f0f63..75ffa160e 100644 --- a/code/uicontrol.cpp +++ b/code/uicontrol.cpp @@ -56,8 +56,8 @@ bool UIControlsClass::Read_INI(CCINIClass const & ini) NavComQueueLineColor = ini.Get_RGBClass(INGAME, "NavComQueueLineColor", NavComQueueLineColor); NavComQueueLineDropShadowColor = ini.Get_RGBClass(INGAME, "NavComQueueLineDropShadowColor", NavComQueueLineDropShadowColor); - InfantryHealthBarPosOffset = ini.Get_Point(INGAME, "InfantryHealthBarPos", InfantryHealthBarPosOffset); - UnitHealthBarPosOffset = ini.Get_Point(INGAME, "UnitHealthBarPos", UnitHealthBarPosOffset); + InfantryHealthBarPos = ini.Get_Point(INGAME, "InfantryHealthBarPos", InfantryHealthBarPos); + UnitHealthBarPos = ini.Get_Point(INGAME, "UnitHealthBarPos", UnitHealthBarPos); UnitGroupNumberOffset = ini.Get_Point(PIPS, "UnitGroupNumberOffset", UnitGroupNumberOffset); InfantryGroupNumberOffset = ini.Get_Point(PIPS, "InfantryGroupNumberOffset", InfantryGroupNumberOffset); diff --git a/code/uicontrol.h b/code/uicontrol.h index acacc37e1..c83e4122b 100644 --- a/code/uicontrol.h +++ b/code/uicontrol.h @@ -85,7 +85,6 @@ class UIControlsClass Point2D BuildingWithPipGroupNumberOffset = Point2D(-4, -8); Point2D AircraftWithPipGroupNumberOffset = Point2D(-4, -8); - // Health bar drawing positions - Point2D InfantryHealthBarPosOffset = Point2D(-5, -24); - Point2D UnitHealthBarPosOffset = Point2D(-15, -25); + Point2D InfantryHealthBarPos = Point2D(-5, -24); + Point2D UnitHealthBarPos = Point2D(-15, -25); }; diff --git a/manual/changes/health-bar-offsets.md b/manual/changes/health-bar-offsets.md index 1d93a2352..e615006a0 100644 --- a/manual/changes/health-bar-offsets.md +++ b/manual/changes/health-bar-offsets.md @@ -1,11 +1,7 @@ --- title: Place health bars from UI.INI category: feature -release: 0.2.0 -breaking: true -migration: -- In the `[Ingame]` section of each `UI.INI` the mod ships, find `InfantryHealthBarPos=` and `UnitHealthBarPos=`. -- Swap the two numbers in each value. OpenTS reads the horizontal offset (X) first, while Vinifera writes the vertical offset (Y) first; for example, `InfantryHealthBarPos=-24,-5` becomes `InfantryHealthBarPos=-5,-24`. The same should be done for `UnitHealthBarPos` as well. +release: 0.3.0 targets: - type: key id: InfantryHealthBarPos @@ -23,4 +19,4 @@ credit: Two keys in the `[Ingame]` section of [UI.INI](/formats/ui-ini/#health-bar-positions) set where an object's row of health pips begins: [`InfantryHealthBarPos`](/keys/infantryhealthbarpos/) for infantry and [`UnitHealthBarPos`](/keys/unithealthbarpos/) for vehicles and aircraft. Each takes a horizontal and a vertical offset in pixels; positive values move the row right and down. Their defaults keep the original position. -Credits to CCHyper for their original Vinifera implementation for this feature. +The keys have Vinifera's names, but Vinifera reads the vertical offset first, so swap the two numbers in a Vinifera file. CCHyper is credited for the Vinifera implementation this follows. diff --git a/manual/content/formats/ui-ini.md b/manual/content/formats/ui-ini.md index 8184dd6fe..c233fe800 100644 --- a/manual/content/formats/ui-ini.md +++ b/manual/content/formats/ui-ini.md @@ -77,6 +77,8 @@ A structure, and a vehicle with [`IsCoreDefender=yes`](/keys/iscoredefender/#sco Each value is two whole numbers, a horizontal and a vertical offset in pixels, separated by a comma. Positive values move the row right and down, while negative values move it left and up. The offset is measured from the object's position on screen. The first pip is centered on that offset point, and each later pip is two pixels further right, so the offset moves the whole row. A value that does not start with two comma-separated numbers keeps the default, and anything after the second number is ignored. +Vinifera reads the same two keys with the vertical offset first, so swap the two numbers when reusing a Vinifera file: `InfantryHealthBarPos=-24,-5` becomes `InfantryHealthBarPos=-5,-24`. Vinifera also moves the vehicle row one pixel left by default; set `UnitHealthBarPos=-16,-25` to match it. + ## When the file is read The game reads the file at startup, after it registers the game archives. It reads the file again each time a scenario or saved game loads, after mounting the archives of the player's side, so a copy inside a side's archive applies to games played as that side. It reads the file once more when a match ends and the menus return, after the side's archives [move behind the other archives](/formats/mix/#theater-side-and-speech-archives). Each read starts from the defaults, so only the keys in the copy read last take effect. diff --git a/manual/data/ini-keys.yaml b/manual/data/ini-keys.yaml index ccaf5d7a1..98b4b7f5d 100644 --- a/manual/data/ini-keys.yaml +++ b/manual/data/ini-keys.yaml @@ -11575,7 +11575,7 @@ InfantryHealthBarPos: _provenance: default_candidate: null declared_in: UIControlsClass - member: InfantryHealthBarPosOffset + member: InfantryHealthBarPos source: code/uicontrol.cpp guard: null level: UIControlsClass @@ -25070,7 +25070,7 @@ UnitHealthBarPos: _provenance: default_candidate: null declared_in: UIControlsClass - member: UnitHealthBarPosOffset + member: UnitHealthBarPos source: code/uicontrol.cpp guard: null level: UIControlsClass