Skip to content

Commit a1c9a4d

Browse files
authored
Merge pull request chsami#1460 from MakeCD/fix-getNpcs-function
fix(util/npc): Ensure thread safety in NPC fetching
2 parents a878a4e + eeb3a84 commit a1c9a4d

1 file changed

Lines changed: 82 additions & 76 deletions

File tree

  • runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc

runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java

Lines changed: 82 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ public static double getHealth(ActorModel npc) {
205205
return (double) ratio / (double) scale * 100;
206206
}
207207

208+
private static final Rs2NpcModel[] EMPTY_ARRAY = new Rs2NpcModel[0];
209+
208210
/**
209211
* Retrieves a stream of NPCs filtered by a given condition.
210212
*
@@ -216,88 +218,92 @@ public static double getHealth(ActorModel npc) {
216218
*/
217219
public static Stream<Rs2NpcModel> getNpcs(Predicate<Rs2NpcModel> predicate) {
218220
try {
219-
// Defensive null checks for client and world view
220-
if (Microbot.getClient() == null) {
221-
log.warn("Client is null, returning empty NPC stream");
222-
return Stream.empty();
223-
}
224-
225-
if (Microbot.getClient().getTopLevelWorldView() == null) {
226-
log.warn("TopLevelWorldView is null, returning empty NPC stream");
227-
return Stream.empty();
228-
}
229-
230-
if (Microbot.getClient().getTopLevelWorldView().npcs() == null) {
231-
log.warn("NPCs collection is null, returning empty NPC stream");
232-
return Stream.empty();
233-
}
234-
235-
if (Microbot.getClient().getLocalPlayer() == null) {
236-
log.warn("Local player is null, returning empty NPC stream");
237-
return Stream.empty();
238-
}
239-
240-
if (Microbot.getClient().getLocalPlayer().getLocalLocation() == null) {
241-
log.warn("Local player location is null, returning empty NPC stream");
242-
return Stream.empty();
243-
}
244-
245-
// Make local copies to avoid null issues during stream processing
246-
final Stream<? extends NPC> npcStream = Microbot.getClient().getTopLevelWorldView().npcs().stream();
247-
final LocalPoint playerLocation = Microbot.getClient().getLocalPlayer().getLocalLocation();
248-
249-
// Safe predicate wrapper to prevent null issues
250-
Predicate<Rs2NpcModel> safePredicate = predicate != null ? predicate : (npc -> true);
251-
List<Rs2NpcModel> npcList = npcStream
252-
.filter(Objects::nonNull) // Filter out null NPCs
253-
.map(npc -> {
254-
try {
255-
return new Rs2NpcModel(npc);
256-
} catch (Exception e) {
257-
log.debug("Error creating Rs2NpcModel: {}", e.getMessage());
258-
return null;
259-
}
260-
})
261-
.filter(Objects::nonNull) // Filter out failed model creations
262-
.filter(npcModel -> {
263-
try {
264-
// Additional safety checks for Rs2NpcModel
265-
return npcModel.getName() != null &&
266-
npcModel.getLocalLocation() != null;
267-
} catch (Exception e) {
268-
log.debug("Error accessing Rs2NpcModel properties: {}", e.getMessage());
269-
return false;
270-
}
271-
})
272-
.filter(npcModel -> {
273-
try {
274-
return safePredicate.test(npcModel);
275-
} catch (Exception e) {
276-
log.debug("Error in predicate test: {}", e.getMessage());
277-
return false;
278-
}
279-
})
280-
.sorted(Comparator.comparingInt(value -> {
281-
try {
282-
if (value != null && value.getLocalLocation() != null && playerLocation != null) {
283-
return value.getLocalLocation().distanceTo(playerLocation);
221+
// Execute all game object access on client thread to prevent race conditions
222+
Rs2NpcModel[] npcArray = Microbot.getClientThread().runOnClientThreadOptional(() -> {
223+
// Defensive null checks for client and world view
224+
if (Microbot.getClient() == null) {
225+
log.warn("Client is null, returning empty NPC stream");
226+
return EMPTY_ARRAY;
227+
}
228+
229+
if (Microbot.getClient().getTopLevelWorldView() == null) {
230+
log.warn("TopLevelWorldView is null, returning empty NPC stream");
231+
return EMPTY_ARRAY;
232+
}
233+
234+
if (Microbot.getClient().getTopLevelWorldView().npcs() == null) {
235+
log.warn("NPCs collection is null, returning empty NPC stream");
236+
return EMPTY_ARRAY;
237+
}
238+
239+
if (Microbot.getClient().getLocalPlayer() == null) {
240+
log.warn("Local player is null, returning empty NPC stream");
241+
return EMPTY_ARRAY;
242+
}
243+
244+
if (Microbot.getClient().getLocalPlayer().getLocalLocation() == null) {
245+
log.warn("Local player location is null, returning empty NPC stream");
246+
return EMPTY_ARRAY;
247+
}
248+
249+
// Make local copies to avoid null issues during stream processing
250+
final Stream<? extends NPC> npcStream = Microbot.getClient().getTopLevelWorldView().npcs().stream();
251+
final LocalPoint playerLocation = Microbot.getClient().getLocalPlayer().getLocalLocation();
252+
253+
// Safe predicate wrapper to prevent null issues
254+
Predicate<Rs2NpcModel> safePredicate = predicate != null ? predicate : (npc -> true);
255+
return npcStream
256+
.filter(Objects::nonNull) // Filter out null NPCs
257+
.map(npc -> {
258+
try {
259+
return new Rs2NpcModel(npc);
260+
} catch (Exception e) {
261+
log.debug("Error creating Rs2NpcModel: {}", e.getMessage());
262+
return null;
263+
}
264+
})
265+
.filter(Objects::nonNull) // Filter out failed model creations
266+
.filter(npcModel -> {
267+
try {
268+
// Additional safety checks for Rs2NpcModel
269+
return npcModel.getName() != null &&
270+
npcModel.getLocalLocation() != null;
271+
} catch (Exception e) {
272+
log.debug("Error accessing Rs2NpcModel properties: {}", e.getMessage());
273+
return false;
284274
}
285-
return Integer.MAX_VALUE; // Put problematic NPCs at the end
286-
} catch (Exception e) {
287-
log.debug("Error calculating distance: {}", e.getMessage());
288-
return Integer.MAX_VALUE;
289-
}
290-
}))
291-
.collect(Collectors.toList());
292-
293-
return npcList.stream();
294-
275+
})
276+
.filter(npcModel -> {
277+
try {
278+
return safePredicate.test(npcModel);
279+
} catch (Exception e) {
280+
log.debug("Error in predicate test: {}", e.getMessage());
281+
return false;
282+
}
283+
})
284+
.sorted(Comparator.comparingInt(value -> {
285+
try {
286+
if (value != null && value.getLocalLocation() != null && playerLocation != null) {
287+
return value.getLocalLocation().distanceTo(playerLocation);
288+
}
289+
return Integer.MAX_VALUE; // Put problematic NPCs at the end
290+
} catch (Exception e) {
291+
log.debug("Error calculating distance: {}", e.getMessage());
292+
return Integer.MAX_VALUE;
293+
}
294+
}))
295+
.toArray(Rs2NpcModel[]::new);
296+
}).orElse(EMPTY_ARRAY);
297+
298+
// Convert array back to stream for API compatibility
299+
return Arrays.stream(npcArray);
300+
295301
} catch (Exception e) {
296302
log.debug("Unexpected error in getNpcs: {}", e.getMessage(), e);
297303
return Stream.empty();
298304
}
299305
}
300-
306+
301307
/**
302308
* Retrieves a stream of all NPCs in the game world.
303309
*

0 commit comments

Comments
 (0)