From 3b34904c9a75edf961b7e3e19b6c4017f1ce7f9e Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:33:28 +0000 Subject: [PATCH 1/2] Add supply hub menus for guilds and hosts. The guild menu gains a Supply Hubs page listing each hub with its upkeep, whether it is active, its connections and the guild's trade power and production there. The guild leader can remove a hub from it. The installation menu gains a page of the hubs hosted there. The owning faction's leader can evict a hub from it, and it lists the faction's hub permits. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../simplefactions/enums/SFGUI.java | 2 + .../managers/InventoryManager.java | 37 ++- .../managers/inventory/GuildCreator.java | 10 + .../managers/inventory/GuildView.java | 9 + .../inventory/InstallationCreator.java | 10 + .../managers/inventory/InstallationView.java | 26 +- .../managers/inventory/SupplyHubCreator.java | 52 +++ .../managers/inventory/SupplyHubView.java | 310 ++++++++++++++++++ .../inventory/InstallationViewTest.java | 5 +- .../managers/inventory/SupplyHubViewTest.java | 42 +++ 10 files changed, 498 insertions(+), 5 deletions(-) create mode 100644 src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubCreator.java create mode 100644 src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubView.java create mode 100644 src/test/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubViewTest.java diff --git a/src/main/java/net/tfminecraft/simplefactions/enums/SFGUI.java b/src/main/java/net/tfminecraft/simplefactions/enums/SFGUI.java index bf242ed7..57619f1e 100644 --- a/src/main/java/net/tfminecraft/simplefactions/enums/SFGUI.java +++ b/src/main/java/net/tfminecraft/simplefactions/enums/SFGUI.java @@ -69,6 +69,8 @@ public enum SFGUI { TREATY_VIEW, INSTALLATIONS_VIEW, INSTALLATION_DETAIL_VIEW, + SUPPLY_HUB_VIEW, + HOSTED_SUPPLY_HUB_VIEW, PLAYER_LEDGER_VIEW, COMPANY_VIEW, COMPANY_SLOTS_VIEW, diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/InventoryManager.java b/src/main/java/net/tfminecraft/simplefactions/managers/InventoryManager.java index eb75b20d..370da93a 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/InventoryManager.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/InventoryManager.java @@ -48,6 +48,7 @@ import net.tfminecraft.simplefactions.managers.inventory.GovernmentView; import net.tfminecraft.simplefactions.managers.inventory.GuildView; import net.tfminecraft.simplefactions.managers.inventory.InstallationView; +import net.tfminecraft.simplefactions.managers.inventory.SupplyHubView; import net.tfminecraft.simplefactions.managers.inventory.InventoryUpdater; import net.tfminecraft.simplefactions.managers.inventory.LawView; import net.tfminecraft.simplefactions.managers.inventory.LoanPayment; @@ -213,6 +214,7 @@ public void factionView(Player player, Faction f) { //Guilds public GuildView guildView = new GuildView(this); + public SupplyHubView supplyHubView = new SupplyHubView(this); public PlayerLedgerView playerLedgerView = new PlayerLedgerView(this); public void guildList(Player player) { guildView.guildList(player); @@ -757,6 +759,14 @@ public void confirmView(Player player, Faction f, String key, String data) { player.openInventory(i); } + public void confirmSupplyHub(Player player, Faction faction, String data) { + confirming.put(player, faction); + Inventory inventory = SimpleFactions.plugin.getServer().createInventory(null, 27, "§7Confirm Action"); + inventory.setItem(11, createButton("confirm", "supply_hub_action", data)); + inventory.setItem(15, createButton("cancel", "supply_hub_action", data)); + player.openInventory(inventory); + } + // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") public void openQueueCancelConfirm(Player player, Faction f, String payload, String title) { @@ -1086,6 +1096,16 @@ public void clickButton(InventoryClickEvent e) { guildList(p); } break; + case SUPPLY_HUB_VIEW: + if (g != null) { + guildView(p, g); + } + break; + case HOSTED_SUPPLY_HUB_VIEW: + if (f != null) { + installationView.installationDetailView(p, f, h.getSecondaryId()); + } + break; case MILITARY_VIEW: factionView(p, f); break; @@ -1276,6 +1296,9 @@ public void clickButton(InventoryClickEvent e) { } else if(h.getType() == SFGUI.INSTALLATIONS_VIEW || h.getType() == SFGUI.INSTALLATION_DETAIL_VIEW) { installationView.click(e, inv, p); + } else if (h.getType() == SFGUI.SUPPLY_HUB_VIEW + || h.getType() == SFGUI.HOSTED_SUPPLY_HUB_VIEW) { + supplyHubView.click(e, inv, p); } else if(h.getType() == SFGUI.GOVERNMENT_VIEW || h.getType() == SFGUI.PROPOSAL_VIEW || h.getType() == SFGUI.PROPOSALS @@ -1356,9 +1379,21 @@ public void clickButton(InventoryClickEvent e) { } if(!confirming.containsKey(p)) return; ItemStack item = e.getCurrentItem(); + if (item == null || !item.hasItemMeta()) { + return; + } ItemMeta m = item.getItemMeta(); - NamespacedKey key = new NamespacedKey(SimpleFactions.plugin, "queue_cancel"); + NamespacedKey key = new NamespacedKey(SimpleFactions.plugin, "supply_hub_action"); String data = m.getPersistentDataContainer().get(key, PersistentDataType.STRING); + if (data != null) { + boolean accepted = item.getType().equals(Material.GREEN_CONCRETE); + supplyHubView.confirm(p, data, accepted); + confirming.remove(p); + p.playSound(p, accepted ? Sound.ENTITY_PLAYER_LEVELUP : Sound.BLOCK_NOTE_BLOCK_BIT, 1f, 1f); + return; + } + key = new NamespacedKey(SimpleFactions.plugin, "queue_cancel"); + data = m.getPersistentDataContainer().get(key, PersistentDataType.STRING); if (data != null) { var parsed = QueueCancelPayload.parse(data); if (parsed.isEmpty()) { diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/GuildCreator.java b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/GuildCreator.java index af1bf526..7ee13e1b 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/GuildCreator.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/GuildCreator.java @@ -58,6 +58,16 @@ public class GuildCreator { FactionRanker r = new FactionRanker(); + @SuppressWarnings("deprecation") + public ItemStack createSupplyHubsItem() { + ItemStack item = new ItemStack(Material.CHEST); + ItemMeta meta = item.getItemMeta(); + meta.setDisplayName(StringFormatter.formatHex("#b5835a§lSupply Hubs")); + meta.setLore(List.of("§7View your guild's supply hubs")); + item.setItemMeta(meta); + return item; + } + // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") public ItemStack createListItem(Player p, Guild guild) { diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/GuildView.java b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/GuildView.java index 2e35159f..a35f1ef6 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/GuildView.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/GuildView.java @@ -202,6 +202,7 @@ public void guildView(Player player, Guild guild, Inventory i) { } i.setItem(13, creator.createMenuItem(player, guild, MenuItemType.TRADE_BREAKDOWN)); i.setItem(14, creator.createLedgerItem(player, guild)); + i.setItem(24, creator.createSupplyHubsItem()); if (!guild.isBase()) { i.setItem(17, creator.createDividendItem(player, guild)); } @@ -251,6 +252,14 @@ public void upgradeView(Player player, Guild guild, Inventory i) { public void click(InventoryClickEvent e, Inventory inventory, Player p) { if(!(inventory.getHolder() instanceof SFInventoryHolder)) return; SFInventoryHolder h = (SFInventoryHolder) inventory.getHolder(); + if (h.getType() == SFGUI.GUILD_VIEW && e.getSlot() == 24) { + e.setCancelled(true); + Guild guild = FactionManager.getGuildByString(h.getId()); + if (guild != null && guild.isMember(p)) { + inv.supplyHubView.guildView(p, guild); + } + return; + } if (e.getView().getTitle().equalsIgnoreCase("§7Guild List")) { e.setCancelled(true); diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/InstallationCreator.java b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/InstallationCreator.java index bea491e0..26b48927 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/InstallationCreator.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/InstallationCreator.java @@ -27,6 +27,16 @@ import net.tfminecraft.tlibs.utils.TimeFormatter; public class InstallationCreator { + + @SuppressWarnings("deprecation") + public ItemStack createSupplyHubsButton() { + ItemStack item = new ItemStack(Material.CHEST, 1); + ItemMeta meta = item.getItemMeta(); + meta.setDisplayName("§eSupply Hubs"); + meta.setLore(List.of("§7View guild hubs hosted here")); + item.setItemMeta(meta); + return item; + } // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") public ItemStack createSummary(Faction f) { diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/InstallationView.java b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/InstallationView.java index 908e54df..02146017 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/InstallationView.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/InstallationView.java @@ -129,7 +129,7 @@ public void installationDetailView(Player player, Faction f, String installation List berthed = SimpleFactions.getVehicleRegistry().getByInstallation(f.getId(), installation.getId()); berthed.sort(Comparator.comparing(PlayerVehicleRecord::getVehicleTypeId)); - for (int index = 0; index < berthed.size() && index < 43; index++) { + for (int index = 0; index < berthed.size() && index < 42; index++) { PlayerVehicleRecord record = berthed.get(index); Optional location = VehicleFramework.getVehicleManager() @@ -158,15 +158,27 @@ public void installationDetailView(Player player, Faction f, String installation inventory.setItem(13, new ItemStack(Material.AIR, 1)); } + if (!isPending && installation != null) { + int hubSlot = supplyHubSlot( + InstallationConfigLoader.getHubSlots(installation.getKind(), installation.getLevel())); + if (hubSlot >= 0) { + inventory.setItem(hubSlot, creator.createSupplyHubsButton()); + } + } + inventory.setItem(53, inv.createBackButton(SFGUI.INSTALLATION_DETAIL_VIEW)); if (open) player.openInventory(inventory); } static int berthedVehicleSlot(int index) { - if (index < 0 || index >= 43) { + if (index < 0 || index >= 42) { return -1; } - return index + (index >= 11 ? 1 : 0) + (index >= 12 ? 1 : 0); + return index + (index >= 11 ? 1 : 0) + (index >= 12 ? 1 : 0) + (index >= 13 ? 1 : 0); + } + + static int supplyHubSlot(int hubSlots) { + return hubSlots > 0 ? 15 : -1; } public void click(InventoryClickEvent event, Inventory inventory, Player player) { @@ -207,6 +219,14 @@ public void click(InventoryClickEvent event, Inventory inventory, Player player) if (holder.getType() == SFGUI.INSTALLATION_DETAIL_VIEW) { int slot = event.getSlot(); + if (slot == 15) { + Installation installation = f.getInstallationHandler().getById(holder.getSecondaryId()); + if (installation != null && InstallationConfigLoader.getHubSlots( + installation.getKind(), installation.getLevel()) > 0) { + inv.supplyHubView.hostedView(player, f, installation); + } + return; + } if (slot == 13) { ItemStack upgradeItem = event.getCurrentItem(); if (upgradeItem == null diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubCreator.java b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubCreator.java new file mode 100644 index 00000000..78b320ad --- /dev/null +++ b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubCreator.java @@ -0,0 +1,52 @@ +package net.tfminecraft.simplefactions.managers.inventory; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.HubStanding; +import net.tfminecraft.simplefactions.utils.Formatter; + +public class SupplyHubCreator { + public static List guildHubLore( + String installationName, + String kind, + String factionName, + double upkeep, + HubStanding standing, + List connections, + double tradePower, + double production) { + List lore = new ArrayList<>(); + lore.add("§7Installation: §e" + installationName + " §8(" + kind + ")"); + lore.add("§7Owned by: §e" + factionName); + lore.add("§7Daily upkeep: §e" + Formatter.formatDouble(upkeep) + "d"); + lore.add(net.tfminecraft.simplefactions.guild.hub.SupplyHubService.statusText(standing)); + if (connections != null) { + lore.addAll(connections); + } + lore.add("§7Trade power here: §e" + Formatter.formatDouble(tradePower)); + lore.add("§7Production here: §e" + Formatter.formatDouble(production)); + return lore; + } + + public static List hostHubLore(String guildName, String factionName, HubStanding standing) { + List lore = new ArrayList<>(); + lore.add("§7Guild: §e" + guildName); + lore.add("§7Faction: §e" + factionName); + lore.add(net.tfminecraft.simplefactions.guild.hub.SupplyHubService.statusText(standing)); + return lore; + } + + public static ItemStack item(Material material, String name, List lore) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + meta.setDisplayName(name); + meta.setLore(lore); + item.setItemMeta(meta); + return item; + } +} diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubView.java b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubView.java new file mode 100644 index 00000000..8c9be6ea --- /dev/null +++ b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubView.java @@ -0,0 +1,310 @@ +package net.tfminecraft.simplefactions.managers.inventory; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; + +import net.tfminecraft.simplefactions.SimpleFactions; +import net.tfminecraft.simplefactions.enums.SFGUI; +import net.tfminecraft.simplefactions.guild.Guild; +import net.tfminecraft.simplefactions.guild.hub.HubNetwork; +import net.tfminecraft.simplefactions.guild.hub.HubTransport.Link; +import net.tfminecraft.simplefactions.guild.hub.SupplyHub; +import net.tfminecraft.simplefactions.guild.hub.SupplyHubService; +import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.HubStanding; +import net.tfminecraft.simplefactions.installation.Installation; +import net.tfminecraft.simplefactions.keys.Keys; +import net.tfminecraft.simplefactions.loaders.InstallationConfigLoader; +import net.tfminecraft.simplefactions.managers.FactionManager; +import net.tfminecraft.simplefactions.managers.InventoryManager; +import net.tfminecraft.simplefactions.managers.ProvinceManager; +import net.tfminecraft.simplefactions.map.provinces.Province; +import net.tfminecraft.simplefactions.objects.Faction; +import net.tfminecraft.simplefactions.utils.Formatter; + +public class SupplyHubView { + private final InventoryManager inv; + + public SupplyHubView(InventoryManager inv) { + this.inv = inv; + } + + public void guildView(Player player, Guild guild) { + Inventory inventory = SimpleFactions.plugin.getServer().createInventory( + new net.tfminecraft.simplefactions.managers.holder.SFInventoryHolder( + guild.getId(), SFGUI.SUPPLY_HUB_VIEW), + 54, + "§7Supply Hubs"); + populateGuildView(player, guild, inventory); + player.openInventory(inventory); + } + + private void populateGuildView(Player player, Guild guild, Inventory inventory) { + inventory.clear(); + ProvinceManager provinces = SimpleFactions.getInstance().getProvinceManager(); + List hubs = SupplyHubService.oldestFirst(guild.getSupplyHubs()); + for (int index = 0; index < hubs.size() && index < 45; index++) { + SupplyHub hub = hubs.get(index); + Installation installation = SupplyHubService.findInstallation( + hub.ownerFactionId(), hub.installationId()); + Faction owner = FactionManager.getByString(hub.ownerFactionId()); + HubStanding standing = standing(guild, hub); + List connections = connectionLore(guild, hub, installation); + Province province = installation == null ? null : provinces.get(installation.getProvince()); + List lore = SupplyHubCreator.guildHubLore( + installation == null ? hub.installationId() : installation.getName(), + installation == null ? "Unknown" : installation.getKind().getDisplayName(), + owner == null ? hub.ownerFactionId() : owner.getName(), + SupplyHubService.upkeepOf(hub, hubs, SupplyHubService.baseUpkeep(), SupplyHubService.upkeepGrowth()), + standing, + connections, + province == null ? 0 : SupplyHubService.exportedTrade(province, guild.getId()), + province == null ? 0 : province.getGuildProduction(guild)); + ItemStack item = SupplyHubCreator.item( + Material.CHEST, + "§e" + (installation == null ? hub.installationId() : installation.getName()), + lore); + if (isLeader(guild, player)) { + ItemMeta meta = item.getItemMeta(); + meta.getPersistentDataContainer().set(Keys.STRING_KEY, PersistentDataType.STRING, + hub.ownerFactionId() + ":" + hub.installationId()); + lore.add("§cClick to remove this hub"); + meta.setLore(lore); + item.setItemMeta(meta); + } + inventory.setItem(index, item); + } + int limit = SupplyHubService.baseLimit(); + inventory.setItem(49, SupplyHubCreator.item(Material.PAPER, "§eSupply Hub Information", List.of( + "§7Hubs: §e" + hubs.size() + "/" + limit, + "§7Daily upkeep: §e" + Formatter.formatDouble(SupplyHubService.dailyCost(guild)) + "d/day", + "§7Build with §e/guild hub build §7while standing in a port, airport or train station"))); + inventory.setItem(53, inv.createBackButton(SFGUI.SUPPLY_HUB_VIEW)); + } + + public void hostedView(Player player, Faction faction, Installation installation) { + Inventory inventory = SimpleFactions.plugin.getServer().createInventory( + new net.tfminecraft.simplefactions.managers.holder.SFInventoryHolder( + faction.getId(), SFGUI.HOSTED_SUPPLY_HUB_VIEW, installation.getId()), + 54, + "§7Hosted Supply Hubs"); + populateHostedView(player, faction, installation, inventory); + player.openInventory(inventory); + } + + private void populateHostedView(Player player, Faction faction, Installation installation, Inventory inventory) { + inventory.clear(); + List guilds = new ArrayList<>(); + for (Guild guild : SupplyHubService.allGuilds()) { + if (SupplyHubService.hasHub(guild.getSupplyHubs(), faction.getId(), installation.getId())) { + guilds.add(guild); + } + } + guilds.sort(Comparator.comparingLong(guild -> SupplyHubService.findHub( + guild.getSupplyHubs(), faction.getId(), installation.getId()).createdAt())); + int max = InstallationConfigLoader.getHubSlots(installation.getKind(), installation.getLevel()); + for (int index = 0; index < guilds.size() && index < 45; index++) { + Guild guild = guilds.get(index); + SupplyHub hub = SupplyHubService.findHub(guild.getSupplyHubs(), faction.getId(), installation.getId()); + HubStanding standing = standing(guild, hub); + Faction host = guild.getFaction(); + ItemStack item = SupplyHubCreator.item(Material.CHEST, + "§e" + guild.getName(), + SupplyHubCreator.hostHubLore(guild.getName(), host == null ? "Unknown" : host.getName(), standing)); + if (faction.getLeader().equalsIgnoreCase(player.getName())) { + ItemMeta meta = item.getItemMeta(); + meta.getPersistentDataContainer().set(Keys.STRING_KEY, PersistentDataType.STRING, guild.getId()); + List lore = new ArrayList<>(meta.getLore()); + lore.add("§cClick to evict this hub"); + meta.setLore(lore); + item.setItemMeta(meta); + } + inventory.setItem(index, item); + } + inventory.setItem(47, SupplyHubCreator.item(Material.PAPER, "§eHub Slots", + List.of("§7Used: §e" + guilds.size() + "/" + max))); + inventory.setItem(49, SupplyHubCreator.item(Material.WRITABLE_BOOK, "§eHub Permits", + permitLore(faction))); + inventory.setItem(53, inv.createBackButton(SFGUI.HOSTED_SUPPLY_HUB_VIEW)); + } + + public static List permitLore(Faction faction) { + List lore = new ArrayList<>(); + lore.add("§7Changed with §e/faction hubpermit "); + lore.add("§7Permitted guilds:"); + if (faction.getHubPermits().isEmpty()) { + lore.add("§8None"); + } else { + for (String permit : faction.getHubPermits()) { + lore.add("§e" + permit); + } + } + return lore; + } + + public static boolean removeGuildHub( + List hubs, String ownerFactionId, String installationId) { + SupplyHub hub = SupplyHubService.findHub(hubs, ownerFactionId, installationId); + return hub != null && hubs.remove(hub); + } + + public void click(InventoryClickEvent event, Inventory inventory, Player player) { + event.setCancelled(true); + if (!(inventory.getHolder() instanceof net.tfminecraft.simplefactions.managers.holder.SFInventoryHolder holder)) { + return; + } + if (holder.getType() == SFGUI.SUPPLY_HUB_VIEW) { + Guild guild = FactionManager.getGuildByString(holder.getId()); + if (guild == null) { + return; + } + if (event.getSlot() == 53) { + inv.guildView(player, guild); + return; + } + if (!isLeader(guild, player)) { + return; + } + ItemStack item = event.getCurrentItem(); + if (item == null || !item.hasItemMeta()) { + return; + } + String data = item.getItemMeta().getPersistentDataContainer() + .get(Keys.STRING_KEY, PersistentDataType.STRING); + if (data == null) { + return; + } + int separator = data.indexOf(':'); + if (separator < 0) { + return; + } + inv.confirmSupplyHub(player, guild.getFaction(), "guild|" + guild.getId() + "|" + + data.substring(0, separator) + "|" + data.substring(separator + 1)); + } else if (holder.getType() == SFGUI.HOSTED_SUPPLY_HUB_VIEW) { + Faction faction = FactionManager.getByString(holder.getId()); + if (faction == null) { + return; + } + Installation installation = faction.getInstallationHandler().getById(holder.getSecondaryId()); + if (event.getSlot() == 53) { + inv.installationView.installationDetailView(player, faction, holder.getSecondaryId()); + return; + } + if (!faction.getLeader().equalsIgnoreCase(player.getName()) || installation == null) { + return; + } + ItemStack item = event.getCurrentItem(); + if (item == null || !item.hasItemMeta()) { + return; + } + String guildId = item.getItemMeta().getPersistentDataContainer() + .get(Keys.STRING_KEY, PersistentDataType.STRING); + if (guildId == null) { + return; + } + inv.confirmSupplyHub(player, faction, + "host|" + faction.getId() + "|" + installation.getId() + "|" + guildId); + } + player.playSound(player, Sound.BLOCK_NOTE_BLOCK_BIT, 1f, 1f); + } + + public boolean confirm(Player player, String data, boolean accepted) { + String[] parts = data.split("\\|", -1); + if (parts.length == 4 && parts[0].equals("guild")) { + Guild guild = FactionManager.getGuildByString(parts[1]); + if (guild == null || !isLeader(guild, player)) { + return false; + } + if (accepted) { + if (removeGuildHub(guild.getSupplyHubs(), parts[2], parts[3])) { + SimpleFactions.getInstance().getProvinceManager().recalculate(); + } + } + guildView(player, guild); + return true; + } + if (parts.length == 4 && parts[0].equals("host")) { + Faction faction = FactionManager.getByString(parts[1]); + if (faction == null || !faction.getLeader().equalsIgnoreCase(player.getName())) { + return false; + } + if (accepted) { + Guild guild = FactionManager.getGuildByString(parts[3]); + if (guild != null && removeGuildHub(guild.getSupplyHubs(), parts[1], parts[2])) { + SimpleFactions.getInstance().getProvinceManager().recalculate(); + org.bukkit.entity.Player online = SimpleFactions.plugin.getServer() + .getPlayerExact(guild.getLeader()); + if (online != null) { + Installation site = faction.getInstallationHandler().getById(parts[2]); + online.sendMessage("§cYour supply hub at §f" + + (site == null ? parts[2] : site.getName()) + " §cwas evicted"); + } + } + } + Installation installation = faction.getInstallationHandler().getById(parts[2]); + if (installation != null) { + hostedView(player, faction, installation); + } + return true; + } + return false; + } + + /** A realm guild is led by its faction's leader, which {@link Guild#getLeader()} resolves. */ + private static boolean isLeader(Guild guild, Player player) { + return player.getName().equalsIgnoreCase(guild.getLeader()); + } + + private static HubStanding standing(Guild guild, SupplyHub hub) { + Installation installation = SupplyHubService.findInstallation( + hub.ownerFactionId(), hub.installationId()); + Faction owner = FactionManager.getByString(hub.ownerFactionId()); + boolean allowed = SupplyHubService.ownerAllows( + guild.getFaction() == null ? null : guild.getFaction().getId(), hub.ownerFactionId(), + owner != null && owner.hasHubPermit(guild.getId())); + int slots = installation == null + ? 0 + : InstallationConfigLoader.getHubSlots(installation.getKind(), installation.getLevel()); + return SupplyHubService.standing(hub, installation != null, allowed, slots, + SupplyHubService.atInstallation( + hub.ownerFactionId(), hub.installationId(), SupplyHubService.allGuilds())); + } + + private static List connectionLore(Guild guild, SupplyHub hub, Installation installation) { + List result = new ArrayList<>(); + if (installation == null) { + return result; + } + for (Link link : HubNetwork.linksFor(guild)) { + if (link.fromProvince() != installation.getProvince()) { + continue; + } + Installation destination = null; + for (SupplyHub other : guild.getSupplyHubs()) { + Installation candidate = SupplyHubService.findInstallation( + other.ownerFactionId(), other.installationId()); + if (candidate != null && candidate.getProvince() == link.toProvince() + && candidate.getKind() == link.mode().getKind()) { + destination = candidate; + break; + } + } + if (destination != null) { + result.add("§7Sends to §f" + destination.getName() + " §7by " + + link.mode().getKey() + " (" + Math.round(link.distance()) + " blocks): §e" + + Math.round(link.tradeFactor() * 100) + "% §7trade, §e" + + Math.round(link.productionFactor() * 100) + "% §7production"); + } + } + return result; + } +} diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/inventory/InstallationViewTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/inventory/InstallationViewTest.java index 70451732..cab0ef6c 100644 --- a/src/test/java/net/tfminecraft/simplefactions/managers/inventory/InstallationViewTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/managers/inventory/InstallationViewTest.java @@ -11,8 +11,11 @@ void berthedVehicleSlotsSkipControlSlots() { assertEquals(10, InstallationView.berthedVehicleSlot(10)); assertEquals(12, InstallationView.berthedVehicleSlot(11)); assertEquals(14, InstallationView.berthedVehicleSlot(12)); - assertEquals(44, InstallationView.berthedVehicleSlot(42)); + assertEquals(44, InstallationView.berthedVehicleSlot(41)); + assertEquals(-1, InstallationView.berthedVehicleSlot(42)); assertEquals(-1, InstallationView.berthedVehicleSlot(43)); + assertEquals(15, InstallationView.supplyHubSlot(1)); + assertEquals(-1, InstallationView.supplyHubSlot(0)); assertNotEquals(11, InstallationView.berthedVehicleSlot(11)); assertNotEquals(13, InstallationView.berthedVehicleSlot(12)); } diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubViewTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubViewTest.java new file mode 100644 index 00000000..1a27757b --- /dev/null +++ b/src/test/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubViewTest.java @@ -0,0 +1,42 @@ +package net.tfminecraft.simplefactions.managers.inventory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import net.tfminecraft.simplefactions.guild.hub.SupplyHub; +import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.DormantReason; +import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.HubStanding; + +class SupplyHubViewTest { + @Test + void guildHubLoreShowsActiveAndDormantState() { + List active = SupplyHubCreator.guildHubLore( + "North Port", "Port", "Northland", 15, new HubStanding(true, null), + List.of("§7South Port: §esea §71000 blocks, trade 45%, production 30%"), 12, 4); + assertTrue(active.contains("§aActive")); + assertTrue(active.stream().anyMatch(line -> line.contains("South Port") && line.contains("trade 45%"))); + + List dormant = SupplyHubCreator.guildHubLore( + "North Port", "Port", "Northland", 15, + new HubStanding(false, DormantReason.NO_PERMIT), List.of(), 12, 4); + assertTrue(dormant.contains("§cDormant §7(the owning faction has not granted a hub permit)")); + } + + @Test + void removalOnlyRemovesTheRequestedGuildsHub() { + SupplyHub target = new SupplyHub("Northland", "North_Port", 10); + SupplyHub otherAtSite = new SupplyHub("Northland", "North_Port", 11); + SupplyHub otherSite = new SupplyHub("Southland", "South_Port", 12); + List hubs = new ArrayList<>(List.of(target, otherAtSite, otherSite)); + + assertTrue(SupplyHubView.removeGuildHub(hubs, "Northland", "North_Port")); + assertEquals(List.of(otherAtSite, otherSite), hubs); + assertFalse(SupplyHubView.removeGuildHub(hubs, "Missing", "North_Port")); + } +} From c231f70b8405296b6bd3b6659ec2d8a03b730e1c Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:58:32 +0000 Subject: [PATCH 2/2] Fit the hub menus to the new branches and fix the Back button. The Supply Hubs button moves to slot 26: slots 20 to 24 now hold the branch upgrade buttons, so it was covering Freight Yards. The hub page shows the guild's limit and shares with its branch bonuses, marks hubs beyond the limit as dormant, and only lists links of the hub's own kind. Removing or evicting a hub from a menu now also queues the map export. Back was handled twice, opening the previous menu two times. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../guild/hub/SupplyHubCommands.java | 2 +- .../managers/inventory/GuildView.java | 5 ++-- .../managers/inventory/SupplyHubView.java | 25 +++++++++++++------ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubCommands.java b/src/main/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubCommands.java index d2c862b7..f8fc1720 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubCommands.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubCommands.java @@ -378,7 +378,7 @@ private static double reachableTrade(Guild guild, Installation installation) { } /** Hub changes move trade power, so the map and incomes are brought up to date at once. */ - private static void recalculateTrade() { + public static void recalculateTrade() { if (FactionManager.getMap() != null && FactionManager.factions != null) { for (Faction faction : FactionManager.factions) { if (faction != null && faction.getRGB() != null) { diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/GuildView.java b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/GuildView.java index a35f1ef6..2313ded8 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/GuildView.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/GuildView.java @@ -202,7 +202,8 @@ public void guildView(Player player, Guild guild, Inventory i) { } i.setItem(13, creator.createMenuItem(player, guild, MenuItemType.TRADE_BREAKDOWN)); i.setItem(14, creator.createLedgerItem(player, guild)); - i.setItem(24, creator.createSupplyHubsItem()); + // Slots 20 to 24 are the branch upgrade buttons, so this sits past Loans. + i.setItem(26, creator.createSupplyHubsItem()); if (!guild.isBase()) { i.setItem(17, creator.createDividendItem(player, guild)); } @@ -252,7 +253,7 @@ public void upgradeView(Player player, Guild guild, Inventory i) { public void click(InventoryClickEvent e, Inventory inventory, Player p) { if(!(inventory.getHolder() instanceof SFInventoryHolder)) return; SFInventoryHolder h = (SFInventoryHolder) inventory.getHolder(); - if (h.getType() == SFGUI.GUILD_VIEW && e.getSlot() == 24) { + if (h.getType() == SFGUI.GUILD_VIEW && e.getSlot() == 26) { e.setCancelled(true); Guild guild = FactionManager.getGuildByString(h.getId()); if (guild != null && guild.isMember(p)) { diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubView.java b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubView.java index 8c9be6ea..c979db37 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubView.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubView.java @@ -15,7 +15,10 @@ import net.tfminecraft.simplefactions.SimpleFactions; import net.tfminecraft.simplefactions.enums.SFGUI; +import net.tfminecraft.simplefactions.enums.GuildModifier; import net.tfminecraft.simplefactions.guild.Guild; +import net.tfminecraft.simplefactions.guild.GuildModifierOverride; +import net.tfminecraft.simplefactions.guild.hub.SupplyHubCommands; import net.tfminecraft.simplefactions.guild.hub.HubNetwork; import net.tfminecraft.simplefactions.guild.hub.HubTransport.Link; import net.tfminecraft.simplefactions.guild.hub.SupplyHub; @@ -83,7 +86,7 @@ private void populateGuildView(Player player, Guild guild, Inventory inventory) } inventory.setItem(index, item); } - int limit = SupplyHubService.baseLimit(); + int limit = SupplyHubService.limit(guild); inventory.setItem(49, SupplyHubCreator.item(Material.PAPER, "§eSupply Hub Information", List.of( "§7Hubs: §e" + hubs.size() + "/" + limit, "§7Daily upkeep: §e" + Formatter.formatDouble(SupplyHubService.dailyCost(guild)) + "d/day", @@ -168,7 +171,7 @@ public void click(InventoryClickEvent event, Inventory inventory, Player player) return; } if (event.getSlot() == 53) { - inv.guildView(player, guild); + // InventoryManager's back button handling opens the guild menu. return; } if (!isLeader(guild, player)) { @@ -196,7 +199,7 @@ public void click(InventoryClickEvent event, Inventory inventory, Player player) } Installation installation = faction.getInstallationHandler().getById(holder.getSecondaryId()); if (event.getSlot() == 53) { - inv.installationView.installationDetailView(player, faction, holder.getSecondaryId()); + // InventoryManager's back button handling opens the installation menu. return; } if (!faction.getLeader().equalsIgnoreCase(player.getName()) || installation == null) { @@ -226,7 +229,7 @@ public boolean confirm(Player player, String data, boolean accepted) { } if (accepted) { if (removeGuildHub(guild.getSupplyHubs(), parts[2], parts[3])) { - SimpleFactions.getInstance().getProvinceManager().recalculate(); + SupplyHubCommands.recalculateTrade(); } } guildView(player, guild); @@ -240,7 +243,7 @@ public boolean confirm(Player player, String data, boolean accepted) { if (accepted) { Guild guild = FactionManager.getGuildByString(parts[3]); if (guild != null && removeGuildHub(guild.getSupplyHubs(), parts[1], parts[2])) { - SimpleFactions.getInstance().getProvinceManager().recalculate(); + SupplyHubCommands.recalculateTrade(); org.bukkit.entity.Player online = SimpleFactions.plugin.getServer() .getPlayerExact(guild.getLeader()); if (online != null) { @@ -265,6 +268,9 @@ private static boolean isLeader(Guild guild, Player player) { } private static HubStanding standing(Guild guild, SupplyHub hub) { + if (SupplyHubService.beyondGuildLimit(guild, hub)) { + return new HubStanding(false, SupplyHubService.DormantReason.BEYOND_GUILD_LIMIT); + } Installation installation = SupplyHubService.findInstallation( hub.ownerFactionId(), hub.installationId()); Faction owner = FactionManager.getByString(hub.ownerFactionId()); @@ -284,8 +290,11 @@ private static List connectionLore(Guild guild, SupplyHub hub, Installat if (installation == null) { return result; } + double tradeBonus = GuildModifierOverride.resolve(guild, GuildModifier.HUB_TRADE); + double productionBonus = GuildModifierOverride.resolve(guild, GuildModifier.HUB_PRODUCTION); for (Link link : HubNetwork.linksFor(guild)) { - if (link.fromProvince() != installation.getProvince()) { + if (link.fromProvince() != installation.getProvince() + || link.mode().getKind() != installation.getKind()) { continue; } Installation destination = null; @@ -301,8 +310,8 @@ private static List connectionLore(Guild guild, SupplyHub hub, Installat if (destination != null) { result.add("§7Sends to §f" + destination.getName() + " §7by " + link.mode().getKey() + " (" + Math.round(link.distance()) + " blocks): §e" - + Math.round(link.tradeFactor() * 100) + "% §7trade, §e" - + Math.round(link.productionFactor() * 100) + "% §7production"); + + Math.round(link.boostedTradeFactor(tradeBonus) * 100) + "% §7trade, §e" + + Math.round(link.boostedProductionFactor(productionBonus) * 100) + "% §7production"); } } return result;