From 658cd5edbff58474cdcd129eb8484f15f437fef9 Mon Sep 17 00:00:00 2001 From: iHateCode <6393143+iHateCode@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:25:10 +0000 Subject: [PATCH 1/4] feat: LunaLib integration, overlay options, and docs - Add LunaLib/LazyLib dependency; settings via LunaSettings (Mod Settings F2) - Config: max factions, text size (Small/Medium/Large), overlay keybind (Toggle/Hold), hostile-only filter - FactionRelationshipsKeybindScript for overlay toggle/hold; fix EveryFrameScript import - Build: optional LUNALIB/LAZYLIB in compile.local.bat for versioned mod folders - Bump font sizes (Large uses ORBITRON_20AA, 24px line) - Update README, COMPILATION.md, compile.local.example.bat Made-with: Cursor --- FactionRelationships/COMPILATION.md | 30 +++-- FactionRelationships/compile.bat | 17 ++- .../compile.local.example.bat | 4 + .../data/config/LunaSettings.csv | 7 ++ FactionRelationships/mod_info.json | 8 +- .../FactionRelationshipsKeybindScript.java | 49 ++++++++ .../FactionRelationshipsPlugin.java | 64 +++++++++- .../FactionRelationshipsUIRenderer.java | 113 +++++++++++++++--- README.md | 19 +-- 9 files changed, 273 insertions(+), 38 deletions(-) create mode 100644 FactionRelationships/data/config/LunaSettings.csv create mode 100644 FactionRelationships/src/com/factionrelationships/FactionRelationshipsKeybindScript.java diff --git a/FactionRelationships/COMPILATION.md b/FactionRelationships/COMPILATION.md index 97daac7..bb49451 100644 --- a/FactionRelationships/COMPILATION.md +++ b/FactionRelationships/COMPILATION.md @@ -7,11 +7,13 @@ - **Mod ID**: `factionrelationships` - **Game Version**: `0.98a-RC8` - **Mod Plugin**: `com.factionrelationships.FactionRelationshipsPlugin` +- **Dependencies**: LazyLib and LunaLib (required at runtime and for compilation). ## Prerequisites 1. **JDK 17** – Starsector 0.98a uses Java 17. Install JDK 17 and run `javac -version`. 2. **Starsector** – JARs are taken from your game install (the folder that contains `starsector-core`). +3. **LunaLib and LazyLib** – Install both mods in your Starsector `mods` folder. The compile script looks for `mods/LunaLib/jars/LunaLib.jar` and `mods/LazyLib/jars/LazyLib.jar` by default. If your mod folders use versioned names (e.g. `LunaLib-2.0.5`, `LazyLib-3.0.0`), set `LUNALIB` and `LAZYLIB` in `compile.local.bat` to point at those JARs (see Developer config below). Required for compilation and at runtime. ## Developer config (optional) @@ -19,6 +21,7 @@ To avoid editing `compile.bat` and to keep your local paths out of the repo: 1. Copy `FactionRelationships/compile.local.example.bat` to `FactionRelationships/compile.local.bat`. 2. Edit `compile.local.bat` and set `GAME_DIR` to your Starsector install directory. +3. If LunaLib or LazyLib use versioned folder names (e.g. `LunaLib-2.0.5`), set `LUNALIB` and `LAZYLIB` to the full paths to their JARs so the build can find them. `compile.local.bat` is gitignored; only the example file is committed. The main `compile.bat` loads it automatically when present. @@ -28,16 +31,20 @@ To avoid editing `compile.bat` and to keep your local paths out of the repo: FactionRelationships/ ├── mod_info.json ├── config/ -│ └── faction_relationships_config.json # optional: maxFactions (default 15, clamped 1–50) +│ └── faction_relationships_config.json # deprecated; settings via LunaLib (see data/config) +├── data/ +│ └── config/ +│ └── LunaSettings.csv # LunaLib settings (max factions, text size, overlay keybind) ├── src/ │ └── com/factionrelationships/ │ ├── FactionRelationshipsPlugin.java -│ └── FactionRelationshipsUIRenderer.java +│ ├── FactionRelationshipsUIRenderer.java +│ └── FactionRelationshipsKeybindScript.java ├── compile.local.example.bat # copy to compile.local.bat (gitignored) to set GAME_DIR └── COMPILATION.md ``` -Output: `FactionRelationships-/` (folder and zip), where `` comes from `mod_info.json` (e.g. `FactionRelationships-1.0.0/`, `FactionRelationships-1.0.0.zip`). Contains `classes/`, `jars/FactionRelationships.jar`, and `config/`. +Output: `FactionRelationships-/` (folder and zip), where `` comes from `mod_info.json`. Contains `classes/`, `jars/FactionRelationships.jar`, `config/`, and `data/`. In-game settings and keybind are configured via **Mod Settings** (F2 in campaign) under this mod's section. ## Compile @@ -47,17 +54,19 @@ From the repo root, run the build script: FactionRelationships\compile.bat ``` -Or manually: +Or manually (set `LUNALIB` and `LAZYLIB` if using versioned mod folders): ```batch set GAME_DIR=YOUR_STARSECTOR_PATH +set LUNALIB=%GAME_DIR%\mods\LunaLib\jars\LunaLib.jar +set LAZYLIB=%GAME_DIR%\mods\LazyLib\jars\LazyLib.jar set CORE=%GAME_DIR%\starsector-core -set VERSION=1.0.0 -javac -encoding UTF-8 -cp "%CORE%\starfarer.api.jar;%CORE%\starfarer_obf.jar;%CORE%\log4j-1.2.9.jar;%CORE%\lwjgl.jar;%CORE%\lwjgl_util.jar" -d FactionRelationships-%VERSION%\classes FactionRelationships\src\com\factionrelationships\*.java +set VERSION=1.3.0 +javac -encoding UTF-8 -cp "%CORE%\starfarer.api.jar;%CORE%\starfarer_obf.jar;%CORE%\json.jar;%CORE%\log4j-1.2.9.jar;%CORE%\lwjgl.jar;%CORE%\lwjgl_util.jar;%LAZYLIB%;%LUNALIB%" -d FactionRelationships-%VERSION%\classes FactionRelationships\src\com\factionrelationships\FactionRelationshipsPlugin.java FactionRelationships\src\com\factionrelationships\FactionRelationshipsUIRenderer.java FactionRelationships\src\com\factionrelationships\FactionRelationshipsKeybindScript.java jar cvf FactionRelationships-%VERSION%\jars\FactionRelationships.jar -C FactionRelationships-%VERSION%\classes . ``` -Set `GAME_DIR` before running (e.g. via `compile.local.bat` as above, or in the batch session). +Set `GAME_DIR` (and optionally `LUNALIB`, `LAZYLIB`) before running (e.g. via `compile.local.bat` as above, or in the batch session). ## Package and Install @@ -76,14 +85,17 @@ Set `GAME_DIR` before running (e.g. via `compile.local.bat` as above, or in the ├── mod_info.json ├── config\ │ └── faction_relationships_config.json + ├── data\ + │ └── config\ + │ └── LunaSettings.csv └── jars\ └── FactionRelationships.jar ``` - To change how many factions are shown, edit `config/faction_relationships_config.json` and set `maxFactions` (1–50). Default is 15. + Configure max factions, text size, overlay keybind (toggle or hold), and hostile-only filter in **Mod Settings** (F2 in campaign). ## Quick Reference -- **Game directory**: Set `GAME_DIR` in `compile.local.bat` (recommended) or in `compile.bat` / manual commands. +- **Game directory**: Set `GAME_DIR` in `compile.local.bat` (recommended) or in `compile.bat` / manual commands. Optionally set `LUNALIB` and `LAZYLIB` in `compile.local.bat` if you use versioned LunaLib/LazyLib folders. - **Source**: `FactionRelationships/src/com/factionrelationships/` - **Output**: `FactionRelationships-/` and `FactionRelationships-.zip` (version from `mod_info.json`); JAR at `jars/FactionRelationships.jar` inside the package. diff --git a/FactionRelationships/compile.bat b/FactionRelationships/compile.bat index a6886b9..b0ffbf4 100644 --- a/FactionRelationships/compile.bat +++ b/FactionRelationships/compile.bat @@ -4,11 +4,23 @@ if exist "%~dp0compile.local.bat" call "%~dp0compile.local.bat" if not defined GAME_DIR set GAME_DIR=YOUR_STARSECTOR_PATH set CORE=%GAME_DIR%\starsector-core +if not defined LUNALIB set LUNALIB=%GAME_DIR%\mods\LunaLib\jars\LunaLib.jar +if not defined LAZYLIB set LAZYLIB=%GAME_DIR%\mods\LazyLib\jars\LazyLib.jar if not exist "%CORE%\starfarer.api.jar" ( echo Error: Starsector path not found. echo Copy FactionRelationships\compile.local.example.bat to compile.local.bat and set GAME_DIR to your Starsector install directory. exit /b 1 ) +if not exist "%LUNALIB%" ( + echo Error: LunaLib not found at %LUNALIB% + echo Install LunaLib in your Starsector mods folder to compile. + exit /b 1 +) +if not exist "%LAZYLIB%" ( + echo Error: LazyLib not found at %LAZYLIB% + echo Install LazyLib in your Starsector mods folder to compile. + exit /b 1 +) REM Read version from mod_info.json for package and zip names for /f "delims=" %%v in ('powershell -NoProfile -Command "(Get-Content '%~dp0mod_info.json' -Raw | ConvertFrom-Json).version"') do set VERSION=%%v @@ -22,7 +34,7 @@ if not exist "%OUT%" mkdir "%OUT%" if not exist "..\%PKG%\jars" mkdir "..\%PKG%\jars" echo Compiling... -javac -encoding UTF-8 -cp "%CORE%\starfarer.api.jar;%CORE%\starfarer_obf.jar;%CORE%\json.jar;%CORE%\log4j-1.2.9.jar;%CORE%\lwjgl.jar;%CORE%\lwjgl_util.jar" -d "%OUT%" "%SRC%\FactionRelationshipsPlugin.java" "%SRC%\FactionRelationshipsUIRenderer.java" +javac -encoding UTF-8 -cp "%CORE%\starfarer.api.jar;%CORE%\starfarer_obf.jar;%CORE%\json.jar;%CORE%\log4j-1.2.9.jar;%CORE%\lwjgl.jar;%CORE%\lwjgl_util.jar;%LAZYLIB%;%LUNALIB%" -d "%OUT%" "%SRC%\FactionRelationshipsPlugin.java" "%SRC%\FactionRelationshipsUIRenderer.java" "%SRC%\FactionRelationshipsKeybindScript.java" if errorlevel 1 ( echo Compilation failed. exit /b 1 @@ -35,9 +47,10 @@ if errorlevel 1 ( exit /b 1 ) -echo Copying mod_info.json and config to package... +echo Copying mod_info.json, config, and data to package... copy /Y "mod_info.json" "..\%PKG%\mod_info.json" >nul if exist "config" xcopy /E /I /Y "config" "..\%PKG%\config" >nul +if exist "data" xcopy /E /I /Y "data" "..\%PKG%\data" >nul echo Creating %PKG%.zip... powershell -NoProfile -Command "Compress-Archive -Path '..\%PKG%' -DestinationPath '..\%PKG%.zip' -Force" diff --git a/FactionRelationships/compile.local.example.bat b/FactionRelationships/compile.local.example.bat index cc09242..0fc02a8 100644 --- a/FactionRelationships/compile.local.example.bat +++ b/FactionRelationships/compile.local.example.bat @@ -3,3 +3,7 @@ REM Copy this file to compile.local.bat and set your paths. REM compile.local.bat is gitignored and will be used by compile.bat when present. set GAME_DIR=YOUR_STARSECTOR_PATH + +REM Optional: if LunaLib/LazyLib use versioned folder names (e.g. LunaLib-2.0.5), set these so the build finds the JARs: +REM set LUNALIB=%GAME_DIR%\mods\LunaLib-2.0.5\jars\LunaLib.jar +REM set LAZYLIB=%GAME_DIR%\mods\LazyLib-3.0.0\jars\LazyLib.jar diff --git a/FactionRelationships/data/config/LunaSettings.csv b/FactionRelationships/data/config/LunaSettings.csv new file mode 100644 index 0000000..eab5910 --- /dev/null +++ b/FactionRelationships/data/config/LunaSettings.csv @@ -0,0 +1,7 @@ +fieldID,fieldName,fieldType,defaultValue,secondaryValue,fieldDescription,minValue,maxValue,tab +factionRelationshipsHeader,Faction Relationships,Header,Faction Relationships,,,,,General +maxFactions,Max Factions Shown,Int,15,,Maximum number of factions to display in the overlay (sorted by relationship).,1,50,General +showOnlyHostile,Show Only Hostile Factions,Boolean,false,,Only display factions with reputation -50 or worse (Hostile/Inhospitable). Helps focus on immediate threats.,,,General +textSize,Text Size,Radio,Medium,"Small,Medium,Large",Text size for the overlay. Use [Large] for 4K or high-DPI displays.,,,General +overlayKeybindMode,Overlay Keybind Mode,Radio,Toggle,"Toggle,Hold",[Toggle]: Press key to show or hide overlay. [Hold]: Overlay only visible while key is held.,,,General +toggleOverlayKeybind,Toggle Overlay Keybind,Keycode,0,,Key to show or hide the faction relationship overlay on the campaign map. Set in Mod Settings (F2). Keycode 0 = unbound.,,,General diff --git a/FactionRelationships/mod_info.json b/FactionRelationships/mod_info.json index 887013e..442b77a 100644 --- a/FactionRelationships/mod_info.json +++ b/FactionRelationships/mod_info.json @@ -2,9 +2,13 @@ "id": "factionrelationships", "name": "Faction Relationships", "author": "boop", - "version": "1.0.0", + "version": "1.3.1", "description": "Shows a list of factions and their relationship with the player on the main navigation screen.", "gameVersion": "0.98a-RC8", "jars": ["jars/FactionRelationships.jar"], - "modPlugin": "com.factionrelationships.FactionRelationshipsPlugin" + "modPlugin": "com.factionrelationships.FactionRelationshipsPlugin", + "dependencies": [ + {"id": "lw_lazylib", "name": "LazyLib"}, + {"id": "lunalib", "name": "LunaLib"} + ] } diff --git a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsKeybindScript.java b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsKeybindScript.java new file mode 100644 index 0000000..cccc144 --- /dev/null +++ b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsKeybindScript.java @@ -0,0 +1,49 @@ +package com.factionrelationships; + +import com.fs.starfarer.api.Global; +import com.fs.starfarer.api.EveryFrameScript; +import lunalib.lunaSettings.LunaSettings; +import org.lwjgl.input.Keyboard; + +/** + * Polls the LunaLib keybind. In Toggle mode: press to show/hide overlay (edge detection). + * In Hold mode: overlay visible only while key is held. + */ +public class FactionRelationshipsKeybindScript implements EveryFrameScript { + + private boolean keyWasDown; + + @Override + public boolean isDone() { + return false; + } + + @Override + public boolean runWhilePaused() { + return false; + } + + @Override + public void advance(float amount) { + if (!Global.getSettings().getModManager().isModEnabled("lunalib")) { + return; + } + Integer keycodeObj = LunaSettings.getInt("factionrelationships", "toggleOverlayKeybind"); + if (keycodeObj == null || keycodeObj.intValue() == 0) { + FactionRelationshipsPlugin.setOverlayKeyHeld(false); + return; + } + int keycode = keycodeObj.intValue(); + boolean keyDown = Keyboard.isKeyDown(keycode); + String mode = FactionRelationshipsPlugin.getOverlayKeybindMode(); + if ("Hold".equals(mode)) { + FactionRelationshipsPlugin.setOverlayKeyHeld(keyDown); + } else { + FactionRelationshipsPlugin.setOverlayKeyHeld(false); + if (!keyWasDown && keyDown) { + FactionRelationshipsPlugin.setOverlayVisible(!FactionRelationshipsPlugin.isOverlayVisible()); + } + } + keyWasDown = keyDown; + } +} diff --git a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsPlugin.java b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsPlugin.java index 8450ded..fc0cc61 100644 --- a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsPlugin.java +++ b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsPlugin.java @@ -2,23 +2,85 @@ import com.fs.starfarer.api.BaseModPlugin; import com.fs.starfarer.api.Global; +import lunalib.lunaSettings.LunaSettings; +import lunalib.lunaSettings.LunaSettingsListener; import org.apache.log4j.Logger; public class FactionRelationshipsPlugin extends BaseModPlugin { private static Logger log; + /** Overlay visibility toggle; read by renderer, written by keybind script (Toggle mode). Default true. */ + private static volatile boolean overlayVisible = true; + + /** Key currently held; used in Hold mode so overlay shows only while key is down. */ + private static volatile boolean overlayKeyHeld = false; + + /** Cached overlay keybind mode (Toggle vs Hold); invalidated when LunaSettings change. */ + private static volatile String cachedOverlayKeybindMode = null; + + public static boolean isOverlayVisible() { + return overlayVisible; + } + + public static void setOverlayVisible(boolean visible) { + overlayVisible = visible; + } + + public static boolean isOverlayKeyHeld() { + return overlayKeyHeld; + } + + public static void setOverlayKeyHeld(boolean held) { + overlayKeyHeld = held; + } + + /** Returns "Toggle" or "Hold"; default "Toggle" if unset or LunaLib disabled. */ + public static String getOverlayKeybindMode() { + if (cachedOverlayKeybindMode != null) { + return cachedOverlayKeybindMode; + } + String mode = "Toggle"; + if (Global.getSettings().getModManager().isModEnabled("lunalib")) { + String s = LunaSettings.getString("factionrelationships", "overlayKeybindMode"); + if (s != null && ("Hold".equalsIgnoreCase(s) || "Toggle".equalsIgnoreCase(s))) { + mode = "Hold".equalsIgnoreCase(s) ? "Hold" : "Toggle"; + } + } + cachedOverlayKeybindMode = mode; + return mode; + } + + /** Called when LunaSettings change so mode and other caches are re-read. */ + public static void invalidateSettingsCache() { + cachedOverlayKeybindMode = null; + } + @Override public void onApplicationLoad() throws Exception { log = Global.getLogger(FactionRelationshipsPlugin.class); log.info("Faction Relationships mod loaded."); + if (Global.getSettings().getModManager().isModEnabled("lunalib")) { + LunaSettings.addSettingsListener(new LunaSettingsListener() { + @Override + public void settingsChanged(String modID) { + if ("factionrelationships".equals(modID)) { + FactionRelationshipsPlugin.invalidateSettingsCache(); + FactionRelationshipsUIRenderer.invalidateSettingsCache(); + } + } + }); + } } @Override public void onGameLoad(boolean newGame) { Global.getSector().getListenerManager().addListener(new FactionRelationshipsUIRenderer(), true); + if (Global.getSettings().getModManager().isModEnabled("lunalib")) { + Global.getSector().addScript(new FactionRelationshipsKeybindScript()); + } if (log != null) { - log.info("Faction Relationships UI renderer registered."); + log.info("Faction Relationships UI renderer and keybind script registered."); } } } diff --git a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsUIRenderer.java b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsUIRenderer.java index f3c5568..7a47a28 100644 --- a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsUIRenderer.java +++ b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsUIRenderer.java @@ -10,12 +10,9 @@ import com.fs.starfarer.api.ui.Fonts; import com.fs.starfarer.api.ui.LabelAPI; import com.fs.starfarer.api.ui.PositionAPI; - -import org.json.JSONException; -import org.json.JSONObject; +import lunalib.lunaSettings.LunaSettings; import java.awt.Color; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -27,31 +24,94 @@ public class FactionRelationshipsUIRenderer implements CampaignUIRenderingListen private static final float X_PAD = 20f; private static final float Y_PAD = 80f; - private static final float LINE_HEIGHT = 14f; private static final int DEFAULT_MAX_FACTIONS = 15; private static final int MIN_MAX_FACTIONS = 1; private static final int MAX_MAX_FACTIONS = 50; - private static final String CONFIG_PATH = "config/faction_relationships_config.json"; private static final String MOD_ID = "factionrelationships"; + /** Reputation -50 in UI = -0.5f in API. */ + private static final float HOSTILE_THRESHOLD = -0.5f; + private static Integer cachedMaxFactions = null; + private static Boolean cachedShowOnlyHostile = null; + private static String cachedFont = null; + private static Float cachedLineHeight = null; + + public static void invalidateSettingsCache() { + cachedMaxFactions = null; + cachedShowOnlyHostile = null; + cachedFont = null; + cachedLineHeight = null; + } + + private static boolean getShowOnlyHostile() { + if (cachedShowOnlyHostile != null) { + return cachedShowOnlyHostile.booleanValue(); + } + boolean value = false; + if (Global.getSettings().getModManager().isModEnabled("lunalib")) { + Boolean v = LunaSettings.getBoolean(MOD_ID, "showOnlyHostile"); + if (v != null) { + value = v.booleanValue(); + } + } + cachedShowOnlyHostile = Boolean.valueOf(value); + return value; + } private static int getMaxFactions() { if (cachedMaxFactions != null) { return cachedMaxFactions.intValue(); } - try { - JSONObject config = Global.getSettings().loadJSON(CONFIG_PATH, MOD_ID); - int value = config.optInt("maxFactions", DEFAULT_MAX_FACTIONS); - cachedMaxFactions = Integer.valueOf(Math.max(MIN_MAX_FACTIONS, Math.min(MAX_MAX_FACTIONS, value))); - } catch (IOException e) { - cachedMaxFactions = Integer.valueOf(DEFAULT_MAX_FACTIONS); - } catch (JSONException e) { - cachedMaxFactions = Integer.valueOf(DEFAULT_MAX_FACTIONS); + int value = DEFAULT_MAX_FACTIONS; + if (Global.getSettings().getModManager().isModEnabled("lunalib")) { + Integer v = LunaSettings.getInt(MOD_ID, "maxFactions"); + if (v != null) { + value = v.intValue(); + } } + cachedMaxFactions = Integer.valueOf(Math.max(MIN_MAX_FACTIONS, Math.min(MAX_MAX_FACTIONS, value))); return cachedMaxFactions.intValue(); } + private static class FontAndLineHeight { + final String font; + final float lineHeight; + + FontAndLineHeight(String font, float lineHeight) { + this.font = font; + this.lineHeight = lineHeight; + } + } + + private static FontAndLineHeight getFontAndLineHeight() { + if (cachedFont != null && cachedLineHeight != null) { + return new FontAndLineHeight(cachedFont, cachedLineHeight.floatValue()); + } + String size = "Medium"; + if (Global.getSettings().getModManager().isModEnabled("lunalib")) { + String s = LunaSettings.getString(MOD_ID, "textSize"); + if (s != null && !s.isEmpty()) { + size = s; + } + } + String font = Fonts.VICTOR_10; + float lineHeight = 18f; + if ("Small".equalsIgnoreCase(size)) { + font = Fonts.VICTOR_10; + lineHeight = 14f; + } else if ("Large".equalsIgnoreCase(size)) { + font = Fonts.ORBITRON_20AA; + lineHeight = 24f; + } else { + font = Fonts.ORBITRON_12; + lineHeight = 18f; + } + cachedFont = font; + cachedLineHeight = Float.valueOf(lineHeight); + return new FontAndLineHeight(font, lineHeight); + } + @Override public void renderInUICoordsBelowUI(ViewportAPI viewport) { // no-op; draw above UI @@ -71,6 +131,13 @@ private void renderPanel(ViewportAPI viewport) { if (Global.getSector() == null || Global.getSector().getPlayerFleet() == null) { return; } + String mode = FactionRelationshipsPlugin.getOverlayKeybindMode(); + boolean showOverlay = "Hold".equals(mode) + ? FactionRelationshipsPlugin.isOverlayKeyHeld() + : FactionRelationshipsPlugin.isOverlayVisible(); + if (!showOverlay) { + return; + } Set factionIdsWithMarkets = new HashSet(); EconomyAPI economy = Global.getSector().getEconomy(); @@ -93,6 +160,16 @@ private void renderPanel(ViewportAPI viewport) { factions.add(faction); } + if (getShowOnlyHostile()) { + List hostileOnly = new ArrayList(); + for (FactionAPI faction : factions) { + if (faction.getRelToPlayer().getRel() <= HOSTILE_THRESHOLD) { + hostileOnly.add(faction); + } + } + factions = hostileOnly; + } + Collections.sort(factions, new Comparator() { @Override public int compare(FactionAPI a, FactionAPI b) { @@ -110,6 +187,8 @@ public int compare(FactionAPI a, FactionAPI b) { float screenW = Global.getSettings().getScreenWidth(); float screenH = Global.getSettings().getScreenHeight(); + FontAndLineHeight fontAndLine = getFontAndLineHeight(); + float lineHeight = fontAndLine.lineHeight; List labels = new ArrayList(); for (int i = 0; i < count; i++) { @@ -120,7 +199,7 @@ public int compare(FactionAPI a, FactionAPI b) { String line = faction.getDisplayName() + " " + formatRepValue(repValue) + " " + levelName; Color color = rel.getRelColor(); - LabelAPI label = Global.getSettings().createLabel(line, Fonts.VICTOR_10); + LabelAPI label = Global.getSettings().createLabel(line, fontAndLine.font); label.setColor(color); labels.add(label); } @@ -132,8 +211,8 @@ public int compare(FactionAPI a, FactionAPI b) { if (i == 0) { pos.inTR(X_PAD, Y_PAD); } else { - float y = Y_PAD + i * LINE_HEIGHT; - pos.setLocation(screenW - X_PAD - w, screenH - y - LINE_HEIGHT); + float y = Y_PAD + i * lineHeight; + pos.setLocation(screenW - X_PAD - w, screenH - y - lineHeight); } label.render(1f); } diff --git a/README.md b/README.md index b3b5218..2b278d9 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,14 @@ A [Starsector](https://fractalsoftworks.com/) mod that shows a list of factions - **Game version**: 0.98a-RC8 - **Mod ID**: `factionrelationships` +- **Dependencies**: [LazyLib](https://fractalsoftworks.com/forum/index.php?topic=12771.0) and [LunaLib](https://github.com/Lukas22041/LunaLib) (required; enable them in the launcher). ## Installation -1. Download the latest release (or build from source; see below). -2. Extract the mod folder into your Starsector `mods` directory. -3. Enable the mod in the game launcher. +1. Install **LazyLib** and **LunaLib** in your Starsector `mods` folder if you have not already. +2. Download the latest release (or build from source; see below). +3. Extract the mod folder into your Starsector `mods` directory. +4. Enable the mod (and LazyLib, LunaLib) in the game launcher. Your `mods` folder should contain something like: @@ -19,20 +21,23 @@ Your `mods` folder should contain something like: mods/FactionRelationships/ ├── mod_info.json ├── config/ -│ └── faction_relationships_config.json +│ └── faction_relationships_config.json # deprecated; settings via Mod Settings +├── data/ +│ └── config/ +│ └── LunaSettings.csv └── jars/ └── FactionRelationships.jar ``` ## Configuration -Edit `config/faction_relationships_config.json` to set how many factions are shown. The `maxFactions` value can be set between 1 and 50 (default is 15). +Configure the mod in-game via **Mod Settings** (press **F2** in campaign): max factions shown, text size, overlay keybind (toggle or hold-to-view), and optional “show only hostile factions” filter. No need to edit JSON files. ## Building from source -- **Requirements**: JDK 17, a Starsector 0.98a install (for API JARs). +- **Requirements**: JDK 17, a Starsector 0.98a install (for API JARs), and **LunaLib** and **LazyLib** installed in your game `mods` folder (the build script needs their JARs). - **Build**: From the repo root, run `FactionRelationships\compile.bat`. - You may need to set `GAME_DIR` inside the script to your Starsector path. + Copy `FactionRelationships\compile.local.example.bat` to `FactionRelationships\compile.local.bat` and set `GAME_DIR` to your Starsector path (and optionally `LUNALIB` / `LAZYLIB` if you use versioned mod folders). - **Details**: See [FactionRelationships/COMPILATION.md](FactionRelationships/COMPILATION.md) for full build steps, project layout, and manual compile commands. ## License From 9b77d189bc18d3aa7e6fe5f2e2d84aaaacf05c22 Mon Sep 17 00:00:00 2001 From: iHateCode <6393143+iHateCode@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:58:49 +0000 Subject: [PATCH 2/4] Add relationship change overlay, LunaLib settings, and intel - LunaSettings.csv and overlay options (position, auto-hide, etc.) - Faction relationship change listener and RelationshipChangeStore - SystemFactionRelationshipsIntel for relationship change intel - FactionRelationshipsCampaignInputListener for input handling - Plugin, UI renderer, and keybind updates for overlay Made-with: Cursor --- FactionRelationships/COMPILATION.md | 10 ++- FactionRelationships/compile.bat | 2 +- .../data/config/LunaSettings.csv | 3 + FactionRelationships/mod_info.json | 2 +- .../FactionRelationshipChangeListener.java | 59 ++++++++++++++++ ...ionRelationshipsCampaignInputListener.java | 70 +++++++++++++++++++ .../FactionRelationshipsKeybindScript.java | 32 ++------- .../FactionRelationshipsPlugin.java | 9 ++- .../FactionRelationshipsUIRenderer.java | 65 +++++++++++++++-- .../RelationshipChangeStore.java | 62 ++++++++++++++++ .../SystemFactionRelationshipsIntel.java | 26 +++++++ README.md | 2 +- 12 files changed, 297 insertions(+), 45 deletions(-) create mode 100644 FactionRelationships/src/com/factionrelationships/FactionRelationshipChangeListener.java create mode 100644 FactionRelationships/src/com/factionrelationships/FactionRelationshipsCampaignInputListener.java create mode 100644 FactionRelationships/src/com/factionrelationships/RelationshipChangeStore.java create mode 100644 FactionRelationships/src/com/factionrelationships/SystemFactionRelationshipsIntel.java diff --git a/FactionRelationships/COMPILATION.md b/FactionRelationships/COMPILATION.md index bb49451..4a84320 100644 --- a/FactionRelationships/COMPILATION.md +++ b/FactionRelationships/COMPILATION.md @@ -39,7 +39,11 @@ FactionRelationships/ │ └── com/factionrelationships/ │ ├── FactionRelationshipsPlugin.java │ ├── FactionRelationshipsUIRenderer.java -│ └── FactionRelationshipsKeybindScript.java +│ ├── FactionRelationshipsCampaignInputListener.java +│ ├── FactionRelationshipsKeybindScript.java # stub for save compatibility +│ ├── FactionRelationshipChangeListener.java +│ ├── RelationshipChangeStore.java +│ └── SystemFactionRelationshipsIntel.java ├── compile.local.example.bat # copy to compile.local.bat (gitignored) to set GAME_DIR └── COMPILATION.md ``` @@ -62,7 +66,7 @@ set LUNALIB=%GAME_DIR%\mods\LunaLib\jars\LunaLib.jar set LAZYLIB=%GAME_DIR%\mods\LazyLib\jars\LazyLib.jar set CORE=%GAME_DIR%\starsector-core set VERSION=1.3.0 -javac -encoding UTF-8 -cp "%CORE%\starfarer.api.jar;%CORE%\starfarer_obf.jar;%CORE%\json.jar;%CORE%\log4j-1.2.9.jar;%CORE%\lwjgl.jar;%CORE%\lwjgl_util.jar;%LAZYLIB%;%LUNALIB%" -d FactionRelationships-%VERSION%\classes FactionRelationships\src\com\factionrelationships\FactionRelationshipsPlugin.java FactionRelationships\src\com\factionrelationships\FactionRelationshipsUIRenderer.java FactionRelationships\src\com\factionrelationships\FactionRelationshipsKeybindScript.java +javac -encoding UTF-8 -cp "%CORE%\starfarer.api.jar;%CORE%\starfarer_obf.jar;%CORE%\json.jar;%CORE%\log4j-1.2.9.jar;%CORE%\lwjgl.jar;%CORE%\lwjgl_util.jar;%LAZYLIB%;%LUNALIB%" -d FactionRelationships-%VERSION%\classes FactionRelationships\src\com\factionrelationships\FactionRelationshipsPlugin.java FactionRelationships\src\com\factionrelationships\FactionRelationshipsUIRenderer.java FactionRelationships\src\com\factionrelationships\FactionRelationshipsCampaignInputListener.java FactionRelationships\src\com\factionrelationships\FactionRelationshipsKeybindScript.java FactionRelationships\src\com\factionrelationships\FactionRelationshipChangeListener.java FactionRelationships\src\com\factionrelationships\RelationshipChangeStore.java FactionRelationships\src\com\factionrelationships\SystemFactionRelationshipsIntel.java jar cvf FactionRelationships-%VERSION%\jars\FactionRelationships.jar -C FactionRelationships-%VERSION%\classes . ``` @@ -92,7 +96,7 @@ Set `GAME_DIR` (and optionally `LUNALIB`, `LAZYLIB`) before running (e.g. via `c └── FactionRelationships.jar ``` - Configure max factions, text size, overlay keybind (toggle or hold), and hostile-only filter in **Mod Settings** (F2 in campaign). + Configure max factions, text size, overlay keybind (toggle or hold), hostile-only filter, relationship-change display, and auto-show overlay on change in **Mod Settings** (F2 in campaign). ## Quick Reference diff --git a/FactionRelationships/compile.bat b/FactionRelationships/compile.bat index b0ffbf4..59aeb83 100644 --- a/FactionRelationships/compile.bat +++ b/FactionRelationships/compile.bat @@ -34,7 +34,7 @@ if not exist "%OUT%" mkdir "%OUT%" if not exist "..\%PKG%\jars" mkdir "..\%PKG%\jars" echo Compiling... -javac -encoding UTF-8 -cp "%CORE%\starfarer.api.jar;%CORE%\starfarer_obf.jar;%CORE%\json.jar;%CORE%\log4j-1.2.9.jar;%CORE%\lwjgl.jar;%CORE%\lwjgl_util.jar;%LAZYLIB%;%LUNALIB%" -d "%OUT%" "%SRC%\FactionRelationshipsPlugin.java" "%SRC%\FactionRelationshipsUIRenderer.java" "%SRC%\FactionRelationshipsKeybindScript.java" +javac -encoding UTF-8 -cp "%CORE%\starfarer.api.jar;%CORE%\starfarer_obf.jar;%CORE%\json.jar;%CORE%\log4j-1.2.9.jar;%CORE%\lwjgl.jar;%CORE%\lwjgl_util.jar;%LAZYLIB%;%LUNALIB%" -d "%OUT%" "%SRC%\FactionRelationshipsPlugin.java" "%SRC%\FactionRelationshipsUIRenderer.java" "%SRC%\FactionRelationshipsCampaignInputListener.java" "%SRC%\FactionRelationshipsKeybindScript.java" "%SRC%\FactionRelationshipChangeListener.java" "%SRC%\RelationshipChangeStore.java" "%SRC%\SystemFactionRelationshipsIntel.java" if errorlevel 1 ( echo Compilation failed. exit /b 1 diff --git a/FactionRelationships/data/config/LunaSettings.csv b/FactionRelationships/data/config/LunaSettings.csv index eab5910..8369bb0 100644 --- a/FactionRelationships/data/config/LunaSettings.csv +++ b/FactionRelationships/data/config/LunaSettings.csv @@ -5,3 +5,6 @@ showOnlyHostile,Show Only Hostile Factions,Boolean,false,,Only display factions textSize,Text Size,Radio,Medium,"Small,Medium,Large",Text size for the overlay. Use [Large] for 4K or high-DPI displays.,,,General overlayKeybindMode,Overlay Keybind Mode,Radio,Toggle,"Toggle,Hold",[Toggle]: Press key to show or hide overlay. [Hold]: Overlay only visible while key is held.,,,General toggleOverlayKeybind,Toggle Overlay Keybind,Keycode,0,,Key to show or hide the faction relationship overlay on the campaign map. Set in Mod Settings (F2). Keycode 0 = unbound.,,,General +showRelationshipChangeInOverlay,Show Relationship Change in Overlay,Boolean,true,,"When a faction relationship changes, show the change (e.g. +5 or -10) next to that faction for the duration set below.",,,General +relationshipChangeDisplaySeconds,Relationship Change Display (seconds),Int,30,,"How long to show the change next to a faction in the overlay, in seconds.",5,120,General +autoShowOverlayOnRelationshipChange,Auto-Show Overlay When Relationship Changes,Boolean,false,,"When a faction relationship changes, automatically show the overlay (for the duration set above).",,,General diff --git a/FactionRelationships/mod_info.json b/FactionRelationships/mod_info.json index 442b77a..8db6096 100644 --- a/FactionRelationships/mod_info.json +++ b/FactionRelationships/mod_info.json @@ -2,7 +2,7 @@ "id": "factionrelationships", "name": "Faction Relationships", "author": "boop", - "version": "1.3.1", + "version": "1.4.6", "description": "Shows a list of factions and their relationship with the player on the main navigation screen.", "gameVersion": "0.98a-RC8", "jars": ["jars/FactionRelationships.jar"], diff --git a/FactionRelationships/src/com/factionrelationships/FactionRelationshipChangeListener.java b/FactionRelationships/src/com/factionrelationships/FactionRelationshipChangeListener.java new file mode 100644 index 0000000..2dbbd07 --- /dev/null +++ b/FactionRelationships/src/com/factionrelationships/FactionRelationshipChangeListener.java @@ -0,0 +1,59 @@ +package com.factionrelationships; + +import com.fs.starfarer.api.Global; +import com.fs.starfarer.api.campaign.BaseCampaignEventListener; +import com.fs.starfarer.api.characters.PersonAPI; +import lunalib.lunaSettings.LunaSettings; + +/** + * Listens for faction relationship changes and records them for the overlay. + * Registered as a transient listener in onGameLoad so it is not saved with the game. + */ +public class FactionRelationshipChangeListener extends BaseCampaignEventListener { + + private static final String MOD_ID = "factionrelationships"; + private static final String AUTO_SHOW_SETTING = "autoShowOverlayOnRelationshipChange"; + private static final String DISPLAY_SECONDS_SETTING = "relationshipChangeDisplaySeconds"; + private static final int DEFAULT_DISPLAY_SECONDS = 30; + private static final int MIN_DISPLAY_SECONDS = 5; + private static final int MAX_DISPLAY_SECONDS = 120; + + public FactionRelationshipChangeListener() { + super(false); + } + + private static int getDisplayDurationSeconds() { + if (!Global.getSettings().getModManager().isModEnabled("lunalib")) { + return DEFAULT_DISPLAY_SECONDS; + } + Integer v = LunaSettings.getInt(MOD_ID, DISPLAY_SECONDS_SETTING); + if (v == null) { + return DEFAULT_DISPLAY_SECONDS; + } + int sec = v.intValue(); + if (sec < MIN_DISPLAY_SECONDS) sec = MIN_DISPLAY_SECONDS; + if (sec > MAX_DISPLAY_SECONDS) sec = MAX_DISPLAY_SECONDS; + return sec; + } + + @Override + public void reportPlayerReputationChange(String factionId, float delta) { + long durationMs = getDisplayDurationSeconds() * 1000L; + RelationshipChangeStore.record(factionId, delta, durationMs); + if (Global.getSettings().getModManager().isModEnabled("lunalib")) { + Boolean autoShow = LunaSettings.getBoolean(MOD_ID, AUTO_SHOW_SETTING); + if (Boolean.TRUE.equals(autoShow)) { + FactionRelationshipsPlugin.setOverlayVisible(true); + RelationshipChangeStore.setAutoShowExpiry(System.currentTimeMillis() + durationMs); + } + } + } + + @Override + public void reportPlayerReputationChange(PersonAPI person, float delta) { + if (person == null || person.getFaction() == null) { + return; + } + reportPlayerReputationChange(person.getFaction().getId(), delta); + } +} diff --git a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsCampaignInputListener.java b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsCampaignInputListener.java new file mode 100644 index 0000000..038c87e --- /dev/null +++ b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsCampaignInputListener.java @@ -0,0 +1,70 @@ +package com.factionrelationships; + +import com.fs.starfarer.api.Global; +import com.fs.starfarer.api.campaign.listeners.CampaignInputListener; +import com.fs.starfarer.api.input.InputEventAPI; +import lunalib.lunaSettings.LunaSettings; + +import java.util.List; + +/** + * Handles the overlay toggle keybind on the campaign map using the game's input + * event stream. This runs when the campaign is active (and while paused when + * registered with addListener(..., true)), so the key is detected reliably + * instead of polling {@code Keyboard.isKeyDown} in an EveryFrameScript. + *

+ * Pattern used by mods like Console Commands and Advanced Weapon Control: + * implement {@link CampaignInputListener} and handle keys in + * {@link #processCampaignInputPreCore(List)}. + */ +public class FactionRelationshipsCampaignInputListener implements CampaignInputListener { + + @Override + public int getListenerInputPriority() { + return 0; + } + + @Override + public void processCampaignInputPreCore(List events) { + if (!Global.getSettings().getModManager().isModEnabled("lunalib")) { + return; + } + Integer keycodeObj = LunaSettings.getInt("factionrelationships", "toggleOverlayKeybind"); + if (keycodeObj == null || keycodeObj.intValue() == 0) { + FactionRelationshipsPlugin.setOverlayKeyHeld(false); + return; + } + final int keycode = keycodeObj.intValue(); + final String mode = FactionRelationshipsPlugin.getOverlayKeybindMode(); + + for (InputEventAPI event : events) { + if (event.isConsumed()) { + continue; + } + if (event.isKeyDownEvent() && event.getEventValue() == keycode) { + RelationshipChangeStore.clearAutoShowExpiry(); + if ("Hold".equals(mode)) { + FactionRelationshipsPlugin.setOverlayKeyHeld(true); + } else { + FactionRelationshipsPlugin.setOverlayVisible(!FactionRelationshipsPlugin.isOverlayVisible()); + } + event.consume(); + break; + } + if ("Hold".equals(mode) && event.isKeyUpEvent() && event.getEventValue() == keycode) { + RelationshipChangeStore.clearAutoShowExpiry(); + FactionRelationshipsPlugin.setOverlayKeyHeld(false); + event.consume(); + break; + } + } + } + + @Override + public void processCampaignInputPreFleetControl(List events) { + } + + @Override + public void processCampaignInputPostCore(List events) { + } +} diff --git a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsKeybindScript.java b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsKeybindScript.java index cccc144..c7268a1 100644 --- a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsKeybindScript.java +++ b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsKeybindScript.java @@ -1,21 +1,17 @@ package com.factionrelationships; -import com.fs.starfarer.api.Global; import com.fs.starfarer.api.EveryFrameScript; -import lunalib.lunaSettings.LunaSettings; -import org.lwjgl.input.Keyboard; /** - * Polls the LunaLib keybind. In Toggle mode: press to show/hide overlay (edge detection). - * In Hold mode: overlay visible only while key is held. + * Stub for save compatibility only. Old saves serialized this script in the + * campaign engine; this class allows them to load. The script reports itself + * done immediately so the engine removes it after load. */ public class FactionRelationshipsKeybindScript implements EveryFrameScript { - private boolean keyWasDown; - @Override public boolean isDone() { - return false; + return true; } @Override @@ -25,25 +21,5 @@ public boolean runWhilePaused() { @Override public void advance(float amount) { - if (!Global.getSettings().getModManager().isModEnabled("lunalib")) { - return; - } - Integer keycodeObj = LunaSettings.getInt("factionrelationships", "toggleOverlayKeybind"); - if (keycodeObj == null || keycodeObj.intValue() == 0) { - FactionRelationshipsPlugin.setOverlayKeyHeld(false); - return; - } - int keycode = keycodeObj.intValue(); - boolean keyDown = Keyboard.isKeyDown(keycode); - String mode = FactionRelationshipsPlugin.getOverlayKeybindMode(); - if ("Hold".equals(mode)) { - FactionRelationshipsPlugin.setOverlayKeyHeld(keyDown); - } else { - FactionRelationshipsPlugin.setOverlayKeyHeld(false); - if (!keyWasDown && keyDown) { - FactionRelationshipsPlugin.setOverlayVisible(!FactionRelationshipsPlugin.isOverlayVisible()); - } - } - keyWasDown = keyDown; } } diff --git a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsPlugin.java b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsPlugin.java index fc0cc61..205bed8 100644 --- a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsPlugin.java +++ b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsPlugin.java @@ -10,7 +10,7 @@ public class FactionRelationshipsPlugin extends BaseModPlugin { private static Logger log; - /** Overlay visibility toggle; read by renderer, written by keybind script (Toggle mode). Default true. */ + /** Overlay visibility toggle; read by renderer, written by campaign input listener (Toggle mode). Default true. */ private static volatile boolean overlayVisible = true; /** Key currently held; used in Hold mode so overlay shows only while key is down. */ @@ -76,11 +76,10 @@ public void settingsChanged(String modID) { @Override public void onGameLoad(boolean newGame) { Global.getSector().getListenerManager().addListener(new FactionRelationshipsUIRenderer(), true); - if (Global.getSettings().getModManager().isModEnabled("lunalib")) { - Global.getSector().addScript(new FactionRelationshipsKeybindScript()); - } + Global.getSector().getListenerManager().addListener(new FactionRelationshipsCampaignInputListener(), true); + Global.getSector().addTransientListener(new FactionRelationshipChangeListener()); if (log != null) { - log.info("Faction Relationships UI renderer and keybind script registered."); + log.info("Faction Relationships UI renderer, campaign input listener, and relationship change listener registered."); } } } diff --git a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsUIRenderer.java b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsUIRenderer.java index 7a47a28..1777de5 100644 --- a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsUIRenderer.java +++ b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsUIRenderer.java @@ -18,6 +18,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; public class FactionRelationshipsUIRenderer implements CampaignUIRenderingListener { @@ -34,16 +35,33 @@ public class FactionRelationshipsUIRenderer implements CampaignUIRenderingListen private static Integer cachedMaxFactions = null; private static Boolean cachedShowOnlyHostile = null; + private static Boolean cachedShowRelationshipChangeInOverlay = null; private static String cachedFont = null; private static Float cachedLineHeight = null; public static void invalidateSettingsCache() { cachedMaxFactions = null; cachedShowOnlyHostile = null; + cachedShowRelationshipChangeInOverlay = null; cachedFont = null; cachedLineHeight = null; } + private static boolean getShowRelationshipChangeInOverlay() { + if (cachedShowRelationshipChangeInOverlay != null) { + return cachedShowRelationshipChangeInOverlay.booleanValue(); + } + boolean value = true; + if (Global.getSettings().getModManager().isModEnabled("lunalib")) { + Boolean v = LunaSettings.getBoolean(MOD_ID, "showRelationshipChangeInOverlay"); + if (v != null) { + value = v.booleanValue(); + } + } + cachedShowRelationshipChangeInOverlay = Boolean.valueOf(value); + return value; + } + private static boolean getShowOnlyHostile() { if (cachedShowOnlyHostile != null) { return cachedShowOnlyHostile.booleanValue(); @@ -131,6 +149,13 @@ private void renderPanel(ViewportAPI viewport) { if (Global.getSector() == null || Global.getSector().getPlayerFleet() == null) { return; } + long autoShowExpiry = RelationshipChangeStore.getAutoShowExpiry(); + if (autoShowExpiry > 0 + && System.currentTimeMillis() >= autoShowExpiry + && FactionRelationshipsPlugin.isOverlayVisible()) { + FactionRelationshipsPlugin.setOverlayVisible(false); + RelationshipChangeStore.clearAutoShowExpiry(); + } String mode = FactionRelationshipsPlugin.getOverlayKeybindMode(); boolean showOverlay = "Hold".equals(mode) ? FactionRelationshipsPlugin.isOverlayKeyHeld() @@ -185,12 +210,17 @@ public int compare(FactionAPI a, FactionAPI b) { return; } + RelationshipChangeStore.removeExpired(); + Map recentChanges = RelationshipChangeStore.getRecentChanges(); + boolean showChangeInOverlay = getShowRelationshipChangeInOverlay(); + float screenW = Global.getSettings().getScreenWidth(); float screenH = Global.getSettings().getScreenHeight(); FontAndLineHeight fontAndLine = getFontAndLineHeight(); float lineHeight = fontAndLine.lineHeight; List labels = new ArrayList(); + List deltaLabels = new ArrayList(); for (int i = 0; i < count; i++) { FactionAPI faction = factions.get(i); RelationshipAPI rel = faction.getRelToPlayer(); @@ -202,20 +232,43 @@ public int compare(FactionAPI a, FactionAPI b) { LabelAPI label = Global.getSettings().createLabel(line, fontAndLine.font); label.setColor(color); labels.add(label); + + LabelAPI deltaLabel = null; + if (showChangeInOverlay) { + RelationshipChangeStore.RecentChange change = recentChanges.get(faction.getId()); + if (change != null) { + String deltaStr = formatDelta(change.delta); + deltaLabel = Global.getSettings().createLabel(deltaStr, fontAndLine.font); + deltaLabel.setColor(change.delta >= 0 ? new Color(100, 255, 100) : new Color(255, 100, 100)); + } + } + deltaLabels.add(deltaLabel); } for (int i = 0; i < labels.size(); i++) { LabelAPI label = labels.get(i); + LabelAPI deltaLabel = deltaLabels.get(i); float w = label.computeTextWidth(label.getText()); + float deltaW = (deltaLabel != null) ? deltaLabel.computeTextWidth(deltaLabel.getText()) + 4f : 0f; + float totalW = w + deltaW; + float y = screenH - (Y_PAD + (i + 1) * lineHeight); PositionAPI pos = label.getPosition(); - if (i == 0) { - pos.inTR(X_PAD, Y_PAD); - } else { - float y = Y_PAD + i * lineHeight; - pos.setLocation(screenW - X_PAD - w, screenH - y - lineHeight); - } + pos.setLocation(screenW - X_PAD - totalW, y); label.render(1f); + if (deltaLabel != null) { + PositionAPI deltaPos = deltaLabel.getPosition(); + deltaPos.setLocation(screenW - X_PAD - totalW + w, y); + deltaLabel.render(1f); + } + } + } + + private static String formatDelta(float delta) { + int pct = (int) Math.round(delta * 100f); + if (pct >= 0) { + return " (+" + pct + ")"; } + return " (" + pct + ")"; } private static String formatRepValue(float rel) { diff --git a/FactionRelationships/src/com/factionrelationships/RelationshipChangeStore.java b/FactionRelationships/src/com/factionrelationships/RelationshipChangeStore.java new file mode 100644 index 0000000..0560ffd --- /dev/null +++ b/FactionRelationships/src/com/factionrelationships/RelationshipChangeStore.java @@ -0,0 +1,62 @@ +package com.factionrelationships; + +import java.util.HashMap; +import java.util.Map; + +/** + * Holds recent faction relationship changes for UI display. + * Listener writes; UI renderer reads and prunes expired entries. + * All access is on the game thread. + */ +public final class RelationshipChangeStore { + + public static final class RecentChange { + public final float delta; + public final long expiryTimeMillis; + + public RecentChange(float delta, long expiryTimeMillis) { + this.delta = delta; + this.expiryTimeMillis = expiryTimeMillis; + } + } + + private static final Map RECENT_CHANGES = new HashMap(); + + /** Expiry time for auto-shown overlay (0 = no auto-show expiry). */ + private static long autoShowOverlayUntilMillis = 0L; + + /** Set when the auto-shown overlay should be hidden. Call when auto-showing. */ + public static void setAutoShowExpiry(long untilMillis) { + autoShowOverlayUntilMillis = untilMillis; + } + + /** Current auto-show overlay expiry (0 if not set). */ + public static long getAutoShowExpiry() { + return autoShowOverlayUntilMillis; + } + + /** Clear auto-show expiry so we do not auto-hide. Call when user uses keybind or when we auto-hide. */ + public static void clearAutoShowExpiry() { + autoShowOverlayUntilMillis = 0L; + } + + /** Record a relationship change for a faction (overwrites any existing for that faction). Duration in milliseconds. */ + public static void record(String factionId, float delta, long durationMs) { + RECENT_CHANGES.put(factionId, new RecentChange(delta, System.currentTimeMillis() + durationMs)); + } + + /** Get the current map of recent changes (read-only view; caller may prune via removeExpired). */ + public static Map getRecentChanges() { + return RECENT_CHANGES; + } + + /** Remove entries that have passed their expiry time. Call from renderer when drawing. */ + public static void removeExpired() { + long now = System.currentTimeMillis(); + for (java.util.Iterator> it = RECENT_CHANGES.entrySet().iterator(); it.hasNext(); ) { + if (it.next().getValue().expiryTimeMillis < now) { + it.remove(); + } + } + } +} diff --git a/FactionRelationships/src/com/factionrelationships/SystemFactionRelationshipsIntel.java b/FactionRelationships/src/com/factionrelationships/SystemFactionRelationshipsIntel.java new file mode 100644 index 0000000..801d2c7 --- /dev/null +++ b/FactionRelationships/src/com/factionrelationships/SystemFactionRelationshipsIntel.java @@ -0,0 +1,26 @@ +package com.factionrelationships; + +import com.fs.starfarer.api.impl.campaign.intel.BaseIntelPlugin; + +/** + * Stub for save compatibility only. Old saves may reference this class in the + * IntelManager; this allows them to load. The intel ends immediately so it + * does not appear in the intel screen. + */ +public class SystemFactionRelationshipsIntel extends BaseIntelPlugin { + + public SystemFactionRelationshipsIntel() { + super(); + } + + @Override + public void advance(float amount) { + super.advance(amount); + endImmediately(); + } + + @Override + public String getSmallDescriptionTitle() { + return "Faction Relationships (legacy)"; + } +} diff --git a/README.md b/README.md index 2b278d9..3fcce07 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ mods/FactionRelationships/ ## Configuration -Configure the mod in-game via **Mod Settings** (press **F2** in campaign): max factions shown, text size, overlay keybind (toggle or hold-to-view), and optional “show only hostile factions” filter. No need to edit JSON files. +Configure the mod in-game via **Mod Settings** (press **F2** in campaign): max factions shown, text size, overlay keybind (toggle or hold-to-view), and optional “show only hostile factions” filter, relationship-change display in overlay (duration configurable, default 30 seconds), and optional auto-show overlay when a relationship changes (when enabled, the overlay auto-hides after the same configured duration). No need to edit JSON files. ## Building from source From c373abf8ddc1672e096837021ba365c6b3d49d63 Mon Sep 17 00:00:00 2001 From: iHateCode <6393143+iHateCode@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:01:26 +0000 Subject: [PATCH 3/4] chore: bump version to 1.4.7 Made-with: Cursor --- FactionRelationships/mod_info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FactionRelationships/mod_info.json b/FactionRelationships/mod_info.json index 8db6096..9600217 100644 --- a/FactionRelationships/mod_info.json +++ b/FactionRelationships/mod_info.json @@ -2,7 +2,7 @@ "id": "factionrelationships", "name": "Faction Relationships", "author": "boop", - "version": "1.4.6", + "version": "1.4.7", "description": "Shows a list of factions and their relationship with the player on the main navigation screen.", "gameVersion": "0.98a-RC8", "jars": ["jars/FactionRelationships.jar"], From 96e568bd8b65a514e80ff32f69235009162b537f Mon Sep 17 00:00:00 2001 From: iHateCode <6393143+iHateCode@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:17:38 +0000 Subject: [PATCH 4/4] refactor: centralize mod ID, LunaLib check, and percent formatting - Add MOD_ID and isLunaLibEnabled() in FactionRelationshipsPlugin; use everywhere - Replace 9 LunaLib isModEnabled('lunalib') calls with isLunaLibEnabled() - Add PERCENT_FACTOR and formatPercent() in UIRenderer; refactor formatRepValue/formatDelta - Bump version to 1.4.8 Made-with: Cursor --- FactionRelationships/mod_info.json | 2 +- .../FactionRelationshipChangeListener.java | 9 ++--- ...ionRelationshipsCampaignInputListener.java | 4 +- .../FactionRelationshipsPlugin.java | 16 ++++++-- .../FactionRelationshipsUIRenderer.java | 40 ++++++++++--------- 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/FactionRelationships/mod_info.json b/FactionRelationships/mod_info.json index 9600217..8772f47 100644 --- a/FactionRelationships/mod_info.json +++ b/FactionRelationships/mod_info.json @@ -2,7 +2,7 @@ "id": "factionrelationships", "name": "Faction Relationships", "author": "boop", - "version": "1.4.7", + "version": "1.4.8", "description": "Shows a list of factions and their relationship with the player on the main navigation screen.", "gameVersion": "0.98a-RC8", "jars": ["jars/FactionRelationships.jar"], diff --git a/FactionRelationships/src/com/factionrelationships/FactionRelationshipChangeListener.java b/FactionRelationships/src/com/factionrelationships/FactionRelationshipChangeListener.java index 2dbbd07..1abc778 100644 --- a/FactionRelationships/src/com/factionrelationships/FactionRelationshipChangeListener.java +++ b/FactionRelationships/src/com/factionrelationships/FactionRelationshipChangeListener.java @@ -11,7 +11,6 @@ */ public class FactionRelationshipChangeListener extends BaseCampaignEventListener { - private static final String MOD_ID = "factionrelationships"; private static final String AUTO_SHOW_SETTING = "autoShowOverlayOnRelationshipChange"; private static final String DISPLAY_SECONDS_SETTING = "relationshipChangeDisplaySeconds"; private static final int DEFAULT_DISPLAY_SECONDS = 30; @@ -23,10 +22,10 @@ public FactionRelationshipChangeListener() { } private static int getDisplayDurationSeconds() { - if (!Global.getSettings().getModManager().isModEnabled("lunalib")) { + if (!FactionRelationshipsPlugin.isLunaLibEnabled()) { return DEFAULT_DISPLAY_SECONDS; } - Integer v = LunaSettings.getInt(MOD_ID, DISPLAY_SECONDS_SETTING); + Integer v = LunaSettings.getInt(FactionRelationshipsPlugin.MOD_ID, DISPLAY_SECONDS_SETTING); if (v == null) { return DEFAULT_DISPLAY_SECONDS; } @@ -40,8 +39,8 @@ private static int getDisplayDurationSeconds() { public void reportPlayerReputationChange(String factionId, float delta) { long durationMs = getDisplayDurationSeconds() * 1000L; RelationshipChangeStore.record(factionId, delta, durationMs); - if (Global.getSettings().getModManager().isModEnabled("lunalib")) { - Boolean autoShow = LunaSettings.getBoolean(MOD_ID, AUTO_SHOW_SETTING); + if (FactionRelationshipsPlugin.isLunaLibEnabled()) { + Boolean autoShow = LunaSettings.getBoolean(FactionRelationshipsPlugin.MOD_ID, AUTO_SHOW_SETTING); if (Boolean.TRUE.equals(autoShow)) { FactionRelationshipsPlugin.setOverlayVisible(true); RelationshipChangeStore.setAutoShowExpiry(System.currentTimeMillis() + durationMs); diff --git a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsCampaignInputListener.java b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsCampaignInputListener.java index 038c87e..3264ca4 100644 --- a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsCampaignInputListener.java +++ b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsCampaignInputListener.java @@ -26,10 +26,10 @@ public int getListenerInputPriority() { @Override public void processCampaignInputPreCore(List events) { - if (!Global.getSettings().getModManager().isModEnabled("lunalib")) { + if (!FactionRelationshipsPlugin.isLunaLibEnabled()) { return; } - Integer keycodeObj = LunaSettings.getInt("factionrelationships", "toggleOverlayKeybind"); + Integer keycodeObj = LunaSettings.getInt(FactionRelationshipsPlugin.MOD_ID, "toggleOverlayKeybind"); if (keycodeObj == null || keycodeObj.intValue() == 0) { FactionRelationshipsPlugin.setOverlayKeyHeld(false); return; diff --git a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsPlugin.java b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsPlugin.java index 205bed8..7dc9607 100644 --- a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsPlugin.java +++ b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsPlugin.java @@ -8,6 +8,9 @@ public class FactionRelationshipsPlugin extends BaseModPlugin { + /** Mod ID for LunaSettings and mod manager; must match mod_info.json "id". */ + public static final String MOD_ID = "factionrelationships"; + private static Logger log; /** Overlay visibility toggle; read by renderer, written by campaign input listener (Toggle mode). Default true. */ @@ -41,8 +44,8 @@ public static String getOverlayKeybindMode() { return cachedOverlayKeybindMode; } String mode = "Toggle"; - if (Global.getSettings().getModManager().isModEnabled("lunalib")) { - String s = LunaSettings.getString("factionrelationships", "overlayKeybindMode"); + if (FactionRelationshipsPlugin.isLunaLibEnabled()) { + String s = LunaSettings.getString(MOD_ID, "overlayKeybindMode"); if (s != null && ("Hold".equalsIgnoreCase(s) || "Toggle".equalsIgnoreCase(s))) { mode = "Hold".equalsIgnoreCase(s) ? "Hold" : "Toggle"; } @@ -51,6 +54,11 @@ public static String getOverlayKeybindMode() { return mode; } + /** Whether LunaLib is enabled; used for settings and keybinds. */ + public static boolean isLunaLibEnabled() { + return Global.getSettings().getModManager().isModEnabled("lunalib"); + } + /** Called when LunaSettings change so mode and other caches are re-read. */ public static void invalidateSettingsCache() { cachedOverlayKeybindMode = null; @@ -60,11 +68,11 @@ public static void invalidateSettingsCache() { public void onApplicationLoad() throws Exception { log = Global.getLogger(FactionRelationshipsPlugin.class); log.info("Faction Relationships mod loaded."); - if (Global.getSettings().getModManager().isModEnabled("lunalib")) { + if (FactionRelationshipsPlugin.isLunaLibEnabled()) { LunaSettings.addSettingsListener(new LunaSettingsListener() { @Override public void settingsChanged(String modID) { - if ("factionrelationships".equals(modID)) { + if (MOD_ID.equals(modID)) { FactionRelationshipsPlugin.invalidateSettingsCache(); FactionRelationshipsUIRenderer.invalidateSettingsCache(); } diff --git a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsUIRenderer.java b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsUIRenderer.java index 1777de5..ac10131 100644 --- a/FactionRelationships/src/com/factionrelationships/FactionRelationshipsUIRenderer.java +++ b/FactionRelationships/src/com/factionrelationships/FactionRelationshipsUIRenderer.java @@ -28,7 +28,8 @@ public class FactionRelationshipsUIRenderer implements CampaignUIRenderingListen private static final int DEFAULT_MAX_FACTIONS = 15; private static final int MIN_MAX_FACTIONS = 1; private static final int MAX_MAX_FACTIONS = 50; - private static final String MOD_ID = "factionrelationships"; + /** Factor to convert relationship value (-1..1) to display percentage. */ + private static final float PERCENT_FACTOR = 100f; /** Reputation -50 in UI = -0.5f in API. */ private static final float HOSTILE_THRESHOLD = -0.5f; @@ -52,8 +53,8 @@ private static boolean getShowRelationshipChangeInOverlay() { return cachedShowRelationshipChangeInOverlay.booleanValue(); } boolean value = true; - if (Global.getSettings().getModManager().isModEnabled("lunalib")) { - Boolean v = LunaSettings.getBoolean(MOD_ID, "showRelationshipChangeInOverlay"); + if (FactionRelationshipsPlugin.isLunaLibEnabled()) { + Boolean v = LunaSettings.getBoolean(FactionRelationshipsPlugin.MOD_ID, "showRelationshipChangeInOverlay"); if (v != null) { value = v.booleanValue(); } @@ -67,8 +68,8 @@ private static boolean getShowOnlyHostile() { return cachedShowOnlyHostile.booleanValue(); } boolean value = false; - if (Global.getSettings().getModManager().isModEnabled("lunalib")) { - Boolean v = LunaSettings.getBoolean(MOD_ID, "showOnlyHostile"); + if (FactionRelationshipsPlugin.isLunaLibEnabled()) { + Boolean v = LunaSettings.getBoolean(FactionRelationshipsPlugin.MOD_ID, "showOnlyHostile"); if (v != null) { value = v.booleanValue(); } @@ -82,8 +83,8 @@ private static int getMaxFactions() { return cachedMaxFactions.intValue(); } int value = DEFAULT_MAX_FACTIONS; - if (Global.getSettings().getModManager().isModEnabled("lunalib")) { - Integer v = LunaSettings.getInt(MOD_ID, "maxFactions"); + if (FactionRelationshipsPlugin.isLunaLibEnabled()) { + Integer v = LunaSettings.getInt(FactionRelationshipsPlugin.MOD_ID, "maxFactions"); if (v != null) { value = v.intValue(); } @@ -107,8 +108,8 @@ private static FontAndLineHeight getFontAndLineHeight() { return new FontAndLineHeight(cachedFont, cachedLineHeight.floatValue()); } String size = "Medium"; - if (Global.getSettings().getModManager().isModEnabled("lunalib")) { - String s = LunaSettings.getString(MOD_ID, "textSize"); + if (FactionRelationshipsPlugin.isLunaLibEnabled()) { + String s = LunaSettings.getString(FactionRelationshipsPlugin.MOD_ID, "textSize"); if (s != null && !s.isEmpty()) { size = s; } @@ -263,19 +264,20 @@ public int compare(FactionAPI a, FactionAPI b) { } } - private static String formatDelta(float delta) { - int pct = (int) Math.round(delta * 100f); - if (pct >= 0) { - return " (+" + pct + ")"; + private static String formatPercent(float value, boolean withPlus, boolean withParens) { + int pct = (int) Math.round(value * PERCENT_FACTOR); + String num = (withPlus && pct >= 0) ? ("+" + pct) : String.valueOf(pct); + if (withParens) { + return " (" + num + ")"; } - return " (" + pct + ")"; + return num; + } + + private static String formatDelta(float delta) { + return " " + formatPercent(delta, true, true); } private static String formatRepValue(float rel) { - int pct = (int) Math.round(rel * 100f); - if (pct >= 0) { - return "+" + pct; - } - return String.valueOf(pct); + return formatPercent(rel, true, false); } }