Cats on bridges, no more infinite familiar call loops

This commit is contained in:
dam 2026-07-04 03:33:17 +03:00
parent 5a37f2f8da
commit 8f855105b9
No known key found for this signature in database
GPG key ID: 4AF4E722399663FB
3 changed files with 54 additions and 5 deletions

View file

@ -115,6 +115,14 @@ public final class HouseZone extends MapZone {
return true;
}
}
// Take the player's familiar along before the teardown pulse below wipes every NPC
// still left inside the house - a wiped familiar is gone until relog (GL #2313). The
// call either materializes it at the destination or parks it invisibly at the owner's
// side until there is room for it.
if (!logout && p.getFamiliarManager().hasFamiliar()) {
p.getFamiliarManager().getFamiliar().call();
}
remove_items(p);
// The below tears down the house if the owner was the one who left

View file

@ -391,10 +391,17 @@ public abstract class Familiar extends NPC implements Plugin<Object> {
return false;
}
@Override
public void onRegionInactivity() {
call();
// NPC.tick() short-circuits into this method while the region is inactive, so Entity.tick()
// never runs to consume a teleport that call() just set; apply it manually here or the
// familiar re-calls forever without ever moving (GL #2313).
if (getProperties().getTeleportLocation() != null && owner.isActive()) {
getWalkingQueue().update();
getUpdateMasks().prepare(this);
}
}
@Override
@ -699,10 +706,20 @@ public abstract class Familiar extends NPC implements Plugin<Object> {
//owner.getPacketDispatch().sendMessage("Your familiar is too big to fit here. Try calling it again when you are standing");
//owner.getPacketDispatch().sendMessage("somewhere with more space.");
//spamTimer = 50;
// No room to materialize, so wait invisibly at the owner's side (like init() does when
// there is no room at summon time). Waiting in place instead means being left behind:
// once the abandoned region unloads, the familiar either gets wiped with it
// (DynamicRegion teardown) or gets stuck endlessly re-calling from an inactive region
// that no longer ticks it normally (GL #2313).
setInvisible(true);
stayWithOwner();
return false;
}
setInvisible(owner.getZoneMonitor().isRestricted(ZoneRestriction.FOLLOWERS) && !owner.getLocks().isLocked("enable_summoning"));
if (isInvisible()) return true;
if (isInvisible()) {
stayWithOwner();
return true;
}
getProperties().setTeleportLocation(destination);
if (!(this instanceof Pet)) {
if (firstCall) {
@ -723,13 +740,33 @@ public abstract class Familiar extends NPC implements Plugin<Object> {
} else {
face(owner);
}
if (!isRenderable() && owner.isActive()) {
// A familiar pulled in from another region must move immediately rather than on its next
// tick: NPCs tick before players, so when this call runs from the owner's movement (e.g.
// leaving a POH), the familiar's tick for this cycle has already passed - and a region
// teardown pulse (POH exit portal, building-mode rebuild) runs at the start of the next
// cycle, wiping any NPC still inside before it can consume a pending teleport (GL #2313).
boolean leftInAnotherRegion = getLocation().getRegionId() != owner.getLocation().getRegionId();
if ((!isRenderable() || leftInAnotherRegion) && owner.isActive()) {
// log(this.getClass(), Log.ERR, "Familiar in inactive region!");
getWalkingQueue().update();
getUpdateMasks().prepare(this);
}
return true;
}
/**
* Moves a familiar that cannot materialize to its owner's location, so it is never left
* behind in a region that may unload. The teleport is applied immediately because this can
* run from an inactive region, where Entity.tick() (the normal consumer) no longer runs.
*/
private void stayWithOwner() {
if (!owner.isActive() || getLocation().equals(owner.getLocation())) {
return;
}
getProperties().setTeleportLocation(owner.getLocation());
getWalkingQueue().update();
getUpdateMasks().prepare(this);
}
/**
* Gets the spawning location of the familiar.

View file

@ -332,10 +332,14 @@ object RegionManager {
}
}
val l = owner.location.transform(stepX, stepY, 0)
// Check if ALL target tiles are unclipped
// Check if the NPC can stand on ALL target tiles. This must not use isClipped(),
// which rejects any tile that has a wall along ANY of its sides: every tile of a
// railed bridge deck has one (e.g. the whole Lumbridge bridge), which made familiars
// and pets unable to materialize anywhere on it (GL #1471). Materializing is the
// same question as teleporting, so ask the teleport predicate.
for (x in 0 until node.size()) {
for (y in 0 until node.size()) {
if (isClipped(l.transform(x, y, 0))) {
if (!isTeleportPermitted(l.transform(x, y, 0))) {
continue@outer
}
}