Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 126 additions & 9 deletions src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@


import com.google.inject.Provides;
import com.toofifty.goaltracker.models.enums.Status;
import com.toofifty.goaltracker.models.enums.TaskType;
import com.toofifty.goaltracker.models.task.ItemTask;
import com.toofifty.goaltracker.models.task.QuestTask;
import com.toofifty.goaltracker.models.task.SkillLevelTask;
import com.toofifty.goaltracker.models.task.SkillXpTask;
import com.toofifty.goaltracker.models.task.Task;
import com.toofifty.goaltracker.services.TaskIconService;
import com.toofifty.goaltracker.services.TaskUpdateService;
Expand Down Expand Up @@ -229,17 +231,28 @@ protected void startUp()
}

goalTrackerPanel.onGoalUpdated((goal) -> goalManager.save());

goalTrackerPanel.onTaskAdded((task) -> {
if (taskUpdateService.update(task)) {
// Send directly to the client thread to fetch live player stats
clientThread.invokeLater(() -> {
// Populate the live metrics into memory instantly upon creation
taskUpdateService.update(task);

// Perform the disk write safely on the background game thread
goalManager.save();

// If the task is instantly completed, notify the player
if (task.getStatus().isCompleted()) {
notifyTask(task);
}

uiStatusManager.refresh(task);
}

goalManager.save();
// Send to the UI thread to handle screen graphics
SwingUtilities.invokeLater(() -> {
uiStatusManager.refresh(task);
});
});
});

goalTrackerPanel.onTaskUpdated((task) -> goalManager.save());

// Preload item icons at plugin startup so they are visible immediately
Expand Down Expand Up @@ -271,9 +284,49 @@ public void onSessionOpen(SessionOpen event)
@Subscribe
public void onStatChanged(StatChanged event)
{
boolean anyTaskChanged = false;

// 1. Process Skill Level task updates
List<SkillLevelTask> skillLevelTasks = goalManager.getIncompleteTasksByType(TaskType.SKILL_LEVEL);
for (SkillLevelTask task : skillLevelTasks) {
// If this skill level task did not receive a status change
if (!taskUpdateService.update(task, event)) continue;
// If the skill level task DID receive a status change
else {
anyTaskChanged = true;

// Update the UI immediately to reflect the new status
uiStatusManager.refresh(task);

// If we completed the task, notify the player
if (task.getStatus().isCompleted()) {
notifyTask(task);
}
}
}

// 2. Process Skill XP task updates
List<SkillXpTask> skillXpTasks = goalManager.getIncompleteTasksByType(TaskType.SKILL_XP);
for (SkillXpTask task : skillXpTasks) {
// If this skill XP task did not receive a status change
if (!taskUpdateService.update(task, event)) continue;
// If the skill XP task DID receive a status change
else {
anyTaskChanged = true;

// Update the UI immediately to reflect the new status
uiStatusManager.refresh(task);

// If we completed the task, notify the player
if (task.getStatus().isCompleted()) {
notifyTask(task);
}
}
}

// Save once if any status changes occurred
if (anyTaskChanged) {
goalManager.save();
}
}

Expand All @@ -282,11 +335,24 @@ public void onGameStateChanged(GameStateChanged event)
{
if (event.getGameState() == GameState.LOGGED_IN)
{
// Re-check quest tasks after login
clientThread.invokeLater(() -> refreshQuestTasks());
// Defer task refreshes until the player's data is loaded in, preventing a race condition.
// Would otherwise cause a check of level-0/0xp against tasks, invalidating our login check entirely.
clientThread.invokeLater(() -> {

// Refresh the panel once, 10s after login, after detection settles
schedulePanelRefresh(10_000);
// If player data hasn't loaded from the server yet, wait and try again next frame
if (client.getRealSkillLevel(Skill.ATTACK) <= 0) { return false; }

// Refresh tasks now that player data exists
refreshQuestTasks();
refreshSkillLevelTasks();
refreshSkillXpTasks();

// Give the UI a moment to settle after data updates before repainting the panel
schedulePanelRefresh(200);

// Stop the refresh frame loop
return true;
});
}
}

Expand Down Expand Up @@ -474,6 +540,26 @@ private static String normalizeBarrowsName(final String raw)
return s;
}

/**
* Safe, multithreaded entry point to force a full data validation sweep across all task types.
* Typically used after batch mutations like adding the quest prerequisites.
*/
public void refreshAllTasks(Runnable onCompleteUIHandler)
{
// Force the execution to run safely on the OSRS client thread
clientThread.invokeLater(() -> {
refreshSkillLevelTasks();
refreshQuestTasks();
refreshSkillXpTasks();

// If the caller provided a UI update script, bounce it back to the Swing thread
if (onCompleteUIHandler != null)
{
javax.swing.SwingUtilities.invokeLater(onCompleteUIHandler);
}
});
}

private void refreshQuestTasks()
{
if (goalManager == null || client == null) return;
Expand All @@ -488,6 +574,37 @@ private void refreshQuestTasks()
}
}
}

private void refreshSkillLevelTasks()
{
if (goalManager == null || client == null) return;
List<SkillLevelTask> skillLevelTasks = goalManager.getIncompleteTasksByType(TaskType.SKILL_LEVEL);
for (SkillLevelTask task : skillLevelTasks)
{
task.refreshStatus(client);
uiStatusManager.refresh(task);
if (task.getStatus().isCompleted())
{
notifyTask(task);
}
}
}

private void refreshSkillXpTasks()
{
if (goalManager == null || client == null) return;
List<SkillXpTask> skillXpTasks = goalManager.getIncompleteTasksByType(TaskType.SKILL_XP);
for (SkillXpTask task : skillXpTasks)
{
task.refreshStatus(client);
uiStatusManager.refresh(task);
if (task.getStatus().isCompleted())
{
notifyTask(task);
}
}
}

@Provides
public GoalTrackerConfig provideConfig(ConfigManager configManager)
{
Expand Down
21 changes: 17 additions & 4 deletions src/main/java/com/toofifty/goaltracker/models/Goal.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,34 @@ public final class Goal
@Builder.Default
private ReorderableList<Task> tasks = new ReorderableList<>();

/**
* Safely retrieves the collection of tasks inside this goal.
* Intercepts null pointers injected during malformed third-party JSON imports
*/
public ReorderableList<Task> getTasks()
{
if (this.tasks == null)
{
this.tasks = new ReorderableList<>();
}
return this.tasks;
}

private List<Task> filterBy(Predicate<Task> predicate)
{
return tasks.stream().filter(predicate).collect(Collectors.toList());
return getTasks().stream().filter(predicate).collect(Collectors.toList());
}

/** True if all tasks are of the given status. */
public boolean isStatus(Status status)
{
return tasks.stream().allMatch(task -> task.getStatus() == status);
return getTasks().stream().allMatch(task -> task.getStatus() == status);
}

/** True if any task matches one of the given statuses. */
public boolean isAnyStatus(Status... statuses)
{
return tasks.stream().anyMatch(task ->
return getTasks().stream().anyMatch(task ->
Arrays.stream(statuses).anyMatch(s -> s == task.getStatus()));
}

Expand All @@ -77,7 +90,7 @@ public Status getStatus()
/** Mark all tasks as complete or not started. */
public void setAllTasksCompleted(boolean completed)
{
for (Task task : tasks)
for (Task task : getTasks())
{
task.setStatus(completed ? Status.COMPLETED : Status.NOT_STARTED);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.toofifty.goaltracker.models.task;

import com.toofifty.goaltracker.models.enums.TaskType;
import com.toofifty.goaltracker.models.enums.Status;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.runelite.api.Skill;
import net.runelite.api.Client;

@Setter
@Getter
Expand All @@ -16,23 +18,74 @@
public final class SkillLevelTask extends Task
{
private Skill skill;
private int level;
private int targetSkillLevel;
private int currentSkillLevel;

@Override
public String getDisplayName()
{
return String.format("Reach level %d %s", level, skill.getName());
return String.format("Reach level %d %s", targetSkillLevel, skill.getName());
}

@Override
public String toString()
{
return String.format("%s %s", level, skill.getName());
return String.format("%s %s", targetSkillLevel, skill.getName());
}

@Override
public TaskType getType()
{
return TaskType.SKILL_LEVEL;
}

/**
* Re-evaluate the player's level in the skill and update the Task.status field.
* Safe to call on login and whenever relevant varbits/varps change.
*/
public void refreshStatus(final Client client)
{
if (client == null || skill == null)
{
return;
}


currentSkillLevel = client.getRealSkillLevel(skill);


if(hasReachedTargetLevel())
{
setStatus(Status.COMPLETED);
}
// This covers cases where players log into a separate character that no longer meets the task requirements
else {
setStatus(Status.NOT_STARTED);
}
}

/**
* Whether the player has reached the target level for this task.
* @return <ul>
* <li>{@code true} - Target level <b>HAS</b> been reached</li>
* <li>{@code false} - Target level <b>HAS NOT</b> been reached</li>
* </ul>
*/
public boolean hasReachedTargetLevel()
{
return (currentSkillLevel >= targetSkillLevel);
}

/**
* Whether the player would reach the target level for this task given the level provided.
* @param level The level to check against
* @return <ul>
* <li>{@code true} - Target level <b>HAS</b> been reached</li>
* <li>{@code false} - Target level <b>HAS NOT</b> been reached</li>
* </ul>
*/
public boolean wouldReachTargetLevel(int level)
{
return (level >= targetSkillLevel);
}
}
Loading