Skip to content
Merged
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
229 changes: 173 additions & 56 deletions src/main/java/io/blert/challenges/chambers/CoxChallenge.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,12 +18,16 @@
import io.blert.events.ChallengeEndEvent;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.GameObject;
import net.runelite.api.Point;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.GameObjectSpawned;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.util.Text;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand All@@ -37,12 +41,6 @@ public class CoxChallenge extends RecordableChallenge {
Pattern.compile("The raid has begun!.*");
private static final Pattern RAID_COMPLETION_REGEX =
Pattern.compile("Congratulations - your raid is complete!.*");
// private static final Pattern RAID_COMPLETION_REGEX =
// Pattern.compile("(Combat room|Puzzle) `.*` complete! Duration: .*");
private static final Pattern ROOM_COMPLETE_REGEX =
Pattern.compile("(Combat room|Puzzle) `.*` complete! Duration: .*");
// private static final Pattern ROOM_COMPLETE_REGEX =
// Pattern.compile("Congratulations - your raid is complete!.*");
private static final Pattern FLOOR_COMPLETE_REGEX =
Pattern.compile(".* level complete! Duration: .*");
private static final Pattern MAP_LAYOUT_REGEX =
Expand All@@ -59,6 +57,12 @@ public class CoxChallenge extends RecordableChallenge {
private int maxCombatLevel = -1; // Cached max combat level for the party
private int avgMiningLevel = -1; // Cached average mining level for the party

// Obstacle tracking for collision-flag room completion detection (dey0 methodology).
// Indexed by CoxRoomUtil room type constant; obstacleP[i] == -1 means not registered.
private final int[] obstacleP = new int[16];
private final int[] obstacleX = new int[16];
private final int[] obstacleY = new int[16];

private static final List<Stage> COX_ROOM_ORDER = List.of(
// Stage.COX_THIEVING,
// Stage.COX_GUARDIANS,
Expand DownExpand Up@@ -132,18 +136,28 @@ protected void onTerminate() {
for (CoxNpc npc : CoxNpc.values()) {
npc.resetScaledHitpoints();
}
Arrays.fill(obstacleP, -1);
Arrays.fill(obstacleX, -1);
Arrays.fill(obstacleY, -1);
currentLocation = null;
enteredLobby = false;
setState(ChallengeState.INACTIVE);
}

@Override
protected void onTick() {
// Update current location
if (client.getLocalPlayer() != null) {
updateCurrentLocation(client.getLocalPlayer().getWorldLocation());
}

// Only use instance check for inRaid logic (like CoxTimersPlugin)
boolean inInstance = client.getTopLevelWorldView() != null && client.getTopLevelWorldView().isInstance();

if (inRaid && !inInstance) {
log.info("Detected raid exit: inInstance={}", inInstance);
if (getState() == ChallengeState.ACTIVE) {
endRaid(ChallengeState.INACTIVE);
endRaid(ChallengeState.COMPLETE);
}
inRaid = false;
} else if (!inRaid && inInstance) {
Expand All@@ -156,6 +170,40 @@ protected void onTick() {
if (roomDataTracker != null) {
roomDataTracker.tick();
}

// Poll collision flags to detect room completion (dey0 methodology).
if (getState() == ChallengeState.ACTIVE && roomDataTracker != null) {
Stage currentStage = roomDataTracker.getStage();
for (int i = 0; i < 16; i++) {
if (obstacleP[i] == -1) continue;
if (CoxRoomUtil.roomTypeToStage(i) != currentStage) continue;
int p = obstacleP[i];
int sceneX = obstacleX[i] - client.getTopLevelWorldView().getBaseX();
int sceneY = obstacleY[i] - client.getTopLevelWorldView().getBaseY();
if (p != client.getTopLevelWorldView().getPlane() || sceneX < 0 || sceneX >= 104 || sceneY < 0 || sceneY >= 104) {
obstacleP[i] = -1;
continue;
}
var collisionMaps = client.getTopLevelWorldView().getCollisionMaps();
if (collisionMaps == null) {
continue;
}
int flags = collisionMaps[p].getFlags()[sceneX][sceneY];
if ((flags & 0x100) == 0) {
int completionTick = getRelativeTick();
log.info("Room complete via collision flag: stage={}, tick={}", currentStage, completionTick);
obstacleP[i] = -1;
final RoomDataTracker tracker = roomDataTracker;
if (tracker == null) {
log.warn("Room completion detected but roomDataTracker is null");
return;
}
tracker.finishRoom(completionTick);
advanceToNextRoom(tracker, completionTick);
break;
}
}
}
}

@Nullable
Expand All@@ -164,6 +212,43 @@ protected Stage getStage() {
return roomDataTracker != null ? roomDataTracker.getStage() : null;
}

@Override
public void onGameObjectSpawned(GameObjectSpawned event) {
if (getState() == ChallengeState.ACTIVE && roomDataTracker != null) {
final RoomDataTracker tracker = roomDataTracker;
GameObject go = event.getGameObject();
switch (go.getId()) {
case 26209: // shamans / thieving / guardians
case 29741: // mystics
case 29749: // tightrope
case 29753: case 29754: case 29755: case 29756: case 29757: // crabs
case 29876: // ice demon
case 30016: // vasa
case 30017: // tekton / vanguards
case 30018: // muttadiles
case 30070: // vespula
Point pt = go.getSceneMinLocation();
int p = go.getPlane();
int sceneX = pt.getX();
int sceneY = pt.getY();
int template = client.getTopLevelWorldView().getInstanceTemplateChunks()[p][sceneX / 8][sceneY / 8];
int roomType = CoxRoomUtil.getRoomType(template);
if (roomType < 16) {
Stage expectedStage = CoxRoomUtil.roomTypeToStage(roomType);
if (expectedStage != null && expectedStage == tracker.getStage()) {
obstacleP[roomType] = p;
obstacleX[roomType] = sceneX + client.getTopLevelWorldView().getBaseX();
obstacleY[roomType] = sceneY + client.getTopLevelWorldView().getBaseY();
log.debug("Registered obstacle for room type {} (stage {}) at world ({},{})",
roomType, expectedStage, obstacleX[roomType], obstacleY[roomType]);
}
}
break;
}
}
super.onGameObjectSpawned(event);
}

@Override
public void onChatMessage(ChatMessage message) {
String stripped = Text.removeTags(message.getMessage());
Expand DownExpand Up@@ -201,6 +286,10 @@ public void onChatMessage(ChatMessage message) {
// endRaid must be delayed with its own invokeLater so it runs AFTER that
// StageUpdateEvent reaches the WebSocketEventHandler, otherwise CHALLENGE_END
// is sent to the server before the final stage update.
if (tracker == null) {
log.warn("Raid completion detected but roomDataTracker is null");
return;
}
tracker.finishLastRoom(currentTick);

// Clean up the old tracker properly
Expand All@@ -214,54 +303,20 @@ public void onChatMessage(ChatMessage message) {
return;
}

// Floor complete (dispatch floor event)
Matcher floorMatcher = FLOOR_COMPLETE_REGEX.matcher(stripped);
if (floorMatcher.find() && getState() == ChallengeState.ACTIVE) {
int currentTick = getRelativeTick();
final RoomDataTracker tracker = roomDataTracker; // Capture non-null value
tracker.finishRoom(currentTick);
log.info("Finished floor at tick {}", currentTick);
removeEventHandler(tracker);
roomDataTracker = null;
// Delay starting the next room to ensure the finish event is dispatched first
Stage nextStage = getNextStage(tracker.getStage());
if (nextStage != null) {
final int tickForNextRoom = currentTick; // Capture tick for lambda
getClientThread().invokeLater(() -> {
roomDataTracker = createRoomDataTracker(nextStage);
final RoomDataTracker newTracker = roomDataTracker; // Capture new non-null value
if (newTracker != null) {
newTracker.startRoom(tickForNextRoom);
log.info("Started tracking next room {} at tick {}", nextStage, tickForNextRoom);
}
});
// Floor completion detection (like dey0's plugin)
Matcher floorCompleteMatcher = FLOOR_COMPLETE_REGEX.matcher(stripped);
if (floorCompleteMatcher.matches() && getState() == ChallengeState.ACTIVE) {
final RoomDataTracker tracker = roomDataTracker;
if (tracker == null) {
log.warn("Floor completion detected but roomDataTracker is null");
return;
}
}

// Room complete (dispatch room event)
Matcher roomMatcher = ROOM_COMPLETE_REGEX.matcher(stripped);
if (roomMatcher.find() && roomDataTracker != null) {

int currentTick = getRelativeTick();
final RoomDataTracker tracker = roomDataTracker; // Capture non-null value
tracker.finishRoom(currentTick);

// Clean up the old tracker properly
removeEventHandler(tracker);
roomDataTracker = null;

// Delay starting the next room to ensure the finish event is dispatched first
Stage nextStage = getNextStage(tracker.getStage());
if (nextStage != null) {
final int tickForNextRoom = currentTick; // Capture tick for lambda
getClientThread().invokeLater(() -> {
roomDataTracker = createRoomDataTracker(nextStage);
final RoomDataTracker newTracker = roomDataTracker; // Capture new non-null value
if (newTracker != null) {
newTracker.startRoom(tickForNextRoom);
log.info("Started tracking next room {} at tick {}", nextStage, tickForNextRoom);
}
});
}
log.info("Finished floor at tick {} (detected via chat message)", currentTick);
advanceToNextRoom(tracker, currentTick);
return;
}
}

Expand All@@ -275,12 +330,31 @@ private int getRelativeTick() {
return client.getTickCount() - raidStartTick;
}

private void advanceToNextRoom(RoomDataTracker finishedTracker, int currentTick) {
removeEventHandler(finishedTracker);
roomDataTracker = null;
Stage nextStage = getNextStage(finishedTracker.getStage());
if (nextStage != null) {
getClientThread().invokeLater(() -> {
roomDataTracker = createRoomDataTracker(nextStage);
final RoomDataTracker newTracker = roomDataTracker;
if (newTracker != null) {
newTracker.startRoom(currentTick);
log.info("Started tracking next room {} at tick {}", nextStage, currentTick);
}
});
}
}

private void startRaid() {
inRaid = true;
setState(ChallengeState.ACTIVE);
raidStartTick = client.getTickCount();
startTick = 0; // relative to raid start

Arrays.fill(obstacleP, -1);
Arrays.fill(obstacleX, -1);
Arrays.fill(obstacleY, -1);

// Add the local player to the party to ensure scale is at least 1
addRaider(new Raider(client.getLocalPlayer(), true));

Expand All@@ -306,11 +380,12 @@ private void startRaid() {
private void endRaid(ChallengeState completionState) {
inRaid = false;
setState(ChallengeState.ENDING);
endTick = getRelativeTick();
endTick = getRelativeTick() - 1; // Adjust end tick to be the last tick of the raid, not the tick after completion message
int overallTime = endTick - startTick;
log.info("Raid end detected at tick {} (end/relative tick: {}). Start Tick: {} ticks", getTick(), endTick, startTick);
// Use parsed completion time if available, otherwise fall back to measured overall time.
int challengeTime = reportedChallengeTime > 0 ? reportedChallengeTime : overallTime;
log.info("Chambers of Xeric raid ended: challenge={}, overall={} ticks", challengeTime, overallTime);
dispatchEvent(new ChallengeEndEvent(overallTime, overallTime));
log.info("Chambers of Xeric raid ended: challenge={}, overall={} ticks", challengeTime, overallTime);
onTerminate();
Expand DownExpand Up@@ -588,17 +663,57 @@ private RoomDataTracker createRoomDataTracker(Stage stage) {
// Add methods for party management, room tracking, etc. as needed.

private static final int COX_LOBBY_REGION_ID = 4919;
// COX lobby area coordinates (center point from Mount Quidamortem bank)
// Fallback coordinates (center point from Mount Quidamortem bank)
private static final int COX_LOBBY_X = 1232;
private static final int COX_LOBBY_Y = 3572;
private static final int COX_LOBBY_Z = 0;
private static final int COX_LOBBY_RADIUS = 5; // Tiles from center point
private boolean enteredLobby = false;

@Nullable
private CoxLocation currentLocation = null;

/**
* Gets the current location in the raid.
*
* @return The current CoxLocation, or null if not in a valid COX location
*/
@Nullable
public CoxLocation getCurrentLocation() {
return currentLocation;
}

/**
* Updates the current location based on the player's world point.
* Called automatically on tick.
*
* @param worldPoint The player's current world point
*/
private void updateCurrentLocation(net.runelite.api.coords.WorldPoint worldPoint) {
CoxLocation newLocation = CoxLocation.fromWorldPoint(client, worldPoint);
if (newLocation != currentLocation) {
CoxLocation oldLocation = currentLocation;
currentLocation = newLocation;

if (currentLocation != null && oldLocation != null) {
log.debug("Location changed: {} -> {}", oldLocation, currentLocation);
}
}
}

@Override
public boolean containsLocation(net.runelite.api.coords.WorldPoint worldPoint) {
if (!enteredLobby) {
// Check if player is in the COX lobby area
// Use CoxLocation for precise instance template chunk detection
CoxLocation location = CoxLocation.fromWorldPoint(client, worldPoint);
if (location != null && location != CoxLocation.UNKNOWN) {
enteredLobby = true;
currentLocation = location;
log.debug("Entered COX raid at location: {}", location);
return true;
}

// Fallback to region-based detection for non-instance areas (outside lobby)
int regionId = worldPoint.getRegionID();
if (regionId == COX_LOBBY_REGION_ID) {
int dx = Math.abs(worldPoint.getX() - COX_LOBBY_X);
Expand All@@ -607,6 +722,8 @@ public boolean containsLocation(net.runelite.api.coords.WorldPoint worldPoint) {

if (dz == 0 && dx <= COX_LOBBY_RADIUS && dy <= COX_LOBBY_RADIUS) {
enteredLobby = true;
currentLocation = CoxLocation.LOBBY;
log.debug("Entered COX raid at lobby (fallback detection)");
return true;
}
}
Expand Down
Loading