Skip to content

Fixes bundle 3 + new things#4212

Open
Henrybk wants to merge 3 commits into
masterfrom
FixBundle3
Open

Fixes bundle 3 + new things#4212
Henrybk wants to merge 3 commits into
masterfrom
FixBundle3

Conversation

@Henrybk
Copy link
Copy Markdown
Contributor

@Henrybk Henrybk commented May 19, 2026

Summary

This PR is a follow-up bundle that fixes and extends several systems touched by the previous routing/attack/config updates.

It focuses on:

  • safer portal recording and portal coordinate correction;
  • preventing duplicate or stale portal entries in portals.txt;
  • new cast-aware condition checks for self, player, and monster conditions;
  • attack target switching improvements using monster priority;
  • better target filtering after recent attack/LOS failures;
  • route-cost defaults for zeny and travel tickets;
  • better route string output for shop/path debugging;
  • safer portal-route handling in Task::Route;
  • autosell result handling fixes;
  • skill-delay status naming cleanup;
  • player job-change hook support;
  • new unit tests for cast-related conditions.

This PR is smaller than the previous bundle, but it improves several practical edge cases that appear during normal bot operation.


Highlights

  • Adds portalUpdatePosition.
  • Adds portal coordinate correction instead of blindly appending new portal records.
  • Adds replacePortalLUT to update or canonicalize portals.txt.
  • Adds cast-aware condition keys:
    • notWhileBeingCasted
    • whileBeingCasted
    • whenNoNearPartyMemberCasting
    • whenNearPartyMemberCasting
    • target_notWhileBeingCasted
    • target_whileBeingCasted
  • Adds helper functions:
    • actorIsBeingCastedOn
    • nearPartyMemberIsCasting
  • Adds CastConditionsTest.pm.
  • Improves attackChangeTarget so it can switch from an aggressive target to a higher-priority aggressive target.
  • Moves recent attack-failure filtering into getBestTarget.
  • Adds ZENY and TICKET route weight defaults.
  • Improves Task::CalcMapRoute::getRouteString.
  • Adds isPortalRoute route metadata.
  • Makes route stopping near occupied destinations less brittle.
  • Fixes autosell result state handling.
  • Uses stable skill handles for skill-delay statuses.
  • Adds job_changed hook for self job changes.
  • Adds all to default priority.txt.

Motivation

This PR addresses several edge cases found after the previous routing and automation changes.

Why portal updates are needed

Portal recording previously tended to append new portal records when the known coordinates were slightly wrong. This can create duplicate or near-duplicate portal lines such as:

pay_fild08 10 20 pay_fild07 50 60
pay_fild08 11 20 pay_fild07 50 61

In practice, this may happen when:

  • OpenKore enters a portal from a slightly different cell;
  • the destination landing coordinate is slightly different from the old table;
  • the route used an existing portal record but the real observed coordinates disagree;
  • portal data comes from old tables or old server behavior.

This PR adds a safer update path that can replace the old canonical portal line instead of appending duplicate data.


Why cast conditions are needed

Many automation blocks need to know whether a skill is already being cast.

Examples:

  • Do not cast Assumptio on a party member if someone is already casting Assumptio on them.
  • Do not cast Decrease Agility on a monster if another party member is already casting it.
  • Wait until a party member is casting Magnificat before using a follow-up behavior.
  • Avoid using a self skill while a specific skill is already being cast on self.

The existing whileCasting / notWhileCasting checks only describe the character’s own casting state. They do not answer:

  • “Is this actor being casted on?”
  • “Is this target already receiving this skill?”
  • “Is a nearby party member casting this skill?”

This PR adds those condition checks.


Why attack target switching needed refinement

The previous attackChangeTarget behavior could switch away from a passive current target to an aggressive one, but it did not handle a second important case:

The current target is aggressive, but another aggressive target has a higher monster priority.

This PR allows attackChangeTarget to switch to a higher-priority aggressive monster when appropriate.

This makes priority.txt more meaningful during combat, especially when the bot is already engaged but a more dangerous or more important monster appears.


Config changes

1. New portalUpdatePosition

New config key:

portalUpdatePosition 1

When enabled, OpenKore tries to update known portal coordinates after a map change if it detects that the real observed portal source/destination differs from the existing portals.txt record.

This works together with:

portalRecord
portalRecord_recompileAfter
portalCompile

Example default area:

portalCompile 1
portalRecord 2
portalRecord_recompileAfter 1
portalUpdatePosition 1

Behavior

When a route uses a known simple portal and the map changes, OpenKore stores a candidate describing the expected old portal line:

$ai_v{portalUpdatePosition_candidate} = {
    oldSourceMap => ...,
    oldSourceX   => ...,
    oldSourceY   => ...,
    oldDestMap   => ...,
    oldDestX     => ...,
    oldDestY     => ...,
    time         => time,
};

After the map change finishes, processPortalRecording compares:

  • old known source;
  • old known destination;
  • observed source;
  • observed arrival position.

If the portal is the same logical portal but the coordinates changed, OpenKore updates the existing portals.txt line instead of appending a duplicate.


2. New cast condition keys in config.txt

New shared self-condition keys:

notWhileBeingCasted
whileBeingCasted
whenNoNearPartyMemberCasting
whenNearPartyMemberCasting

New player target condition keys:

target_notWhileBeingCasted
target_whileBeingCasted

New monster target condition keys:

target_notWhileBeingCasted
target_whileBeingCasted

These are documented in the shared condition templates in control/config.txt.


3. New route weight defaults

control/routeweights.txt now includes:

# Additional route cost added for each 1 zeny actually spent while routing.
ZENY 0.1

# Additional route cost added for each travel ticket consumed while routing.
TICKET 100

These make the route cost model explicit for:

  • zeny spent on NPC warps;
  • travel tickets consumed during routing.

This is important because the route calculator now tracks real zeny and ticket usage in addition to walk cost.


4. Default priority.txt catch-all

control/priority.txt now includes:

all

This gives a catch-all priority entry by default.

This matters because the attack target-switching update now uses monster priority to decide whether to switch from one aggressive target to another higher-priority aggressive target.


Portal update system

1. processPortalRecording now supports update-only mode

Previously:

return unless $config{portalRecord};

Now:

return unless ($config{portalRecord} || $config{portalUpdatePosition});

This allows portal position updates to run even when the user wants to update known portal coordinates rather than only record unknown portals.


2. Portal update candidate tracking

Task::MapRoute::mapChanged now stores an update candidate when:

  • portalUpdatePosition is enabled;
  • the current map route has a portal solution;
  • the portal is a simple portal, not an NPC-step portal;
  • the portal exists in %portals_lut.

Example stored state:

$ai_v{portalUpdatePosition_candidate} = {
    oldSourceMap => $self->{mapSolution}[0]{map},
    oldSourceX   => $self->{mapSolution}[0]{pos}{x},
    oldSourceY   => $self->{mapSolution}[0]{pos}{y},
    oldDestMap   => $dest_map,
    oldDestX     => $dest_x,
    oldDestY     => $dest_y,
    time         => time,
};

This lets processPortalRecording know which portals.txt line the route expected to use.


3. Updating known portal positions

New helper:

_tryUpdateKnownPortalPositions

It checks whether the observed portal transition corresponds to the previously expected portal route.

If the source/destination maps match but the coordinates changed, it calls:

FileParsers::replacePortalLUT(...)

and logs:

Updated portal coordinates in portals.txt: oldSource (...) -> oldDest (...) became newSource (...) -> newDest (...)

Then it recompiles portals.


4. Updating nearby sibling portal records

New helper:

_tryUpdateNearbyPortalRecordSibling

This handles the case where no exact old candidate exists, but a nearby portal record likely refers to the same logical portal.

It uses:

_findNearbyPortalRecordSibling

with drift thresholds:

my $maxSourceDrift = 2;
my $maxDestDrift = 6;

Meaning:

  • source portal can drift up to 2 cells;
  • destination portal can drift up to 6 cells.

If a nearby sibling is found, it replaces that line in portals.txt.

Example log:

Updated nearby portal sibling in portals.txt: oldSource (...) -> oldDest (...) became newSource (...) -> newDest (...)

5. Portal recompilation helper

New helper:

recompilePortals

It performs:

Settings::loadByRegexp(qr/portals/);
Misc::compilePortals() if Misc::compilePortals_check();

This centralizes portal reload/recompile behavior after recording or updating.


replacePortalLUT

src/FileParsers.pm adds:

replacePortalLUT

This function replaces or canonicalizes portal records in portals.txt.

Behavior

It can:

  • append a new portal line if no old line exists;
  • replace an old source/destination line;
  • remove duplicate matching lines;
  • preserve comments and blank lines;
  • preserve leading whitespace;
  • preserve trailing data from old records when replacing;
  • avoid rewriting the file if the canonical line already exists.

Return values:

0 = failed / no update
1 = file changed
2 = line already canonical / no change needed

updatePortalLUT and updatePortalLUT2 now use replacePortalLUT

Before, both functions appended directly:

open FILE, ">>:utf8", $file;
print FILE "...";
close FILE;

Now they call:

replacePortalLUT(...)

This prevents duplicate entries and gives callers a meaningful result.


Example

Old file:

# Prontera portal
prontera 100 100 izlude 50 50

Observed transition:

prontera 101 100 izlude 51 50

With portalUpdatePosition 1, OpenKore can replace the old line:

# Prontera portal
prontera 101 100 izlude 51 50

instead of appending:

# Prontera portal
prontera 100 100 izlude 50 50
prontera 101 100 izlude 51 50

Cast-aware condition system

1. New helper: actorIsBeingCastedOn

actorIsBeingCastedOn($target, $skills)

Checks whether any visible caster is currently casting one of the listed skills on the target actor.

It scans:

$char
@$playersList
@$monstersList
@$npcsList
@$slavesList
@$elementalsList

It checks each caster’s:

$caster->{casting}

and compares:

$cast->{targetID}
$cast->{target}{ID}
$cast->{skill}->getHandle()

The skill list is comma-separated.

Example skill list:

Blessing,HP_ASSUMPTIO,MER_DECAGI

2. New helper: nearPartyMemberIsCasting

nearPartyMemberIsCasting($skills)

Checks whether a visible party member is currently casting one of the listed skills.

It only considers actors that:

  • are in playersList;
  • have an ID;
  • are in $char->{party}{users};
  • have an active casting structure;
  • are casting a matching skill handle.

This is useful for avoiding duplicate party support skill casts.


3. Self-condition cast checks

New checkSelfCondition keys:

notWhileBeingCasted
whileBeingCasted
whenNoNearPartyMemberCasting
whenNearPartyMemberCasting

notWhileBeingCasted

Fails if one of the listed skills is currently being cast on self.

Example:

useSelf_skill Blessing {
    lvl 10
    notWhileBeingCasted Blessing
    # [checkSelfCondition]
}

Meaning:

  • do not use Blessing if someone is already casting Blessing on me.

whileBeingCasted

Passes only if one of the listed skills is currently being cast on self.

Example:

useSelf_skill Emergency Skill {
    whileBeingCasted HP_ASSUMPTIO
    # [checkSelfCondition]
}

Meaning:

  • only use this while HP_ASSUMPTIO is being cast on me.

whenNoNearPartyMemberCasting

Fails if a visible party member is casting one of the listed skills.

Example:

useSelf_skill Magnificat {
    lvl 5
    whenNoNearPartyMemberCasting PR_MAGNIFICAT
    # [checkSelfCondition]
}

Meaning:

  • do not cast Magnificat if a party member is already casting PR_MAGNIFICAT.

whenNearPartyMemberCasting

Passes only if a visible party member is casting one of the listed skills.

Example:

useSelf_skill Follow Up Skill {
    whenNearPartyMemberCasting PR_MAGNIFICAT
    # [checkSelfCondition]
}

Meaning:

  • only run this block while a party member is casting PR_MAGNIFICAT.

4. Player target cast checks

New checkPlayerCondition keys:

target_notWhileBeingCasted
target_whileBeingCasted

target_notWhileBeingCasted

Fails if the player target is already receiving one of the listed casts.

Example:

partySkill Assumptio {
    lvl 5
    target
    target_notWhileBeingCasted HP_ASSUMPTIO
    # [checkSelfCondition]
    # [checkPlayerCondition]
}

Meaning:

  • do not cast Assumptio if another actor is already casting HP_ASSUMPTIO on that party target.

target_whileBeingCasted

Passes only if the player target is receiving one of the listed casts.

Example:

partySkill Support Chain {
    target
    target_whileBeingCasted HP_ASSUMPTIO
    # [checkPlayerCondition]
}

5. Monster target cast checks

New checkMonsterCondition keys:

target_notWhileBeingCasted
target_whileBeingCasted

target_notWhileBeingCasted

Fails if the monster target is already receiving one of the listed casts.

Example:

attackSkillSlot Decrease Agility {
    lvl 10
    target_notWhileBeingCasted MER_DECAGI
    # [checkSelfCondition]
    # [checkMonsterCondition]
}

Meaning:

  • do not cast Decrease Agility if someone is already casting it on that monster.

target_whileBeingCasted

Passes only if the monster is receiving one of the listed casts.

Example:

attackSkillSlot Follow Up {
    lvl 10
    target_whileBeingCasted MER_DECAGI
    # [checkMonsterCondition]
}

Cast condition examples

Prevent duplicate Assumptio on party targets
partySkill Assumptio {
    lvl 5
    target
    target_hp < 95%
    target_notWhileBeingCasted HP_ASSUMPTIO
    # [checkSelfCondition]
    # [checkPlayerCondition]
}
Prevent duplicate Magnificat casts in party
useSelf_skill Magnificat {
    lvl 5
    sp > 40%
    whenNoNearPartyMemberCasting PR_MAGNIFICAT
    # [checkSelfCondition]
}
Do not debuff a monster while another actor is already casting the same debuff
attackSkillSlot Decrease Agility {
    lvl 10
    target_notWhileBeingCasted MER_DECAGI
    # [checkSelfCondition]
    # [checkMonsterCondition]
}
Only run a block while a party member is casting a specific skill
useSelf_skill Prepare Followup {
    whenNearPartyMemberCasting PR_MAGNIFICAT
    # [checkSelfCondition]
}
Only run a block while self is being targeted by a skill cast
useSelf_skill Emergency Response {
    whileBeingCasted Blessing,HP_ASSUMPTIO
    # [checkSelfCondition]
}

Attack logic updates

1. attackChangeTarget can now switch to higher-priority aggressive targets

Previously, target switching mainly handled:

current target is not aggressive, but another aggressive monster exists.

Now it also handles:

current target is aggressive, but another aggressive monster has higher priority.

New logic compares:

my $current_priority = Misc::monsterPriority($target->{name}, $target->{nameID});
my $new_priority = Misc::monsterPriority($new_target->{name}, $new_target->{nameID});

It can switch when:

$switch_to_aggressive

or:

$switch_to_higher_priority

Example log when switching to a higher-priority aggressive target:

Changing target to higher priority monster: oldTarget -> newTarget.

Example log when switching from passive to aggressive:

Your target is not aggressive: oldTarget, changing target to aggressive: newTarget.

2. Why this matters

With priority.txt now including:

all

and user-specific priorities, the bot can make better choices in crowded combat.

Example:

Raydric
Mimic
all

If the bot is attacking a normal aggressive monster and a higher-priority aggressive monster appears, attackChangeTarget can switch to the more important target.


3. Recent failed targets are filtered inside getBestTarget

New helper:

_targetRecentlyFailedAttack

It checks:

$target->{attack_failedLOS}
$target->{$failed_timeout_key}

using:

$timeout{ai_attack_failedLOS}
$timeout{ai_attack_unfail}

getBestTarget now skips recently failed targets.

This centralizes failed-target filtering instead of scattering it in processAutoAttack.


4. getBestTarget now respects attackRouteMaxPathDistance

Target selection now skips monsters whose direct block distance exceeds:

attackRouteMaxPathDistance

It also skips pathfinding candidates whose route solution length exceeds the same limit.

This prevents target selection from choosing monsters that are technically visible or known but too far to route to under the configured attack path limits.


5. Removed old failed-target filtering from processAutoAttack

The old logic in processAutoAttack directly skipped monsters using:

attack_failed
attack_failedLOS

That filtering was removed from the loop because it is now handled in getBestTarget.

This keeps target filtering more consistent across different callers.


Attack code documentation cleanup

src/AI/Attack.pm now includes many clarifying comments around the attack loop.

This does not only document the code; it makes future review of the attack state machine easier by explaining the purpose of each stage.

Documented areas include:

  • attack queue dispatch;
  • moving-to-attack vs active-attacking stages;
  • target disappearance;
  • plugin veto hook;
  • give-up timeout;
  • target switching;
  • kill-steal/cleanness checks;
  • attack_auto == 3;
  • route reset when the target moves;
  • give-up timer adjustment during suspend/avoidance;
  • heavy attack loop throttling;
  • attack method selection;
  • combo window priority;
  • skill fallback;
  • run-from-target behavior;
  • out-of-range/LOS handling;
  • meeting-position routing;
  • tank-mode behavior.

This is especially useful because AI::Attack contains complex interactions between movement prediction, skill selection, kiting, route resets, and target switching.


Route calculation updates

1. getRouteString now includes coordinates and walk cost

Old route string example:

payon -> pay_arche -> pay_dun00 -> pay_dun01

New format:

payon (228,329) [walk 30] -> pay_arche (36,131) [walk 55] -> pay_dun01 (286,25)

This is much more useful for debugging:

  • searchshop;
  • map route choices;
  • portal route choices;
  • warp-item path decisions;
  • route weight behavior.

2. searchStep debug improvements

Task::CalcMapRoute::searchStep now tracks:

$self->{searchStepCount}

and logs:

[CalcMapRoute] [searchStep N] [size X] key (cost Y)

This gives better visibility into route expansion behavior.


3. Route expansion cleanup

searchStep was restructured to:

  • move one selected heap/openlist parent into closelist;
  • check for target completion;
  • populate synthetic route branches;
  • return early if no LOS children exist;
  • expand portal/NPC/airship children consistently.

This improves readability and reduces nested control flow.


4. canAddOpenListEntry TODO

A TODO was added around duplicate route states:

# TODO: After fixing the current route-cost mismatch bug, add a stricter
# duplicate-state guard here so the same portal/path key is not re-added to
# the open list/heap over and over with alternate costs during expansion.

This documents a known route-cost/heap duplicate-state issue for future work.


5. Dynamic portal debug noise reduction

The verbose debug message inside isPortalDestinationEnabledForRoute when a dynamic portal group is blocked was removed.

The actual blocking behavior remains, but the route calculation becomes less noisy.


MapRoute and Route changes

1. Portal route metadata

Task::MapRoute now passes:

isPortalRoute => 1

when creating route subtasks for portal movement.

Task::Route now allows:

isPortalRoute

as a valid constructor argument.

This makes it possible to distinguish normal routes from portal-entry routes.


2. Missing portal proximity tolerance

The missing portal check changed from:

blockDistance(...) == 0

to:

blockDistance(...) <= 1

This is more forgiving when the actor ends up adjacent to the expected portal cell rather than exactly on it.

This helps with real in-game movement where the client/server may stop the character one cell away from the portal trigger cell.


3. Occupied destination stopping logic

Task::Route now stops one cell away from an occupied destination when:

isCellOccupied($solution->[-1], $self->{actor})
&& blockDistance($current_pos_to, $self->{dest}{pos}) <= 1

Previously it depended on stepsleft == 2 and not being a meeting subroute.

The new condition is more general and uses actual distance to destination.

This helps when the destination cell is occupied by another actor, portal object, or obstacle and the route is already close enough.


Network/packet handling changes

1. Skill delay statuses now use stable skill handles

skill_post_delay and skill_post_delaylist now set status using skill handles:

$skillHandleName . "_DELAY"

instead of localized display names plus " Delay".

Before:

$skillName . " " . $status

After:

$skillHandleName . "_DELAY"

Example:

AL_HEAL_DELAY
PR_MAGNIFICAT_DELAY
HP_ASSUMPTIO_DELAY

This is more stable for config/status checks because it avoids localized skill names.


2. Self job-change hook

sprite_change now distinguishes self job changes from other player job changes.

For self:

Plugins::callHook('job_changed', {
    old_job => $old_job,
    new_job => $value1,
});

It also logs:

Your job changed from oldJob to: newJob

For other players, behavior remains a normal message:

Player changed Job to: JobName

This gives plugins a clean way to react to the player’s own job changes.


3. Sell result handling fix

sell_result now counts sold items using:

my $itemCount = scalar @sellList;

and only prints item count if greater than zero:

message TF("Sold %d items.\n", $itemCount), "success" if ($itemCount > 0);

It also now finds the sellAuto action by index:

my $sellAutoIndex = AI::findAction("sellAuto");

and sets:

AI::args($sellAutoIndex)->{recv_sell_packet} = 1;

This is safer than assuming the current AI action is sellAuto.


4. Casting packets now store targetID

skill_cast now stores:

targetID => $targetID

inside:

$source->{casting}

This is required by the new actorIsBeingCastedOn helper.

It lets cast conditions reliably identify the target actor even when the target object reference is missing or stale.


Autosell fix

Inside processAutoSell, the sell list now uses the global @sellList directly instead of a local @sellItems.

Before:

my @sellItems;
...
push @sellItems, \%obj;
...
completeNpcSell(\@sellItems);

After:

@sellList = ();
...
push @sellList, \%obj;
...
completeNpcSell(\@sellList);

This matches the later sell_result logic, which checks @sellList.

This fixes mismatches where the sell result handler could not accurately report or track the items that were sent for selling.


Tests

New test file:

src/test/CastConditionsTest.pm

Registered in:

src/test/unittests.pl

Cast condition tests cover

The new tests cover:

  • self condition fails while a specific skill is being cast on self;
  • self condition passes when a different skill is being cast;
  • self condition requires a specific skill being cast on self;
  • self condition blocks when a nearby party member is casting a specific skill;
  • self condition ignores non-party players casting the same skill;
  • self condition requires a nearby party member to be casting a specific skill;
  • player target condition blocks duplicate target casts;
  • player target condition requires a specific target cast;
  • monster target condition blocks duplicate target casts;
  • monster target condition requires a specific target cast.

Example tested scenarios

Self being casted on

$caster->{casting} = {
    skill => Skill->new(auto => 'Blessing'),
    targetID => $char->{ID},
    target => $char,
};

Config:

selftest_notWhileBeingCasted => 'Blessing'

Expected:

checkSelfCondition fails

Party member casting nearby

$party_caster->{casting} = {
    skill => Skill->new(auto => 'PR_MAGNIFICAT'),
    targetID => $party_caster->{ID},
    target => $party_caster,
};

Config:

selftest_whenNoNearPartyMemberCasting => 'PR_MAGNIFICAT'

Expected:

checkSelfCondition fails while party member is casting PR_MAGNIFICAT

Player target already receiving Assumptio

$caster->{casting} = {
    skill => Skill->new(auto => 'HP_ASSUMPTIO'),
    targetID => $target->{ID},
    target => $target,
};

Config:

playertest_target_notWhileBeingCasted => 'HP_ASSUMPTIO'

Expected:

checkPlayerCondition fails

Monster target already receiving Decrease Agility

$caster->{casting} = {
    skill => Skill->new(auto => 'MER_DECAGI'),
    targetID => $monster->{ID},
    target => $monster,
};

Config:

monstertest_target_notWhileBeingCasted => 'MER_DECAGI'

Expected:

checkMonsterCondition fails

Example configurations

Prevent duplicate party Assumptio casts
partySkill Assumptio {
    lvl 5
    target
    target_hp < 95%
    target_notWhileBeingCasted HP_ASSUMPTIO
    # [checkSelfCondition]
    # [checkPlayerCondition]
}
Prevent duplicate Magnificat in party
useSelf_skill Magnificat {
    lvl 5
    sp > 40%
    whenNoNearPartyMemberCasting PR_MAGNIFICAT
    # [checkSelfCondition]
}
Only cast when a party member is casting Magnificat
useSelf_skill Follow Up {
    whenNearPartyMemberCasting PR_MAGNIFICAT
    # [checkSelfCondition]
}
Do not debuff a monster while the same debuff is already being cast
attackSkillSlot Decrease Agility {
    lvl 10
    target_notWhileBeingCasted MER_DECAGI
    # [checkSelfCondition]
    # [checkMonsterCondition]
}
Only run a monster skill while a target is being casted on
attackSkillSlot Follow Up Skill {
    lvl 10
    target_whileBeingCasted MER_DECAGI
    # [checkSelfCondition]
    # [checkMonsterCondition]
}
Attack target switching with priority
attackChangeTarget 1
attackAuto 2
attackAuto_party 1

Example priority.txt:

Raydric
Mimic
all

If currently attacking a lower-priority aggressive monster and a higher-priority aggressive monster appears, OpenKore can switch targets.

Portal update behavior
portalRecord 2
portalRecord_recompileAfter 1
portalUpdatePosition 1

Expected behavior:

  • known portal route is used;
  • map changes;
  • observed source/destination differs slightly from old portals.txt;
  • old line is replaced;
  • portals are reloaded/recompiled.

Manual test plan

Portal update system

  • Enable portalUpdatePosition 1.
  • Use a route through a known simple portal.
  • Force or simulate a slight source/destination coordinate mismatch.
  • Verify portals.txt updates the old line instead of appending a duplicate.
  • Verify comments and blank lines in portals.txt are preserved.
  • Verify portalRecord_recompileAfter 1 reloads/recompiles portals.
  • Verify no update happens when the old and observed coordinates are identical.
  • Verify nearby sibling portal correction works when exact old coordinates are not found.

Cast conditions

  • Test notWhileBeingCasted on self.
  • Test whileBeingCasted on self.
  • Test whenNoNearPartyMemberCasting.
  • Test whenNearPartyMemberCasting.
  • Test target_notWhileBeingCasted for partySkill.
  • Test target_whileBeingCasted for partySkill.
  • Test target_notWhileBeingCasted for attackSkillSlot.
  • Test target_whileBeingCasted for attackSkillSlot.
  • Verify non-party players casting the same skill do not trigger party-member checks.
  • Verify comma-separated skill lists work.

Attack target switching

  • Enable attackChangeTarget.
  • Attack a passive monster while an aggressive monster is nearby.
  • Verify OpenKore switches to the aggressive monster.
  • Attack an aggressive low-priority monster.
  • Spawn or approach a higher-priority aggressive monster.
  • Verify OpenKore switches to the higher-priority aggressive monster.
  • Verify recently failed LOS/path targets are skipped.
  • Verify targets beyond attackRouteMaxPathDistance are skipped.

Autosell

  • Trigger sellAuto.
  • Verify @sellList is populated.
  • Verify sell_result reports the correct item count.
  • Verify recv_sell_packet is set on the queued sellAuto action, not necessarily the current action.

Skill delay statuses

  • Use a skill that receives post-delay.
  • Verify status is stored as <SKILL_HANDLE>_DELAY.
  • Verify delay-list packet also uses handle-based delay statuses.

Job change hook

  • Change the player’s own job.
  • Verify job_changed hook is called with old/new job IDs.
  • Verify other players changing jobs still log normally.

Route behavior

  • Verify route strings now include map coordinates and walk cost.
  • Verify portal routes pass isPortalRoute.
  • Verify route can stop adjacent to occupied destination cells.
  • Verify missing portal detection works when the actor is within distance <= 1.

Unit tests

Suggested command:

perl src/test/unittests.pl

New test registration:

CastConditionsTest

New files

src/test/CastConditionsTest.pm

Modified files

control/config.txt
control/priority.txt
control/routeweights.txt
src/AI/Attack.pm
src/AI/CoreLogic.pm
src/FileParsers.pm
src/Misc.pm
src/Network/Receive.pm
src/Task/CalcMapRoute.pm
src/Task/MapRoute.pm
src/Task/Route.pm
src/test/CastConditionsTest.pm
src/test/unittests.pl

Compatibility notes

  • portalUpdatePosition defaults to 1, so portal recording can now update known portal coordinates.
  • updatePortalLUT and updatePortalLUT2 now go through replacePortalLUT instead of direct append.
  • Skill delay statuses now use skill handles with _DELAY, not localized names plus Delay.
  • Cast condition checks compare against skill handles.
  • Users should prefer skill handles in the new cast condition keys, for example:
    • HP_ASSUMPTIO
    • PR_MAGNIFICAT
    • MER_DECAGI
  • priority.txt now includes all, which can affect target priority behavior when attackChangeTarget is enabled.
  • getBestTarget now skips recently failed targets and targets exceeding attackRouteMaxPathDistance.
  • Task::Route now accepts isPortalRoute as route metadata.

Review focus

Please review carefully:

  • replacePortalLUT behavior with comments, blank lines, existing duplicates, and stepped portal lines.
  • Portal coordinate update behavior in processPortalRecording.
  • Candidate tracking in Task::MapRoute::mapChanged.
  • The nearby sibling threshold values:
    • source drift 2;
    • destination drift 6.
  • Whether portalUpdatePosition 1 should be default-enabled.
  • Cast-condition skill-handle matching behavior.
  • actorIsBeingCastedOn scanning all actor lists.
  • nearPartyMemberIsCasting only considering visible party members.
  • Attack target switching from aggressive target to higher-priority aggressive target.
  • getBestTarget filtering recently failed targets.
  • attackRouteMaxPathDistance filtering in direct and pathfinding branches.
  • Autosell transition from local @sellItems to global @sellList.
  • Skill delay status naming compatibility.
  • job_changed hook naming and payload.

Short changelog

  • Added portalUpdatePosition.
  • Added portal coordinate update/canonicalization logic.
  • Added replacePortalLUT.
  • Updated updatePortalLUT and updatePortalLUT2 to avoid duplicate appends.
  • Added cast-aware self/player/monster condition keys.
  • Added actorIsBeingCastedOn.
  • Added nearPartyMemberIsCasting.
  • Added CastConditionsTest.
  • Added ZENY and TICKET route weight defaults.
  • Added catch-all all to priority.txt.
  • Improved attackChangeTarget to switch to higher-priority aggressive monsters.
  • Moved recent failed-target filtering into getBestTarget.
  • Added attackRouteMaxPathDistance filtering to target selection.
  • Improved Task::CalcMapRoute::getRouteString.
  • Added isPortalRoute route metadata.
  • Improved missing portal distance tolerance.
  • Improved occupied destination handling in Task::Route.
  • Fixed autosell list/result handling.
  • Changed skill delay statuses to handle-based _DELAY statuses.
  • Added job_changed hook for self job changes.
  • Added targetID to skill casting state.

Henrybk referenced this pull request May 19, 2026
* Added missing keys to config

* bundle

* Update config.txt

* Update Misc.pm

* pot

* Update config.txt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant