From 13e6437f4ac9475c8330ed2d13f3e121b6b57fd8 Mon Sep 17 00:00:00 2001
From: Des Herriott <des.herriott@gmail.com>
Date: Wed, 14 Aug 2024 16:37:11 +0100
Subject: [PATCH 1/5] fix: hide until deps visible/completed behaviour

Fixed incorrect current behaviour: "Hide quest until deps visible"
actually requires dependency completion, not visibility

Added new "Hide quest until deps completed" setting

https://github.com/FTBTeam/FTB-Mods-Issues/issues/1306
---
 .../dev/ftb/mods/ftbquests/quest/Chapter.java | 17 +++++--
 .../dev/ftb/mods/ftbquests/quest/Quest.java   | 49 +++++++++++++++----
 .../ftb/mods/ftbquests/quest/TeamData.java    | 43 ++++++++--------
 .../assets/ftbquests/lang/en_us.json          |  5 +-
 4 files changed, 80 insertions(+), 34 deletions(-)

diff --git a/common/src/main/java/dev/ftb/mods/ftbquests/quest/Chapter.java b/common/src/main/java/dev/ftb/mods/ftbquests/quest/Chapter.java
index b46d88f3..33a305ab 100644
--- a/common/src/main/java/dev/ftb/mods/ftbquests/quest/Chapter.java
+++ b/common/src/main/java/dev/ftb/mods/ftbquests/quest/Chapter.java
@@ -42,6 +42,7 @@ public final class Chapter extends QuestObject {
 	private int defaultMinWidth = 0;
 	private ProgressionMode progressionMode;
 	private boolean hideQuestDetailsUntilStartable;
+	private boolean hideQuestUntilDepsComplete;
 	private boolean hideQuestUntilDepsVisible;
 	private boolean defaultRepeatable;
 	private Tristate consumeItems;
@@ -68,6 +69,7 @@ public Chapter(long id, BaseQuestFile file, ChapterGroup group, String filename)
 		defaultHideDependencyLines = false;
 		progressionMode = ProgressionMode.DEFAULT;
 		hideQuestUntilDepsVisible = false;
+		hideQuestUntilDepsComplete = false;
 		hideQuestDetailsUntilStartable = false;
 		defaultRepeatable = false;
 		consumeItems = Tristate.DEFAULT;
@@ -187,6 +189,7 @@ public void writeData(CompoundTag nbt) {
 
 		if (hideQuestDetailsUntilStartable) nbt.putBoolean("hide_quest_details_until_startable", true);
 		if (hideQuestUntilDepsVisible) nbt.putBoolean("hide_quest_until_deps_visible", true);
+		if (hideQuestUntilDepsComplete) nbt.putBoolean("hide_quest_until_deps_complete", true);
 		if (defaultRepeatable) nbt.putBoolean("default_repeatable_quest", true);
 		if (requireSequentialTasks) nbt.putBoolean("require_sequential_tasks", true);
 
@@ -233,6 +236,7 @@ public void readData(CompoundTag nbt) {
 		consumeItems = Tristate.read(nbt, "consume_items");
 		hideQuestDetailsUntilStartable = nbt.getBoolean("hide_quest_details_until_startable");
 		hideQuestUntilDepsVisible = nbt.getBoolean("hide_quest_until_deps_visible");
+		hideQuestUntilDepsComplete = nbt.getBoolean("hide_quest_until_deps_complete");
 		defaultRepeatable = nbt.getBoolean("default_repeatable_quest");
 		requireSequentialTasks = nbt.getBoolean("require_sequential_tasks");
 		autoFocusId = nbt.getString("autofocus_id");
@@ -253,12 +257,13 @@ public void writeNetData(FriendlyByteBuf buffer) {
 		flags = Bits.setFlag(flags, 0x01, alwaysInvisible);
 		flags = Bits.setFlag(flags, 0x02, defaultHideDependencyLines);
 		flags = Bits.setFlag(flags, 0x04, hideQuestDetailsUntilStartable);
-		flags = Bits.setFlag(flags, 0x08, hideQuestUntilDepsVisible);
+		flags = Bits.setFlag(flags, 0x08, hideQuestUntilDepsComplete);
 		flags = Bits.setFlag(flags, 0x10, defaultRepeatable);
 		flags = Bits.setFlag(flags, 0x20, consumeItems != Tristate.DEFAULT);
 		flags = Bits.setFlag(flags, 0x40, consumeItems == Tristate.TRUE);
 		flags = Bits.setFlag(flags, 0x80, requireSequentialTasks);
 		flags = Bits.setFlag(flags, 0x100, !autoFocusId.isEmpty());
+		flags = Bits.setFlag(flags, 0x200, hideQuestUntilDepsVisible);
 		buffer.writeVarInt(flags);
 
 		if (!autoFocusId.isEmpty()) buffer.writeLong(QuestObjectBase.parseHexId(autoFocusId).orElse(0L));
@@ -283,10 +288,11 @@ public void readNetData(FriendlyByteBuf buffer) {
 		alwaysInvisible = Bits.getFlag(flags, 0x01);
 		defaultHideDependencyLines = Bits.getFlag(flags, 0x02);
 		hideQuestDetailsUntilStartable = Bits.getFlag(flags, 0x04);
-		hideQuestUntilDepsVisible = Bits.getFlag(flags, 0x08);
+		hideQuestUntilDepsComplete = Bits.getFlag(flags, 0x08);
 		defaultRepeatable = Bits.getFlag(flags, 0x10);
 		consumeItems = Bits.getFlag(flags, 0x20) ? Bits.getFlag(flags, 0x40) ? Tristate.TRUE : Tristate.FALSE : Tristate.DEFAULT;
 		requireSequentialTasks = Bits.getFlag(flags, 0x80);
+		hideQuestUntilDepsVisible = Bits.getFlag(flags, 0x200);
 
 		autoFocusId = Bits.getFlag(flags, 0x100) ? QuestObjectBase.getCodeString(buffer.readLong()) : "";
 	}
@@ -441,6 +447,7 @@ public void fillConfigGroup(ConfigGroup config) {
 		visibility.addBool("default_hide_dependency_lines", defaultHideDependencyLines, v -> defaultHideDependencyLines = v, false);
 		visibility.addBool("hide_quest_details_until_startable", hideQuestDetailsUntilStartable, v -> hideQuestDetailsUntilStartable = v, false);
 		visibility.addBool("hide_quest_until_deps_visible", hideQuestUntilDepsVisible, v -> hideQuestUntilDepsVisible = v, false);
+		visibility.addBool("hide_quest_until_deps_complete", hideQuestUntilDepsComplete, v -> hideQuestUntilDepsComplete = v, false);
 
 		ConfigGroup misc = config.getOrCreateSubgroup("misc").setNameKey("ftbquests.quest.misc");
 		misc.addString("autofocus_id", autoFocusId, v -> autoFocusId = v, "", HEX_STRING);
@@ -513,7 +520,11 @@ public boolean hideQuestDetailsUntilStartable() {
 		return hideQuestDetailsUntilStartable;
 	}
 
-	public boolean hideQuestUntilDepsVisible() {
+	public boolean hideQuestUntilDepsComplete() {
+		return hideQuestUntilDepsComplete;
+	}
+
+	public boolean isHideQuestUntilDepsVisible() {
 		return hideQuestUntilDepsVisible;
 	}
 
diff --git a/common/src/main/java/dev/ftb/mods/ftbquests/quest/Quest.java b/common/src/main/java/dev/ftb/mods/ftbquests/quest/Quest.java
index b6656265..2362b4af 100644
--- a/common/src/main/java/dev/ftb/mods/ftbquests/quest/Quest.java
+++ b/common/src/main/java/dev/ftb/mods/ftbquests/quest/Quest.java
@@ -49,6 +49,7 @@ public final class Quest extends QuestObject implements Movable {
 	private String rawSubtitle;
 	private double x, y;
 	private Tristate hideUntilDepsVisible;
+	private Tristate hideUntilDepsComplete;
 	private String shape;
 	private final List<String> rawDescription;
 	private final List<QuestObject> dependencies;
@@ -94,6 +95,7 @@ public Quest(long id, Chapter chapter) {
 		hideDependencyLines = Tristate.DEFAULT;
 		hideDependentLines = false;
 		hideUntilDepsVisible = Tristate.DEFAULT;
+		hideUntilDepsComplete = Tristate.DEFAULT;
 		dependencyRequirement = DependencyRequirement.ALL_COMPLETED;
 		minRequiredDependencies = 0;
 		hideTextUntilComplete = Tristate.DEFAULT;
@@ -265,7 +267,8 @@ public void writeData(CompoundTag nbt) {
 			nbt.put("dependencies", deps);
 		}
 
-		hideUntilDepsVisible.write(nbt, "hide");
+		hideUntilDepsVisible.write(nbt, "hide_until_deps_visible");
+		hideUntilDepsComplete.write(nbt, "hide_until_deps_complete");
 
 		if (dependencyRequirement != DependencyRequirement.ALL_COMPLETED) {
 			nbt.putString("dependency_requirement", dependencyRequirement.getId());
@@ -357,7 +360,14 @@ public void readData(CompoundTag nbt) {
 			}
 		}
 
-		hideUntilDepsVisible = Tristate.read(nbt, "hide");
+		if (nbt.contains("hide", Tag.TAG_BYTE)) {
+			// TODO legacy; remove in 1.22
+            hideUntilDepsVisible = Tristate.read(nbt, "hide");
+        } else {
+			hideUntilDepsVisible = Tristate.read(nbt, "hide_until_deps_visible");
+		}
+		hideUntilDepsComplete = Tristate.read(nbt, "hide_until_deps_complete");
+
 		dependencyRequirement = DependencyRequirement.NAME_MAP.get(nbt.getString("dependency_requirement"));
 		hideTextUntilComplete = Tristate.read(nbt, "hide_text_until_complete");
 		size = nbt.getDouble("size");
@@ -398,6 +408,7 @@ public void writeNetData(FriendlyByteBuf buffer) {
 		flags = Bits.setFlag(flags, 0x20000, iconScale != 1f);
 		buffer.writeVarInt(flags);
 
+		hideUntilDepsComplete.write(buffer);
 		hideUntilDepsVisible.write(buffer);
 		hideDependencyLines.write(buffer);
 		hideTextUntilComplete.write(buffer);
@@ -449,6 +460,7 @@ public void writeNetData(FriendlyByteBuf buffer) {
 	public void readNetData(FriendlyByteBuf buffer) {
 		super.readNetData(buffer);
 		int flags = buffer.readVarInt();
+		hideUntilDepsComplete = Tristate.read(buffer);
 		hideUntilDepsVisible = Tristate.read(buffer);
 		hideDependencyLines = Tristate.read(buffer);
 		hideTextUntilComplete = Tristate.read(buffer);
@@ -668,7 +680,8 @@ public void onClicked(Widget clicked, MouseButton button, ConfigCallback callbac
 		appearance.addDouble("icon_scale", iconScale, v -> iconScale = v, 1f, 0.1, 2.0);
 
 		ConfigGroup visibility = config.getOrCreateSubgroup("visibility");
-		visibility.addTristate("hide", hideUntilDepsVisible, v -> hideUntilDepsVisible = v);
+		visibility.addTristate("hide_until_deps_complete", hideUntilDepsComplete, v -> hideUntilDepsComplete = v);
+		visibility.addTristate("hide_until_deps_visible", hideUntilDepsVisible, v -> hideUntilDepsVisible = v);
 		visibility.addBool("invisible", invisible, v -> invisible = v, false);
 		visibility.addInt("invisible_until_tasks", invisibleUntilTasks, v -> invisibleUntilTasks = v, 0, 0, Integer.MAX_VALUE).setCanEdit(invisible);
 		visibility.addTristate("hide_details_until_startable", hideDetailsUntilStartable, v -> hideDetailsUntilStartable = v);
@@ -750,8 +763,10 @@ public boolean isVisible(TeamData data) {
 			return true;
 		}
 
-		if (hideUntilDepsVisible.get(chapter.hideQuestUntilDepsVisible())) {
+		if (hideUntilDepsComplete.get(chapter.hideQuestUntilDepsComplete())) {
 			return data.areDependenciesComplete(this);
+		} else if (hideUntilDepsVisible.get(chapter.isHideQuestUntilDepsVisible())) {
+				return data.areDependenciesVisible(this);
 		}
 
 		return streamDependencies().anyMatch(object -> object.isVisible(data));
@@ -1051,21 +1066,35 @@ public void removeReward(Reward reward) {
 		rewards.remove(reward);
 	}
 
-	public boolean areDependenciesComplete(TeamData teamData) {
+	@FunctionalInterface
+	private interface DependencyChecker {
+		default boolean check(QuestObject questObject) {
+			return !questObject.invalid && check0(questObject);
+		}
+		boolean check0(QuestObject questObject);
+	}
+
+	private boolean checkDependencies(DependencyChecker checker) {
 		if (minRequiredDependencies > 0) {
 			return streamDependencies()
-					.filter(dep -> teamData.isCompleted(dep) && !dep.invalid)
+					.filter(checker::check)
 					.limit(minRequiredDependencies)
 					.count() == minRequiredDependencies;
 		} else if (dependencyRequirement.needOnlyOne()) {
-			return streamDependencies()
-					.anyMatch(dep -> !dep.invalid && (dependencyRequirement.needCompletion() ? teamData.isCompleted(dep) : teamData.isStarted(dep)));
+			return streamDependencies().anyMatch(checker::check);
 		} else {
-			return streamDependencies()
-					.allMatch(dep -> !dep.invalid && (dependencyRequirement.needCompletion() ? teamData.isCompleted(dep) : teamData.isStarted(dep)));
+			return streamDependencies().allMatch(checker::check);
 		}
 	}
 
+	public boolean areDependenciesComplete(TeamData teamData) {
+		return checkDependencies(dep -> dependencyRequirement.needCompletion() ? teamData.isCompleted(dep) : teamData.isStarted(dep));
+	}
+
+	public boolean areDependenciesVisible(TeamData teamData) {
+		return checkDependencies(dep -> dep.isVisible(teamData));
+	}
+
 	public List<Pair<Integer,Integer>> buildDescriptionIndex() {
 		List<Pair<Integer,Integer>> index = new ArrayList<>();
 
diff --git a/common/src/main/java/dev/ftb/mods/ftbquests/quest/TeamData.java b/common/src/main/java/dev/ftb/mods/ftbquests/quest/TeamData.java
index 57f5213b..4f7379ab 100644
--- a/common/src/main/java/dev/ftb/mods/ftbquests/quest/TeamData.java
+++ b/common/src/main/java/dev/ftb/mods/ftbquests/quest/TeamData.java
@@ -28,6 +28,7 @@
 import net.minecraft.server.level.ServerPlayer;
 import net.minecraft.world.entity.player.Player;
 import net.minecraft.world.item.ItemStack;
+import org.apache.commons.lang3.function.ToBooleanBiFunction;
 import org.jetbrains.annotations.Nullable;
 
 import java.nio.file.Path;
@@ -59,8 +60,9 @@ public class TeamData {
 	private final Long2LongOpenHashMap completed;
 	private final Object2ObjectOpenHashMap<UUID,PerPlayerData> perPlayerData;
 
-	private Long2ByteOpenHashMap areDependenciesCompleteCache;
-	private Object2ByteOpenHashMap<QuestKey> unclaimedRewardsCache;
+	private final Long2ByteOpenHashMap areDependenciesCompleteCache;
+	private final Long2ByteOpenHashMap areDependenciesVisibleCache;
+	private final Object2ByteOpenHashMap<QuestKey> unclaimedRewardsCache;
 
 	public TeamData(UUID teamId, BaseQuestFile file) {
 		this(teamId, file, "");
@@ -82,6 +84,9 @@ public TeamData(UUID teamId, BaseQuestFile file, String name) {
 		completed = new Long2LongOpenHashMap();
 		completed.defaultReturnValue(0L);
 		perPlayerData = new Object2ObjectOpenHashMap<>();
+		areDependenciesCompleteCache = new Long2ByteOpenHashMap();
+		areDependenciesVisibleCache = new Long2ByteOpenHashMap();
+		unclaimedRewardsCache = new Object2ByteOpenHashMap<>();
 	}
 
 	public UUID getTeamId() {
@@ -238,14 +243,8 @@ public boolean isRewardClaimed(UUID player, Reward reward) {
 	}
 
 	public boolean hasUnclaimedRewards(UUID player, QuestObject object) {
-		if (unclaimedRewardsCache == null) {
-			unclaimedRewardsCache = new Object2ByteOpenHashMap<>();
-			unclaimedRewardsCache.defaultReturnValue(BOOL_UNKNOWN);
-		}
-
 		QuestKey key = QuestKey.create(player, object.id);
-		byte b = unclaimedRewardsCache.getByte(key);
-
+		byte b = unclaimedRewardsCache.getOrDefault(key, BOOL_UNKNOWN);
 		if (b == BOOL_UNKNOWN) {
 			b = object.hasUnclaimedRewardsRaw(this, player) ? BOOL_TRUE : BOOL_FALSE;
 			unclaimedRewardsCache.put(key, b);
@@ -301,8 +300,9 @@ public boolean resetReward(UUID player, Reward reward) {
 	}
 
 	public void clearCachedProgress() {
-		areDependenciesCompleteCache = null;
-		unclaimedRewardsCache = null;
+		areDependenciesCompleteCache.clear();
+		areDependenciesVisibleCache.clear();
+		unclaimedRewardsCache.clear();
 	}
 
 	public SNBTCompoundTag serializeNBT() {
@@ -510,25 +510,28 @@ public boolean isCompleted(QuestObject object) {
 		return completed.containsKey(object.id);
 	}
 
-	public boolean areDependenciesComplete(Quest quest) {
+	private boolean checkDepsCached(Quest quest, Long2ByteOpenHashMap cache, ToBooleanBiFunction<Quest,TeamData> checker) {
 		if (!quest.hasDependencies()) {
 			return true;
 		}
 
-		if (areDependenciesCompleteCache == null) {
-			areDependenciesCompleteCache = new Long2ByteOpenHashMap();
-			areDependenciesCompleteCache.defaultReturnValue(BOOL_UNKNOWN);
-		}
-
-		byte res = areDependenciesCompleteCache.get(quest.id);
+		byte res = cache.getOrDefault(quest.id, BOOL_UNKNOWN);
 		if (res == BOOL_UNKNOWN) {
-			res = quest.areDependenciesComplete(this) ? BOOL_TRUE : BOOL_FALSE;
-			areDependenciesCompleteCache.put(quest.id, res);
+			res = checker.applyAsBoolean(quest, this) ? BOOL_TRUE : BOOL_FALSE;
+			cache.put(quest.id, res);
 		}
 
 		return res == BOOL_TRUE;
 	}
 
+	public boolean areDependenciesComplete(Quest quest) {
+		return checkDepsCached(quest, areDependenciesCompleteCache, Quest::areDependenciesComplete);
+	}
+
+	public boolean areDependenciesVisible(Quest quest) {
+		return checkDepsCached(quest, areDependenciesVisibleCache, Quest::areDependenciesVisible);
+	}
+
 	public boolean canStartTasks(Quest quest) {
 		return quest.getProgressionMode() == ProgressionMode.FLEXIBLE || areDependenciesComplete(quest);
 	}
diff --git a/common/src/main/resources/assets/ftbquests/lang/en_us.json b/common/src/main/resources/assets/ftbquests/lang/en_us.json
index dbd2cca3..2f40be6e 100644
--- a/common/src/main/resources/assets/ftbquests/lang/en_us.json
+++ b/common/src/main/resources/assets/ftbquests/lang/en_us.json
@@ -152,6 +152,7 @@
 	"ftbquests.chapter.image.corner": "Rotate and offset from corner",
 	"ftbquests.chapter.appearance.default_min_width": "Default Min Opened Quest Window Width",
 	"ftbquests.chapter.visibility.hide_quest_details_until_startable": "Hide Quest Details Until Startable",
+	"ftbquests.chapter.visibility.hide_quest_until_deps_complete": "Hide Quests Until Dependencies Complete",
 	"ftbquests.chapter.visibility.hide_quest_until_deps_visible": "Hide Quests Until Dependencies Visible",
 	"ftbquests.quest": "Quest",
 	"ftbquests.quests": "Quests",
@@ -170,7 +171,8 @@
 	"ftbquests.quest.appearance.size.tooltip": "0 means to use chapter default",
 	"ftbquests.quest.appearance.icon_scale": "Icon Scaling",
 	"ftbquests.quest.appearance.icon_scale.tooltip": "Independent of the overall button size",
-	"ftbquests.quest.visibility.hide": "Hide Quest Until Dependencies are Visible",
+	"ftbquests.quest.visibility.hide_until_deps_complete": "Hide Quest Until Dependencies Complete",
+	"ftbquests.quest.visibility.hide_until_deps_visible": "Hide Quest Until Dependencies Visible",
 	"ftbquests.quest.appearance.shape": "Shape",
 	"ftbquests.quest_link.shape": "Shape",
 	"ftbquests.quest_link.size": "Size",
@@ -203,6 +205,7 @@
 	"ftbquests.quest.dependencies.min_required_dependencies": "Min Required Dependencies",
 	"ftbquests.quest.min_required_dependencies.tooltip": "If you set this to anything more than 0, it becomes an OR quest, where only certain amount of dependencies is required for it to unlock",
 	"ftbquests.quest.visibility.hide_text_until_complete": "Hide Text Until Quest is Completed",
+	"ftbquests.quest.visibility.hide_text_until_complete.tooltip": "Quest details can be opened, but no description text is displayed until the quest is completed",
 	"ftbquests.quest.misc.disable_jei": "Disable JEI Recipe",
 	"ftbquests.quest.misc.optional": "Optional Quest",
 	"ftbquests.quest.locked": "Locked: uncompleted dependencies",

From 58bc147237ee3eefde0de4cc1f1e5649d96ffc95 Mon Sep 17 00:00:00 2001
From: Des Herriott <des.herriott@gmail.com>
Date: Fri, 16 Aug 2024 13:56:58 +0100
Subject: [PATCH 2/5] build: version -> 2001.4.9, changelog updated

---
 CHANGELOG.md      | 7 +++++++
 gradle.properties | 2 +-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1723f757..ab6ccc04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [2001.4.9]
+
+### Fixed
+* Fixed "Hide Quests until Dependencies Visible" setting actually checking for dependencies being _complete_
+  * Added new "Hide Quests until Dependencies Complete" setting
+  * So there are now two independent setting for hiding quests based on dependency visibility and/or completion
+
 ## [2001.4.8]
 
 ### Fixed
diff --git a/gradle.properties b/gradle.properties
index f1dedc47..73d851d2 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -5,7 +5,7 @@ mod_id=ftbquests
 archives_base_name=ftb-quests
 minecraft_version=1.20.1
 # Build time
-mod_version=2001.4.8
+mod_version=2001.4.9
 maven_group=dev.ftb.mods
 mod_author=FTB Team
 # Curse release

From d77d7530f0a673da78f839e6cf324005918a99f4 Mon Sep 17 00:00:00 2001
From: Des Herriott <des.herriott@gmail.com>
Date: Fri, 16 Aug 2024 15:02:56 +0100
Subject: [PATCH 3/5] feat: added a few more command substitutions

Also re-did the templating algorithm; should be more performant as the
number of overrides increases

https://github.com/FTBTeam/FTB-Mods-Issues/issues/1311
---
 .../ftbquests/quest/reward/CommandReward.java | 47 ++++++++++++++-----
 1 file changed, 36 insertions(+), 11 deletions(-)

diff --git a/common/src/main/java/dev/ftb/mods/ftbquests/quest/reward/CommandReward.java b/common/src/main/java/dev/ftb/mods/ftbquests/quest/reward/CommandReward.java
index c7a1f3f3..afb4a0c6 100644
--- a/common/src/main/java/dev/ftb/mods/ftbquests/quest/reward/CommandReward.java
+++ b/common/src/main/java/dev/ftb/mods/ftbquests/quest/reward/CommandReward.java
@@ -14,11 +14,16 @@
 import net.minecraft.network.chat.MutableComponent;
 import net.minecraft.server.level.ServerPlayer;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 public class CommandReward extends Reward {
 	private static final String DEFAULT_COMMAND = "/say Hi, @p!";
+	public static final Pattern PATTERN = Pattern.compile("[{](\\w+)}");
 
 	private String command;
 	private boolean elevatePerms;
@@ -92,17 +97,15 @@ public void claim(ServerPlayer player, boolean notify) {
 		}
 
 		overrides.put("quest", quest);
-		overrides.put("team", FTBTeamsAPI.api().getManager().getTeamForPlayer(player)
-				.map(team -> team.getName().getString())
-				.orElse(player.getGameProfile().getName())
-		);
-
-		String cmd = command;
-		for (Map.Entry<String, Object> entry : overrides.entrySet()) {
-			if (entry.getValue() != null) {
-				cmd = cmd.replace("{" + entry.getKey() + "}", entry.getValue().toString());
-			}
-		}
+		FTBTeamsAPI.api().getManager().getTeamForPlayer(player).ifPresent(team -> {
+			overrides.put("team", team.getName().getString());
+			overrides.put("team_id", team.getShortName());
+			overrides.put("long_team_id", team.getId().toString());
+			overrides.put("member_count", team.getMembers().size());
+			overrides.put("online_member_count", team.getOnlineMembers().size());
+		});
+
+		String cmd = format(command, overrides);
 
 		CommandSourceStack source = player.createCommandSourceStack();
 		if (elevatePerms) source = source.withPermission(2);
@@ -116,4 +119,26 @@ public void claim(ServerPlayer player, boolean notify) {
 	public MutableComponent getAltTitle() {
 		return Component.translatable("ftbquests.reward.ftbquests.command").append(": ").append(Component.literal(command).withStyle(ChatFormatting.RED));
 	}
+
+	public static String format(String template, Map<String, Object> parameters) {
+		StringBuilder newTemplate = new StringBuilder(template);
+		List<Object> valueList = new ArrayList<>();
+
+		Matcher matcher = PATTERN.matcher(template);
+
+		while (matcher.find()) {
+			String key = matcher.group(1);
+
+			if (parameters.containsKey(key)) {
+				String paramName = "{" + key + "}";
+				int index = newTemplate.indexOf(paramName);
+				if (index != -1) {
+					newTemplate.replace(index, index + paramName.length(), "%s");
+					valueList.add(parameters.get(key));
+				}
+			}
+		}
+
+		return String.format(newTemplate.toString(), valueList.toArray());
+	}
 }

From 6169f4ee6fd8f3a189194236c97675ffaf841523 Mon Sep 17 00:00:00 2001
From: Des Herriott <des.herriott@gmail.com>
Date: Fri, 16 Aug 2024 15:06:20 +0100
Subject: [PATCH 4/5] chore: changelog updated

---
 CHANGELOG.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab6ccc04..9ca52ffc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [2001.4.9]
 
+### Added
+* A few new template substitutions are available in command rewards
+  * `{team_id}` - the short team name, e.g. "Dev#380df991"
+  * `{long_team_id}` - the full team UUID, e.g. "380df991-f603-344c-a090-369bad2a924a"
+  * `{member_count}` - the number of players in the team
+  * `{online_member_count}` - the number of currently-online players in the team
+
 ### Fixed
 * Fixed "Hide Quests until Dependencies Visible" setting actually checking for dependencies being _complete_
   * Added new "Hide Quests until Dependencies Complete" setting

From 52c9d57adf79029b3cf533abef2bfd83ac4650d3 Mon Sep 17 00:00:00 2001
From: Des Herriott <des.herriott@gmail.com>
Date: Tue, 20 Aug 2024 09:12:45 +0100
Subject: [PATCH 5/5] chore: make default overlay panel inset 4/4 instead of
 2/2

A bit more inset just looks better
---
 .../dev/ftb/mods/ftbquests/client/FTBQuestsClientConfig.java  | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/common/src/main/java/dev/ftb/mods/ftbquests/client/FTBQuestsClientConfig.java b/common/src/main/java/dev/ftb/mods/ftbquests/client/FTBQuestsClientConfig.java
index c2287b33..57f2217f 100644
--- a/common/src/main/java/dev/ftb/mods/ftbquests/client/FTBQuestsClientConfig.java
+++ b/common/src/main/java/dev/ftb/mods/ftbquests/client/FTBQuestsClientConfig.java
@@ -21,8 +21,8 @@ public interface FTBQuestsClientConfig {
     SNBTConfig UI = CONFIG.addGroup("ui", 0);
     BooleanValue OLD_SCROLL_WHEEL = UI.addBoolean("old_scroll_wheel", false);
     EnumValue<PanelPositioning> PINNED_QUESTS_POS = UI.addEnum("pinned_quests_pos", PanelPositioning.NAME_MAP, PanelPositioning.RIGHT);
-    IntValue PINNED_QUESTS_INSET_X = UI.addInt("pinned_quests_inset_x", 2);
-    IntValue PINNED_QUESTS_INSET_Y = UI.addInt("pinned_quests_inset_y", 2);
+    IntValue PINNED_QUESTS_INSET_X = UI.addInt("pinned_quests_inset_x", 4);
+    IntValue PINNED_QUESTS_INSET_Y = UI.addInt("pinned_quests_inset_y", 4);
 
     // TODO migrate chapter-pinned and pinned-quests data out of per-player team data into here