diff --git a/src/main/java/com/duckblade/osrs/sailing/SailingConfig.java b/src/main/java/com/duckblade/osrs/sailing/SailingConfig.java index 36205a0f..022d2fec 100644 --- a/src/main/java/com/duckblade/osrs/sailing/SailingConfig.java +++ b/src/main/java/com/duckblade/osrs/sailing/SailingConfig.java @@ -783,6 +783,60 @@ default Color salvagingHighLevelWrecksColour() return ColorUtil.colorWithAlpha(Color.RED, 64); } + @ConfigItem( + keyName = "salvagingOverlayEnabled", + name = "Salvaging Overlay", + description = "Small overlay showing crew and player salvaging status.", + section = SECTION_SALVAGING, + position = 7 + ) + default boolean salvagingOverlayEnabled() { return false; } + + @ConfigItem( + keyName = "salvagingNotifCrewStop", + name = "Notify on Crew Stop", + description = "Notify when crewmates stop salvaging.", + section = SECTION_SALVAGING, + position = 8 + ) + default Notification salvagingNotifCrewStop() { return Notification.OFF; } + + @ConfigItem( + keyName = "salvagingNotifCrewStart", + name = "Notify on Crew Start", + description = "Notify when crewmates start salvaging.", + section = SECTION_SALVAGING, + position = 9 + ) + default Notification salvagingNotifCrewStart() { return Notification.OFF; } + + @ConfigItem( + keyName = "salvagingNotifPlayerStop", + name = "Notify on Player Stop", + description = "Notify when the player stops salvaging.", + section = SECTION_SALVAGING, + position = 10 + ) + default Notification salvagingNotifPlayerStop() { return Notification.OFF; } + + @ConfigItem( + keyName = "salvagingNotifSortStop", + name = "Notify on Sorting stop", + description = "Notify when the player stops sorting salvage.", + section = SECTION_SALVAGING, + position = 11 + ) + default Notification salvagingNotifSortStop() { return Notification.OFF; } + + @ConfigItem( + keyName = "salvagingNotifCargoFull", + name = "Notify on Full Cargo", + description = "Notify when the ships cargo is full", + section = SECTION_SALVAGING, + position = 12 + ) + default Notification salvagingNotifCargoFull() { return Notification.OFF; } + @ConfigItem( keyName = "cargoHoldDummy", name = "Under Development", diff --git a/src/main/java/com/duckblade/osrs/sailing/features/salvaging/SalvagingNotification.java b/src/main/java/com/duckblade/osrs/sailing/features/salvaging/SalvagingNotification.java new file mode 100644 index 00000000..c4493a53 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/salvaging/SalvagingNotification.java @@ -0,0 +1,323 @@ +package com.duckblade.osrs.sailing.features.salvaging; + +import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.features.util.SailingUtil; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import com.google.common.collect.ImmutableSet; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import net.runelite.api.events.*; +import net.runelite.api.gameval.DBTableID; +import net.runelite.api.gameval.ObjectID; +import net.runelite.client.Notifier; +import net.runelite.client.eventbus.Subscribe; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.*; + +@Slf4j +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class SalvagingNotification implements PluginLifecycleComponent +{ + + private static final ImmutableSet SALVAGE_WRECKS = ImmutableSet.of( + ObjectID.SAILING_SMALL_SHIPWRECK, + ObjectID.SAILING_SMALL_SHIPWRECK_STUMP, + ObjectID.SAILING_FISHERMAN_SHIPWRECK, + ObjectID.SAILING_FISHERMAN_SHIPWRECK_STUMP, + ObjectID.SAILING_BARRACUDA_SHIPWRECK, + ObjectID.SAILING_BARRACUDA_SHIPWRECK_STUMP, + ObjectID.SAILING_LARGE_SHIPWRECK, + ObjectID.SAILING_LARGE_SHIPWRECK_STUMP, + ObjectID.SAILING_PIRATE_SHIPWRECK, + ObjectID.SAILING_PIRATE_SHIPWRECK_STUMP, + ObjectID.SAILING_MERCENARY_SHIPWRECK, + ObjectID.SAILING_MERCENARY_SHIPWRECK_STUMP, + ObjectID.SAILING_FREMENNIK_SHIPWRECK, + ObjectID.SAILING_FREMENNIK_SHIPWRECK_STUMP, + ObjectID.SAILING_MERCHANT_SHIPWRECK, + ObjectID.SAILING_MERCHANT_SHIPWRECK_STUMP + ); + + private final Client client; + private final SailingConfig config; + private final Notifier notifier; + + @Getter + private boolean playerSalvaging = false; + @Getter + private boolean cargoFull = false; + + private boolean playerSorting = false; + private boolean playerSalvageTracker = false; + private boolean playerSortTracker = false; + private boolean crewSalvageTracker = false; + private boolean fullCargoTracker = false; + + @Getter + private final Set crewmates = new HashSet<>(); + + @Getter + private final HashMap crewSalvaging = new HashMap<>(); + private final Set crewNames = new HashSet<>(); + private final Set wrecks = new HashSet<>(); + private final HashMap crewIdleTicks = new HashMap<>(); + + @Override + public boolean isEnabled(SailingConfig config) + { + boolean activeNotifCrewStop = config.salvagingNotifCrewStop().isEnabled(); + boolean activeNotifCrewStart = config.salvagingNotifCrewStart().isEnabled(); + boolean activeNotifPlayerStop = config.salvagingNotifPlayerStop().isEnabled(); + boolean activeNotifSortStop = config.salvagingNotifSortStop().isEnabled(); + boolean activeNotifCargoFull = config.salvagingNotifCargoFull().isEnabled(); + + return + activeNotifCrewStop || activeNotifCrewStart || + activeNotifPlayerStop || activeNotifSortStop || + activeNotifCargoFull; + } + + public boolean atSalvage() + { + return SailingUtil.isSailing(client) && !wrecks.isEmpty(); + } + + @Subscribe + // Sometimes it doesn't call NpcDespawn on a world hop, so we manually clear them + public void onGameStateChanged(GameStateChanged state) + { + if (state.getGameState() == GameState.LOGGING_IN || state.getGameState() == GameState.HOPPING) + { + crewSalvaging.clear(); + crewIdleTicks.clear(); + crewmates.clear(); + playerSalvageTracker = false; + playerSortTracker = false; + crewSalvageTracker = false; + fullCargoTracker = false; + } + } + + @Subscribe + // Unloaded world views don't always call the ObjectDespawn event, so make sure we clear that + public void onWorldViewUnloaded(WorldViewUnloaded e) + { + if (e.getWorldView().isTopLevel()) + { + wrecks.clear(); + } + } + + @Subscribe + public void onNpcSpawned(NpcSpawned event) + { + NPC npc = event.getNpc(); + if (!isCrewName(npc.getName()) || npc.getWorldView() != client.getLocalPlayer().getWorldView()) + { + return; + } + + if (!crewmates.contains(npc)) { + log.debug("Found Crewmate: {}, ID: {}", npc.getName(), npc.getId()); + crewmates.add(npc); + crewSalvaging.put(npc, false); + crewIdleTicks.put(npc, 0); + } + } + + @Subscribe + public void onNpcDespawned(NpcDespawned event) + { + NPC npc = event.getNpc(); + crewmates.remove(npc); + crewSalvaging.remove(npc); + crewIdleTicks.remove(npc); + } + + @Subscribe + public void onGameObjectSpawned(GameObjectSpawned obj) + { + int objId = obj.getGameObject().getId(); + if (SALVAGE_WRECKS.contains(objId) && !wrecks.contains(obj.getGameObject())) + { + log.debug("Adding Shipwreck with ID: {}", objId); + wrecks.add(obj.getGameObject()); + } + } + + @Subscribe + public void onGameObjectDespawned(GameObjectDespawned obj) + { + wrecks.remove(obj.getGameObject()); + } + + @Subscribe + //Tracking crew idle ticks so we don't spam notifs when wrecks swap, cap at 10 so it doesn't overflow + public void onGameTick(GameTick tick) + { + for (Actor crew : crewmates) + { + int currentIdle = crewIdleTicks.get(crew); + if (!crewSalvaging.get(crew) && currentIdle < 10) + { + crewIdleTicks.replace(crew, currentIdle + 1); + handleSalvageUpdate(); + } + } + } + + @Subscribe + public void onAnimationChanged(AnimationChanged animationChanged) + { + final Actor actor = animationChanged.getActor(); + final int anim = actor.getAnimation(); + + if (actor.getWorldView().isTopLevel()) + { + return; + } + + if (SailingUtil.isLocalPlayer(client, actor)) + { + playerSalvaging = isAnimationSalvaging(anim); + playerSorting = (anim == 13599); + } + else if (crewmates.contains(actor)) + { + if (isAnimationSalvaging(anim)) + { + if (cargoFull) + { + cargoFull = false; + } + crewSalvaging.replace(actor, true); + } + else + { + crewSalvaging.replace(actor, false); + crewIdleTicks.replace(actor, 0); + } + } + handleSalvageUpdate(); + } + + @Subscribe + public void onChatMessage(ChatMessage msg) + { + if (msg.getType() != ChatMessageType.SPAM) + { + return; + } + if (msg.getMessage().equals("Your crewmate on the salvaging hook cannot salvage as the cargo hold is full.")) + { + cargoFull = true; + handleSalvageUpdate(); + } + } + + private void handleSalvageUpdate() + { + //Player state has updated + if (playerSalvaging != playerSalvageTracker) + { + if (!playerSalvaging && atSalvage()) + { + notifier.notify(config.salvagingNotifPlayerStop(), "Salvaging: Player stopped salvaging"); + } + playerSalvageTracker = playerSalvaging; + } + + boolean crewSalvage = crewSalvaging.values().stream().anyMatch(b -> b); + //Crew salvage state has updated + if (crewSalvage != crewSalvageTracker) + { + if (crewSalvage) + { + if(atSalvage()) + { + notifier.notify(config.salvagingNotifCrewStart(), "Salvaging: Crew started salvaging"); + } + crewSalvageTracker = true; + } + else + { + if(maxCrewIdleTicks() > 3) + { + if(atSalvage()) + { + notifier.notify(config.salvagingNotifCrewStop(), "Salvaging: Crew stopped salvaging"); + } + crewSalvageTracker = false; + } + } + } + + //Sorting state has updated + if (playerSorting != playerSortTracker) + { + if (!playerSorting && atSalvage()) + { + notifier.notify(config.salvagingNotifSortStop(), "Salvaging: Player stopped sorting salvage"); + } + playerSortTracker = playerSorting; + } + + if(cargoFull != fullCargoTracker) + { + if (cargoFull && atSalvage()) + { + notifier.notify(config.salvagingNotifCargoFull(), "Salvaging: Full cargo"); + } + fullCargoTracker = cargoFull; + } + } + + private boolean isAnimationSalvaging(int anim) + { + return anim == 13576 || anim == 13577 || anim == 13584 || anim == 13583; + } + + private boolean isCrewName(String name) + { + if (crewNames.isEmpty()) + { + populateCrewNameList(); + } + return crewNames.contains(name); + } + + private void populateCrewNameList() + { + List rows = client.getDBTableRows(DBTableID.SailingCrew.ID); + for (int row : rows) + { + Object[] npcs = client.getDBTableField(row, DBTableID.SailingCrew.COL_CARGO_NPC, 0); + for (Object npc : npcs) + { + NPCComposition npcDef = client.getNpcDefinition((int) npc); + crewNames.add(npcDef.getName()); + } + } + } + + //Get the maximum idle tick of a crewmate that isn't at the max + private int maxCrewIdleTicks() + { + int max = 0; + for (int i : crewIdleTicks.values()) + { + if (i > max && i != 10) + { + max = i; + } + } + return max; + } + + +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/features/salvaging/SalvagingOverlay.java b/src/main/java/com/duckblade/osrs/sailing/features/salvaging/SalvagingOverlay.java new file mode 100644 index 00000000..4044aaad --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/salvaging/SalvagingOverlay.java @@ -0,0 +1,73 @@ +package com.duckblade.osrs.sailing.features.salvaging; + +import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.TitleComponent; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.awt.*; + +@Singleton +public class SalvagingOverlay extends OverlayPanel implements PluginLifecycleComponent +{ + private final SailingConfig config; + private final SalvagingNotification salvagingNotification; + + @Inject + public SalvagingOverlay( SailingConfig config, SalvagingNotification salvagingNotification) + { + this.config = config; + this.salvagingNotification = salvagingNotification; + } + + @Override + public boolean isEnabled(SailingConfig config) + { + return config.salvagingOverlayEnabled(); + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (!salvagingNotification.atSalvage() || !config.salvagingOverlayEnabled()) + { + return null; + } + boolean playerSalvaging = salvagingNotification.isPlayerSalvaging(); + + String player = playerSalvaging ? "Yes":"No"; + Color playerColor = playerSalvaging ? Color.GREEN:Color.RED; + + int crewCount = salvagingNotification.getCrewmates().size(); + int crewSalvaging = (int) salvagingNotification.getCrewSalvaging().values().stream().filter(b -> b).count(); + String crewSalvage = crewSalvaging + "/" + crewCount; + Color crewSalvageColor = (crewSalvaging > 0) ? Color.GREEN:Color.RED; + + panelComponent.getChildren().add(TitleComponent.builder().text("Salvaging").color(Color.WHITE).build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Player Salvaging:").leftColor(playerColor) + .right(player).rightColor(playerColor) + .build()); + panelComponent.getChildren().add(LineComponent.builder() + .left("Crew Salvaging:").leftColor(crewSalvageColor) + .right(crewSalvage).rightColor(crewSalvageColor) + .build()); + + if (salvagingNotification.isCargoFull()) + { + panelComponent.getChildren().add(LineComponent.builder() + .left("Cargo:").leftColor(Color.RED) + .right("FULL").rightColor(Color.RED) + .build()); + } + + + panelComponent.setPreferredSize(new Dimension(graphics.getFontMetrics().stringWidth("Player Salvaging: Yes") + 10, 0)); + + return super.render(graphics); + } +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/module/SailingModule.java b/src/main/java/com/duckblade/osrs/sailing/module/SailingModule.java index c94b9d7a..afb92ee1 100644 --- a/src/main/java/com/duckblade/osrs/sailing/module/SailingModule.java +++ b/src/main/java/com/duckblade/osrs/sailing/module/SailingModule.java @@ -40,6 +40,8 @@ import com.duckblade.osrs.sailing.features.oceanencounters.MysteriousGlow; import com.duckblade.osrs.sailing.features.oceanencounters.OceanMan; import com.duckblade.osrs.sailing.features.salvaging.SalvagingHighlight; +import com.duckblade.osrs.sailing.features.salvaging.SalvagingNotification; +import com.duckblade.osrs.sailing.features.salvaging.SalvagingOverlay; import com.duckblade.osrs.sailing.features.util.BoatTracker; import com.google.common.collect.ImmutableSet; import com.google.inject.AbstractModule; @@ -96,6 +98,8 @@ Set lifecycleComponents( RapidsOverlay rapidsOverlay, ReverseBeep reverseBeep, SalvagingHighlight salvagingHighlight, + SalvagingOverlay salvagingOverlay, + SalvagingNotification salvagingNotification, SeaChartMapPointManager seaChartMapPointManager, SeaChartOverlay seaChartOverlay, SeaChartPanelOverlay seaChartPanelOverlay, @@ -139,6 +143,8 @@ Set lifecycleComponents( .add(rapidsOverlay) .add(reverseBeep) .add(salvagingHighlight) + .add(salvagingOverlay) + .add(salvagingNotification) .add(seaChartOverlay) .add(seaChartMapPointManager) .add(seaChartPanelOverlay)